Skip to content

Commit

Permalink
feat: remove cache from local provider and save in instance variable
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Aug 26, 2024
1 parent 3c39170 commit 7f5b664
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 193 deletions.
76 changes: 35 additions & 41 deletions packages/metadata/src/providers/localFileMetadata.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { existsSync, readFileSync } from "fs";
import { z } from "zod";

import {
Cache,
ILogger,
Token,
TokenType,
Expand All @@ -17,21 +16,23 @@ export const LOCALFILE_METADATA_PREFIX = "local-metadata";

/**
* Represents a provider that retrieves metadata from local files.
* Note: Files are read only once and saved to instance variables.
*/
export class LocalFileMetadataProvider implements IMetadataProvider {
private readonly chainMetadata: ZKChainMetadata;
private readonly tokenMetadata: Token<TokenType>[];

/**
* Constructs a new instance of the LocalFileMetadataProvider class.
* @param tokenJsonPath The path to the token JSON file.
* @param chainJsonPath The path to the chain JSON file.
* @param logger The logger instance.
* @param cache The cache instance.
* @throws {FileNotFound} if any of the files is not found.
*/
constructor(
private readonly tokenJsonPath: string,
private readonly chainJsonPath: string,
private readonly logger: ILogger,
private readonly cache: Cache,
) {
if (!existsSync(tokenJsonPath)) {
throw new FileNotFound(tokenJsonPath);
Expand All @@ -40,58 +41,51 @@ export class LocalFileMetadataProvider implements IMetadataProvider {
if (!existsSync(chainJsonPath)) {
throw new FileNotFound(chainJsonPath);
}

this.tokenMetadata = this.readAndParseTokenMetadata();
this.chainMetadata = this.readAndParseChainMetadata();
}

/** @inheritdoc */
async getChainsMetadata(): Promise<ZKChainMetadata> {
let cachedData = await this.cache.get<ZKChainMetadata>(
`${LOCALFILE_METADATA_PREFIX}-chains`,
);
if (!cachedData) {
const jsonData = readFileSync(this.chainJsonPath, "utf-8");
const parsed = JSON.parse(jsonData);

const validatedData = z.array(ChainSchema).safeParse(parsed);

if (!validatedData.success) {
this.logger.error(`Invalid ZKChains metadata: ${validatedData.error.errors}`);
throw new InvalidSchema("Invalid ZKChains metadata");
}

cachedData = validatedData.data.reduce((acc, chain) => {
const { chainId, ...rest } = chain;
const chainIdBn = BigInt(chainId);
acc.set(chainIdBn, { ...rest, chainId: chainIdBn });
return acc;
}, new Map<bigint, ZKChainMetadataItem>());

await this.cache.set(`${LOCALFILE_METADATA_PREFIX}-chains`, cachedData);
return Promise.resolve(this.chainMetadata);
}

readAndParseChainMetadata() {
const jsonData = readFileSync(this.chainJsonPath, "utf-8");
const parsed = JSON.parse(jsonData);

const validatedData = z.array(ChainSchema).safeParse(parsed);

if (!validatedData.success) {
this.logger.error(`Invalid ZKChains metadata: ${validatedData.error.errors}`);
throw new InvalidSchema("Invalid ZKChains metadata");
}

return cachedData;
return validatedData.data.reduce((acc, chain) => {
const { chainId, ...rest } = chain;
const chainIdBn = BigInt(chainId);
acc.set(chainIdBn, { ...rest, chainId: chainIdBn });
return acc;
}, new Map<bigint, ZKChainMetadataItem>());
}

/** @inheritdoc */
async getTokensMetadata(): Promise<Token<TokenType>[]> {
let cachedData = await this.cache.get<Token<TokenType>[]>(
`${LOCALFILE_METADATA_PREFIX}-tokens`,
);
if (!cachedData) {
const jsonData = readFileSync(this.tokenJsonPath, "utf-8");
const parsed = JSON.parse(jsonData);

const validatedData = z.array(TokenSchema).safeParse(parsed);
return Promise.resolve(this.tokenMetadata);
}

if (!validatedData.success) {
this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`);
throw new InvalidSchema("Invalid Tokens metadata");
}
readAndParseTokenMetadata() {
const jsonData = readFileSync(this.tokenJsonPath, "utf-8");
const parsed = JSON.parse(jsonData);

cachedData = validatedData.data;
const validatedData = z.array(TokenSchema).safeParse(parsed);

await this.cache.set(`${LOCALFILE_METADATA_PREFIX}-tokens`, cachedData);
if (!validatedData.success) {
this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`);
throw new InvalidSchema("Invalid Tokens metadata");
}

return cachedData;
return validatedData.data;
}
}
193 changes: 41 additions & 152 deletions packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from "fs";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { Cache, ILogger, ZKChainMetadataItem } from "@zkchainhub/shared";
import { ILogger } from "@zkchainhub/shared";

import { FileNotFound, InvalidSchema, LocalFileMetadataProvider } from "../../../src/internal";
import { mockChainData, mockTokenData } from "../../fixtures/metadata.fixtures.js";
Expand All @@ -14,14 +14,6 @@ const mockLogger: ILogger = {
debug: vi.fn(),
};

const mockCache: Cache = {
store: {} as any,
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
reset: vi.fn(),
};

// Mock the file system functions
vi.mock("fs", () => ({
existsSync: vi.fn(),
Expand All @@ -38,90 +30,44 @@ describe("LocalFileMetadataProvider", () => {
vi.spyOn(fs, "existsSync").mockReturnValueOnce(false);

expect(
() =>
new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
),
() => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger),
).toThrow(FileNotFound);
});

it("throws FileNotFound error if chain JSON file does not exist", () => {
vi.spyOn(fs, "existsSync").mockReturnValueOnce(true).mockReturnValueOnce(false);

expect(
() =>
new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
),
() => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger),
).toThrow(FileNotFound);
});

it("not throws any error if both token JSON file and chain JSON file exist", () => {
vi.spyOn(fs, "existsSync").mockReturnValue(true);
expect(
() =>
new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
),
).not.toThrow();
});
});
it("throws error on token schema validation", async () => {
const invalidTokenData = [
{
name: "Ethereum",
symbol: "ETH",
decimals: 18,
rpcUrl: "https://mainnet.infura.io/v3/your-infura-key",
explorerUrl: "https://etherscan.io",
},
{
name: "Wrapped Ether",
decimals: 18.5,
rpcUrl: "https://mainnet.infura.io/v3/your-infura-key",
explorerUrl: "https://etherscan.io",
},
];

describe("getChainsMetadata", () => {
it("return the cached chain data if it exists", async () => {
const cachedData = new Map<bigint, ZKChainMetadataItem>();
vi.spyOn(fs, "existsSync").mockReturnValue(true);
vi.spyOn(mockCache, "get").mockResolvedValue(cachedData);

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

const result = await provider.getChainsMetadata();
vi.spyOn(fs, "readFileSync").mockReturnValueOnce(JSON.stringify(invalidTokenData));

expect(result).toEqual(cachedData);
expect(mockCache.get).toHaveBeenCalledWith("local-metadata-chains");
expect(fs.readFileSync).not.toHaveBeenCalled();
});

it("read and parse the chain JSON file if the cached chain data does not exist", async () => {
vi.spyOn(mockCache, "get").mockResolvedValue(undefined);
vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(mockChainData));
vi.spyOn(fs, "existsSync").mockReturnValue(true);
const expectedMap = new Map<bigint, ZKChainMetadataItem>();
for (const chain of mockChainData) {
const { chainId, ...rest } = chain;
const chainIdBn = BigInt(chainId);
expectedMap.set(chainIdBn, { ...rest, chainId: chainIdBn } as ZKChainMetadataItem);
}

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

const result = await provider.getChainsMetadata();

expect(result).toEqual(expectedMap);
expect(fs.readFileSync).toHaveBeenCalledWith("chain.json", "utf-8");
expect(mockCache.set).toHaveBeenCalledWith("local-metadata-chains", expectedMap);
expect(
() => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger),
).toThrow(InvalidSchema);
});

it("throws an error if schema validation fails", async () => {
it("throws error on chain schema validation", async () => {
const invalidChainData = [
{
name: "Ethereum",
Expand All @@ -140,88 +86,31 @@ describe("LocalFileMetadataProvider", () => {
},
];

vi.spyOn(mockCache, "get").mockResolvedValue(undefined);
vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(invalidChainData));
vi.spyOn(fs, "existsSync").mockReturnValue(true);

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

await expect(provider.getChainsMetadata()).rejects.toThrow(InvalidSchema);
});
});

describe("getTokensMetadata", () => {
it("returns the cached token data if it exists", async () => {
vi.spyOn(fs, "existsSync").mockReturnValue(true);
vi.spyOn(mockCache, "get").mockResolvedValue(mockTokenData);
vi.spyOn(fs, "readFileSync")
.mockReturnValueOnce(JSON.stringify(mockTokenData))
.mockReturnValue(JSON.stringify(invalidChainData));

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

const result = await provider.getTokensMetadata();

expect(result).toBe(mockTokenData);
expect(mockCache.get).toHaveBeenCalledWith("local-metadata-tokens");
expect(fs.readFileSync).not.toHaveBeenCalled();
expect(
() => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger),
).toThrow(InvalidSchema);
});

it("read and parse the token JSON file if the cached token data does not exist", async () => {
vi.spyOn(mockCache, "get").mockResolvedValue(undefined);
vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(mockTokenData));
it("read, parse and saves to variables the file data", async () => {
vi.spyOn(fs, "existsSync").mockReturnValue(true);
vi.spyOn(fs, "readFileSync")
.mockReturnValueOnce(JSON.stringify(mockTokenData))
.mockReturnValueOnce(JSON.stringify(mockChainData));

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

const result = await provider.getTokensMetadata();
const provider = new LocalFileMetadataProvider("token.json", "chain.json", mockLogger);
const chainMetadata = await provider.getChainsMetadata();
const tokenMetadata = await provider.getTokensMetadata();

expect(result).toEqual(mockTokenData);
expect(provider).toBeDefined();
expect(tokenMetadata).toEqual(provider["tokenMetadata"]);
expect(chainMetadata).toEqual(provider["chainMetadata"]);
expect(fs.readFileSync).toHaveBeenCalledWith("chain.json", "utf-8");
expect(fs.readFileSync).toHaveBeenCalledWith("token.json", "utf-8");
expect(mockCache.set).toHaveBeenCalledWith("local-metadata-tokens", mockTokenData);
});

it("throws an error if schema validation fails", async () => {
const invalidTokenData = [
{
name: "Ethereum",
symbol: "ETH",
decimals: 18,
rpcUrl: "https://mainnet.infura.io/v3/your-infura-key",
explorerUrl: "https://etherscan.io",
},
{
name: "Wrapped Ether",
decimals: 18.5,
rpcUrl: "https://mainnet.infura.io/v3/your-infura-key",
explorerUrl: "https://etherscan.io",
},
];

vi.spyOn(mockCache, "get").mockResolvedValue(undefined);
vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(invalidTokenData));
vi.spyOn(fs, "existsSync").mockReturnValue(true);

const provider = new LocalFileMetadataProvider(
"token.json",
"chain.json",
mockLogger,
mockCache,
);

await expect(provider.getTokensMetadata()).rejects.toThrow(InvalidSchema);
});
});
});

0 comments on commit 7f5b664

Please sign in to comment.