From 6a14ab6102c79de383591366b854acb26baf26bd Mon Sep 17 00:00:00 2001 From: CNine Date: Thu, 24 Mar 2022 01:01:52 +0800 Subject: [PATCH] feat: name records cache interface --- src/client.ts | 46 +++++++++++++++++++- src/index.ts | 1 + src/internal/base.ts | 14 ++++++ src/internal/interfaces.ts | 10 +++++ test/client.test.ts | 88 +++++++++++++++++++++++++++++++++++++- 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/internal/interfaces.ts diff --git a/src/client.ts b/src/client.ts index 65a0d4b..bfb37bb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -97,8 +97,50 @@ export class IcNamingClient extends IcNamingClientBase { // --- Resolver --- - async getRecordsOfName(name: string) { - return await throwable(() => this.resolver.get_record_value(name)); + async getRecordsOfName(name: string): Promise> { + let cachedRecords: Array<[string, string]> | undefined; + + await this.dispatchNameRecordsCache(async (store) => { + const target = await store.getRecordsByName(name); + + if (target) { + if (Date.now() >= target.expired_at) { + const { ttl } = await this.getRegistryOfName(name); + + await store.setRecordsByName(name, { + name, + expired_at: new Date(Date.now() + Number(ttl) * 1000).getTime(), + }); + } else { + if (target.records) { + cachedRecords = target.records.map((i) => [i.key, i.value]); + } + } + } else { + const { ttl } = await this.getRegistryOfName(name); + await store.setRecordsByName(name, { + name, + expired_at: new Date(Date.now() + Number(ttl) * 1000).getTime(), + }); + } + }); + + if (cachedRecords) return cachedRecords; + + const result = await throwable(() => this.resolver.get_record_value(name)); + + await this.dispatchNameRecordsCache(async (store) => { + const target = await store.getRecordsByName(name); + + if (target) { + await store.setRecordsByName(name, { + ...target, + records: result.map(([key, value]) => ({ key, value })), + }); + } + }); + + return result; } // --- Favorites --- diff --git a/src/index.ts b/src/index.ts index 57989f3..cd5c53b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export * from "./internal/interfaces"; export * from "./internal/errors"; export * from "./client"; diff --git a/src/internal/base.ts b/src/internal/base.ts index 1a5cd24..4c34469 100644 --- a/src/internal/base.ts +++ b/src/internal/base.ts @@ -17,11 +17,17 @@ import { MAINNET_CANISTER_ID_GROUP, TICP_CANISTER_ID_GROUP, } from "./config"; +import { NameRecordsCacheStore } from "./interfaces"; export interface IcNamingClientInitOptions { net: "MAINNET" | "TICP"; mode: "production" | "local"; httpAgentOptions?: HttpAgentOptions; + nameRecordsCacheStore?: NameRecordsCacheStore; +} + +export interface InternalStores { + nameRecordsCacheStore?: NameRecordsCacheStore; } export class IcNamingClientBase { @@ -91,6 +97,14 @@ export class IcNamingClientBase { canisterId, }) as ActorSubclass; } + + public async dispatchNameRecordsCache( + fn: (store: NameRecordsCacheStore) => Promise + ) { + if (this._options.nameRecordsCacheStore) { + await fn(this._options.nameRecordsCacheStore); + } + } } const toDidModuleType = (module: T) => { diff --git a/src/internal/interfaces.ts b/src/internal/interfaces.ts new file mode 100644 index 0000000..f2ed9e3 --- /dev/null +++ b/src/internal/interfaces.ts @@ -0,0 +1,10 @@ +export interface NameRecordsValue { + name: string; + expired_at: number; + records?: Array<{ key: string; value: string }>; +} + +export interface NameRecordsCacheStore { + getRecordsByName: (name: string) => Promise; + setRecordsByName: (name: string, data: NameRecordsValue) => Promise; +} diff --git a/test/client.test.ts b/test/client.test.ts index a19a637..32754d9 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,5 +1,9 @@ import { Principal } from "@dfinity/principal"; -import { IcNamingClient } from "../src"; +import { + IcNamingClient, + NameRecordsCacheStore, + NameRecordsValue, +} from "../src"; jest.mock("../src/internal/base"); @@ -202,6 +206,88 @@ describe("IcNamingClient", () => { expect(client["resolver"].get_record_value).toBeCalledTimes(1); }); + it("should return records of name with cache store", async () => { + const simpleMemoryNameRecordsCacheStore = { + map: {} as Record, + async getRecordsByName(name: string) { + return this.map[name]; + }, + async setRecordsByName(name: string, value: NameRecordsValue) { + this.map[name] = value; + }, + }; + const spyStoreGet = jest.spyOn( + simpleMemoryNameRecordsCacheStore, + "getRecordsByName" + ); + const spyStoreSet = jest.spyOn( + simpleMemoryNameRecordsCacheStore, + "setRecordsByName" + ); + + const client = new IcNamingClient({ + net: "MAINNET", + mode: "local", + nameRecordsCacheStore: simpleMemoryNameRecordsCacheStore, + }); + + client["resolver"] = { get_record_value: () => {} } as any; + client["dispatchNameRecordsCache"] = jest + .fn() + .mockImplementation(async (fn) => { + await fn(simpleMemoryNameRecordsCacheStore); + }); + client["getRegistryOfName"] = jest.fn().mockResolvedValue({ + ttl: BigInt(600), + resolver: dummyPrincipal, + owner: dummyPrincipal, + name: "name", + }); + + jest.spyOn(client["resolver"], "get_record_value").mockResolvedValue({ + Ok: [ + ["key1", "value1"], + ["key2", "value2"], + ["key3", "value3"], + ], + }); + + await expect(client.getRecordsOfName("name")).resolves.toMatchObject([ + ["key1", "value1"], + ["key2", "value2"], + ["key3", "value3"], + ]); + expect(client["dispatchNameRecordsCache"]).toHaveBeenCalledTimes(2); + expect(client["resolver"].get_record_value).toBeCalledTimes(1); + expect(client["getRegistryOfName"]).toBeCalledTimes(1); + expect(spyStoreGet).toBeCalledTimes(2); + expect(spyStoreSet).toBeCalledTimes(2); + + await expect(client.getRecordsOfName("name")).resolves.toMatchObject([ + ["key1", "value1"], + ["key2", "value2"], + ["key3", "value3"], + ]); + expect(client["dispatchNameRecordsCache"]).toHaveBeenCalledTimes(2 + 1); + expect(client["resolver"].get_record_value).toBeCalledTimes(1 + 0); + expect(client["getRegistryOfName"]).toBeCalledTimes(1 + 0); + expect(spyStoreGet).toBeCalledTimes(2 + 1); + expect(spyStoreSet).toBeCalledTimes(2 + 0); + + simpleMemoryNameRecordsCacheStore.map["name"].expired_at = Date.now(); + + await expect(client.getRecordsOfName("name")).resolves.toMatchObject([ + ["key1", "value1"], + ["key2", "value2"], + ["key3", "value3"], + ]); + expect(client["dispatchNameRecordsCache"]).toHaveBeenCalledTimes(2 + 1 + 2); + expect(client["resolver"].get_record_value).toBeCalledTimes(1 + 0 + 1); + expect(client["getRegistryOfName"]).toBeCalledTimes(1 + 0 + 1); + expect(spyStoreGet).toBeCalledTimes(2 + 1 + 2); + expect(spyStoreSet).toBeCalledTimes(2 + 0 + 2); + }); + it("should return registry of name", async () => { const client = new IcNamingClient({ net: "MAINNET",