From 45602db5cc34c28aa26e2355175835570c6eaaa3 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 13 Mar 2025 11:05:22 -0400 Subject: [PATCH 1/7] feat: Add TTL caching support --- .../sdk/cloudflare/__tests__/index.test.ts | 198 ++++++++++++------ packages/sdk/cloudflare/src/index.ts | 23 +- .../src/api/EdgeFeatureStore.ts | 65 ++++-- .../shared/sdk-server-edge/src/api/cache.ts | 5 + .../shared/sdk-server-edge/src/api/index.ts | 3 +- packages/shared/sdk-server-edge/src/index.ts | 4 +- 6 files changed, 205 insertions(+), 93 deletions(-) create mode 100644 packages/shared/sdk-server-edge/src/api/cache.ts diff --git a/packages/sdk/cloudflare/__tests__/index.test.ts b/packages/sdk/cloudflare/__tests__/index.test.ts index 941438c0a5..04bc912a6b 100644 --- a/packages/sdk/cloudflare/__tests__/index.test.ts +++ b/packages/sdk/cloudflare/__tests__/index.test.ts @@ -21,87 +21,155 @@ const namespace = 'LD_KV'; const rootEnvKey = `LD-Env-${clientSideID}`; describe('init', () => { - let kv: KVNamespace; - let ldClient: LDClient; - - beforeAll(async () => { - kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; - await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); - ldClient = init(clientSideID, kv); - await ldClient.waitForInitialization(); - }); - - afterAll(() => { - ldClient.close(); - }); + describe('without caching', () => { + let kv: KVNamespace; + let ldClient: LDClient; + + beforeAll(async () => { + kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; + await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); + ldClient = init(clientSideID, kv); + await ldClient.waitForInitialization(); + }); - describe('flags', () => { - test('variation default', async () => { - const value = await ldClient.variation(flagKey1, context, false); - expect(value).toBeTruthy(); + afterAll(() => { + ldClient.close(); }); - test('variation default rollout', async () => { - const contextWithEmail = { ...context, email: 'test@yahoo.com' }; - const value = await ldClient.variation(flagKey2, contextWithEmail, false); - const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false); + describe('flags', () => { + test('variation default', async () => { + const value = await ldClient.variation(flagKey1, context, false); + expect(value).toBeTruthy(); + }); - expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }); - expect(value).toBeTruthy(); - }); + test('variation default rollout', async () => { + const contextWithEmail = { ...context, email: 'test@yahoo.com' }; + const value = await ldClient.variation(flagKey2, contextWithEmail, false); + const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false); - test('rule match', async () => { - const contextWithEmail = { ...context, email: 'test@falsemail.com' }; - const value = await ldClient.variation(flagKey1, contextWithEmail, false); - const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); + expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }); + expect(value).toBeTruthy(); + }); - expect(detail).toEqual({ - reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, - value: false, - variationIndex: 1, + test('rule match', async () => { + const contextWithEmail = { ...context, email: 'test@falsemail.com' }; + const value = await ldClient.variation(flagKey1, contextWithEmail, false); + const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); + + expect(detail).toEqual({ + reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, + value: false, + variationIndex: 1, + }); + expect(value).toBeFalsy(); }); - expect(value).toBeFalsy(); - }); - test('fallthrough', async () => { - const contextWithEmail = { ...context, email: 'test@yahoo.com' }; - const value = await ldClient.variation(flagKey1, contextWithEmail, false); - const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); + test('fallthrough', async () => { + const contextWithEmail = { ...context, email: 'test@yahoo.com' }; + const value = await ldClient.variation(flagKey1, contextWithEmail, false); + const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); - expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }); - expect(value).toBeTruthy(); + expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 }); + expect(value).toBeTruthy(); + }); + + test('allFlags fallthrough', async () => { + const allFlags = await ldClient.allFlagsState(context); + + expect(allFlags).toBeDefined(); + expect(allFlags.toJSON()).toEqual({ + $flagsState: { + testFlag1: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, + testFlag2: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, + testFlag3: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, + }, + $valid: true, + testFlag1: true, + testFlag2: true, + testFlag3: true, + }); + }); }); - test('allFlags fallthrough', async () => { - const allFlags = await ldClient.allFlagsState(context); - - expect(allFlags).toBeDefined(); - expect(allFlags.toJSON()).toEqual({ - $flagsState: { - testFlag1: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, - testFlag2: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, - testFlag3: { debugEventsUntilDate: 2000, variation: 0, version: 2 }, - }, - $valid: true, - testFlag1: true, - testFlag2: true, - testFlag3: true, + describe('segments', () => { + test('segment by country', async () => { + const contextWithCountry = { ...context, country: 'australia' }; + const value = await ldClient.variation(flagKey3, contextWithCountry, false); + const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false); + + expect(detail).toEqual({ + reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, + value: false, + variationIndex: 1, + }); + expect(value).toBeFalsy(); }); }); }); - describe('segments', () => { - test('segment by country', async () => { - const contextWithCountry = { ...context, country: 'australia' }; - const value = await ldClient.variation(flagKey3, contextWithCountry, false); - const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false); + describe('with caching', () => { + test('will cache across multiple variation calls', async () => { + const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; + await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); + const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); - expect(detail).toEqual({ - reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 }, - value: false, - variationIndex: 1, - }); - expect(value).toBeFalsy(); + await ldClient.waitForInitialization(); + const spy = jest.spyOn(kv, 'get'); + await ldClient.variation(flagKey1, context, false); + await ldClient.variation(flagKey2, context, false); + ldClient.close(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('will cache across multiple allFlags calls', async () => { + const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; + await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); + const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); + + await ldClient.waitForInitialization(); + const spy = jest.spyOn(kv, 'get'); + await ldClient.allFlagsState(context); + await ldClient.allFlagsState(context); + ldClient.close(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('will cache between allFlags and variation', async () => { + const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; + await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); + const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); + + await ldClient.waitForInitialization(); + const spy = jest.spyOn(kv, 'get'); + await ldClient.variation(flagKey1, context, false); + await ldClient.allFlagsState(context); + ldClient.close(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('will eventually expire', async () => { + jest.spyOn(Date, 'now').mockImplementation(() => 0); + + const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; + await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); + const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); + + await ldClient.waitForInitialization(); + const spy = jest.spyOn(kv, 'get'); + await ldClient.variation(flagKey1, context, false); + await ldClient.variation(flagKey2, context, false); + + expect(spy).toHaveBeenCalledTimes(1); + + jest.spyOn(Date, 'now').mockImplementation(() => 60 * 1000 + 1); + + await ldClient.variation(flagKey2, context, false); + expect(spy).toHaveBeenCalledTimes(2); + + ldClient.close(); }); }); }); diff --git a/packages/sdk/cloudflare/src/index.ts b/packages/sdk/cloudflare/src/index.ts index 064e6e969b..2fa6abd74b 100644 --- a/packages/sdk/cloudflare/src/index.ts +++ b/packages/sdk/cloudflare/src/index.ts @@ -14,14 +14,26 @@ import { BasicLogger, EdgeFeatureStore, init as initEdge, + internalServer, type LDClient, - type LDOptions, + type LDOptions as LDOptionsCommon, } from '@launchdarkly/js-server-sdk-common-edge'; import createPlatformInfo from './createPlatformInfo'; export * from '@launchdarkly/js-server-sdk-common-edge'; +/** + * The Launchdarkly Edge SDKs configuration options. + */ +type LDOptions = { + /** + * Optional TTL cache configuration which allows for caching feature flags in + * memory. + */ + cache?: internalServer.TtlCacheOptions; +} & LDOptionsCommon; + export type { LDClient }; /** @@ -41,7 +53,7 @@ export type { LDClient }; * @param kvNamespace * The Cloudflare KV configured for LaunchDarkly. * @param options - * Optional configuration settings. The only supported option is logger. + * Optional configuration settings. * @return * The new {@link LDClient} instance. */ @@ -51,9 +63,12 @@ export const init = ( options: LDOptions = {}, ): LDClient => { const logger = options.logger ?? BasicLogger.get(); + + const { cache: _cacheOptions, ...rest } = options; + const cache = options.cache ? new internalServer.TtlCache(options.cache) : undefined; return initEdge(clientSideID, createPlatformInfo(), { - featureStore: new EdgeFeatureStore(kvNamespace, clientSideID, 'Cloudflare', logger), + featureStore: new EdgeFeatureStore(kvNamespace, clientSideID, 'Cloudflare', logger, cache), logger, - ...options, + ...rest, }); }; diff --git a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts index 039ee4a300..b20c833013 100644 --- a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts +++ b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts @@ -8,6 +8,8 @@ import type { } from '@launchdarkly/js-server-sdk-common'; import { deserializePoll, noop } from '@launchdarkly/js-server-sdk-common'; +import Cache from './cache'; + export interface EdgeProvider { get: (rootKey: string) => Promise; } @@ -20,6 +22,7 @@ export class EdgeFeatureStore implements LDFeatureStore { sdkKey: string, private readonly _description: string, private _logger: LDLogger, + private _cache?: Cache, ) { this._rootKey = `LD-Env-${sdkKey}`; } @@ -34,23 +37,17 @@ export class EdgeFeatureStore implements LDFeatureStore { this._logger.debug(`Requesting ${dataKey} from ${this._rootKey}.${kindKey}`); try { - const i = await this._edgeProvider.get(this._rootKey); - - if (!i) { - throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`); - } - - const item = deserializePoll(i); - if (!item) { - throw new Error(`Error deserializing ${kindKey}`); + const storePayload = await this._getStorePayload(); + if (!storePayload) { + throw new Error(`Error deserializing ${this._rootKey}`); } switch (namespace) { case 'features': - callback(item.flags[dataKey]); + callback(storePayload.flags[dataKey]); break; case 'segments': - callback(item.segments[dataKey]); + callback(storePayload.segments[dataKey]); break; default: callback(null); @@ -66,22 +63,17 @@ export class EdgeFeatureStore implements LDFeatureStore { const kindKey = namespace === 'features' ? 'flags' : namespace; this._logger.debug(`Requesting all from ${this._rootKey}.${kindKey}`); try { - const i = await this._edgeProvider.get(this._rootKey); - if (!i) { + const storePayload = await this._getStorePayload(); + if (!storePayload) { throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`); } - const item = deserializePoll(i); - if (!item) { - throw new Error(`Error deserializing ${kindKey}`); - } - switch (namespace) { case 'features': - callback(item.flags); + callback(storePayload.flags); break; case 'segments': - callback(item.segments); + callback(storePayload.segments); break; default: callback({}); @@ -92,6 +84,34 @@ export class EdgeFeatureStore implements LDFeatureStore { } } + /** + * This method is used to retrieve the environment payload from the edge + * provider. If a cache is provided, it will serve from that. + * + * @returns + */ + private async _getStorePayload(): Promise> { + let payload = this._cache?.get(this._rootKey); + if (payload !== undefined) { + return payload; + } + + const providerData = await this._edgeProvider.get(this._rootKey); + + if (!providerData) { + throw new Error(`${this._rootKey} is not found in KV.`); + } + + payload = deserializePoll(providerData); + if (!payload) { + throw new Error(`Error deserializing ${this._rootKey}`); + } + + this._cache?.set(this._rootKey, payload); + + return payload; + } + async initialized(callback: (isInitialized: boolean) => void = noop): Promise { const config = await this._edgeProvider.get(this._rootKey); const result = config !== null; @@ -107,8 +127,11 @@ export class EdgeFeatureStore implements LDFeatureStore { return this._description; } + close(): void { + return this._cache?.close(); + } + // unused - close = noop; delete = noop; diff --git a/packages/shared/sdk-server-edge/src/api/cache.ts b/packages/shared/sdk-server-edge/src/api/cache.ts new file mode 100644 index 0000000000..4f3972e070 --- /dev/null +++ b/packages/shared/sdk-server-edge/src/api/cache.ts @@ -0,0 +1,5 @@ +export default interface Cache { + get(key: string): any; + set(key: string, value: any): void; + close(): void; +} diff --git a/packages/shared/sdk-server-edge/src/api/index.ts b/packages/shared/sdk-server-edge/src/api/index.ts index c4ae612f9d..569e65c0c7 100644 --- a/packages/shared/sdk-server-edge/src/api/index.ts +++ b/packages/shared/sdk-server-edge/src/api/index.ts @@ -1,4 +1,5 @@ +import Cache from './cache'; import LDClient from './LDClient'; export * from './EdgeFeatureStore'; -export { LDClient }; +export { LDClient, Cache }; diff --git a/packages/shared/sdk-server-edge/src/index.ts b/packages/shared/sdk-server-edge/src/index.ts index 24a6c49b77..b539aaa499 100644 --- a/packages/shared/sdk-server-edge/src/index.ts +++ b/packages/shared/sdk-server-edge/src/index.ts @@ -7,12 +7,12 @@ */ import type { Info } from '@launchdarkly/js-server-sdk-common'; -import { EdgeFeatureStore, EdgeProvider, LDClient } from './api'; +import { Cache, EdgeFeatureStore, EdgeProvider, LDClient } from './api'; import validateOptions, { LDOptions, LDOptionsInternal } from './utils/validateOptions'; export * from '@launchdarkly/js-server-sdk-common'; export { EdgeFeatureStore }; -export type { LDClient, LDOptions, EdgeProvider }; +export type { LDClient, LDOptions, EdgeProvider, Cache }; /** * Do not use this function directly. From 2578e672b26f1460ad093ec80ee3a44decdc339d Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 14:52:44 -0400 Subject: [PATCH 2/7] Export cache options --- packages/sdk/cloudflare/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/cloudflare/src/index.ts b/packages/sdk/cloudflare/src/index.ts index 2fa6abd74b..d4986ad1c5 100644 --- a/packages/sdk/cloudflare/src/index.ts +++ b/packages/sdk/cloudflare/src/index.ts @@ -23,6 +23,8 @@ import createPlatformInfo from './createPlatformInfo'; export * from '@launchdarkly/js-server-sdk-common-edge'; +export type TtlCacheOptions = internalServer.TtlCacheOptions; + /** * The Launchdarkly Edge SDKs configuration options. */ @@ -31,7 +33,7 @@ type LDOptions = { * Optional TTL cache configuration which allows for caching feature flags in * memory. */ - cache?: internalServer.TtlCacheOptions; + cache?: TtlCacheOptions; } & LDOptionsCommon; export type { LDClient }; From 7707986dffd6ac24f7763273f66e1347f04733c3 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 15:50:43 -0400 Subject: [PATCH 3/7] test -> it --- .../sdk/cloudflare/__tests__/index.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/sdk/cloudflare/__tests__/index.test.ts b/packages/sdk/cloudflare/__tests__/index.test.ts index 04bc912a6b..5b1f760fed 100644 --- a/packages/sdk/cloudflare/__tests__/index.test.ts +++ b/packages/sdk/cloudflare/__tests__/index.test.ts @@ -37,12 +37,12 @@ describe('init', () => { }); describe('flags', () => { - test('variation default', async () => { + it('variation default', async () => { const value = await ldClient.variation(flagKey1, context, false); expect(value).toBeTruthy(); }); - test('variation default rollout', async () => { + it('variation default rollout', async () => { const contextWithEmail = { ...context, email: 'test@yahoo.com' }; const value = await ldClient.variation(flagKey2, contextWithEmail, false); const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false); @@ -51,7 +51,7 @@ describe('init', () => { expect(value).toBeTruthy(); }); - test('rule match', async () => { + it('rule match', async () => { const contextWithEmail = { ...context, email: 'test@falsemail.com' }; const value = await ldClient.variation(flagKey1, contextWithEmail, false); const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); @@ -64,7 +64,7 @@ describe('init', () => { expect(value).toBeFalsy(); }); - test('fallthrough', async () => { + it('fallthrough', async () => { const contextWithEmail = { ...context, email: 'test@yahoo.com' }; const value = await ldClient.variation(flagKey1, contextWithEmail, false); const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false); @@ -73,7 +73,7 @@ describe('init', () => { expect(value).toBeTruthy(); }); - test('allFlags fallthrough', async () => { + it('allFlags fallthrough', async () => { const allFlags = await ldClient.allFlagsState(context); expect(allFlags).toBeDefined(); @@ -92,7 +92,7 @@ describe('init', () => { }); describe('segments', () => { - test('segment by country', async () => { + it('segment by country', async () => { const contextWithCountry = { ...context, country: 'australia' }; const value = await ldClient.variation(flagKey3, contextWithCountry, false); const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false); @@ -108,7 +108,7 @@ describe('init', () => { }); describe('with caching', () => { - test('will cache across multiple variation calls', async () => { + it('will cache across multiple variation calls', async () => { const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); @@ -122,7 +122,7 @@ describe('init', () => { expect(spy).toHaveBeenCalledTimes(1); }); - test('will cache across multiple allFlags calls', async () => { + it('will cache across multiple allFlags calls', async () => { const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); @@ -136,7 +136,7 @@ describe('init', () => { expect(spy).toHaveBeenCalledTimes(1); }); - test('will cache between allFlags and variation', async () => { + it('will cache between allFlags and variation', async () => { const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; await kv.put(rootEnvKey, JSON.stringify(allFlagsSegments)); const ldClient = init(clientSideID, kv, { cache: { ttl: 60, checkInterval: 600 } }); @@ -150,7 +150,7 @@ describe('init', () => { expect(spy).toHaveBeenCalledTimes(1); }); - test('will eventually expire', async () => { + it('will eventually expire', async () => { jest.spyOn(Date, 'now').mockImplementation(() => 0); const kv = (await mf.getKVNamespace(namespace)) as unknown as KVNamespace; From 18110223878dbf972bdc682164301d0907880bcb Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 15:54:53 -0400 Subject: [PATCH 4/7] Add doc --- .../shared/sdk-server-edge/src/api/cache.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/shared/sdk-server-edge/src/api/cache.ts b/packages/shared/sdk-server-edge/src/api/cache.ts index 4f3972e070..d32039cd10 100644 --- a/packages/shared/sdk-server-edge/src/api/cache.ts +++ b/packages/shared/sdk-server-edge/src/api/cache.ts @@ -1,5 +1,24 @@ +/** + * General-purpose cache interface. + * + * This is used by the SDK to cache feature flags and other data. The SDK does + * not assume any particular implementation of the cache, so you can provide + * your own. + */ export default interface Cache { + /** + * Get a value from the cache. Returning `undefined` means the key was not found. + */ get(key: string): any; + + /** + * Set a value in the cache. + */ set(key: string, value: any): void; + + /** + * The close method offers a way to clean up any resources used by the cache + * on shutdown. + */ close(): void; } From 6d010c00a1c103b38247286d6a6d177cae6d6228 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 15:55:23 -0400 Subject: [PATCH 5/7] Remove redundant return --- .../sdk-server-edge/src/api/EdgeFeatureStore.ts | 2 -- packages/shared/sdk-server-edge/src/api/cache.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts index b20c833013..a019162042 100644 --- a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts +++ b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts @@ -87,8 +87,6 @@ export class EdgeFeatureStore implements LDFeatureStore { /** * This method is used to retrieve the environment payload from the edge * provider. If a cache is provided, it will serve from that. - * - * @returns */ private async _getStorePayload(): Promise> { let payload = this._cache?.get(this._rootKey); diff --git a/packages/shared/sdk-server-edge/src/api/cache.ts b/packages/shared/sdk-server-edge/src/api/cache.ts index d32039cd10..e33ad61b46 100644 --- a/packages/shared/sdk-server-edge/src/api/cache.ts +++ b/packages/shared/sdk-server-edge/src/api/cache.ts @@ -7,18 +7,18 @@ */ export default interface Cache { /** - * Get a value from the cache. Returning `undefined` means the key was not found. - */ + * Get a value from the cache. Returning `undefined` means the key was not found. + */ get(key: string): any; /** - * Set a value in the cache. - */ + * Set a value in the cache. + */ set(key: string, value: any): void; /** - * The close method offers a way to clean up any resources used by the cache - * on shutdown. - */ + * The close method offers a way to clean up any resources used by the cache + * on shutdown. + */ close(): void; } From 893ab930c76c6abacd60a32b4efa0cbca100f783 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 16:25:00 -0400 Subject: [PATCH 6/7] Tweak error message --- packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts index a019162042..b39638a128 100644 --- a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts +++ b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts @@ -39,7 +39,7 @@ export class EdgeFeatureStore implements LDFeatureStore { try { const storePayload = await this._getStorePayload(); if (!storePayload) { - throw new Error(`Error deserializing ${this._rootKey}`); + throw new Error(`Error retrieving valid store payload`); } switch (namespace) { From 255bb049cf0b04fbaa8a8e322eaeb406a9c9a0cc Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 17 Mar 2025 16:33:58 -0400 Subject: [PATCH 7/7] Don't allow undefined --- .../shared/sdk-server-edge/src/api/EdgeFeatureStore.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts index b39638a128..2d62150b7c 100644 --- a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts +++ b/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts @@ -38,9 +38,6 @@ export class EdgeFeatureStore implements LDFeatureStore { try { const storePayload = await this._getStorePayload(); - if (!storePayload) { - throw new Error(`Error retrieving valid store payload`); - } switch (namespace) { case 'features': @@ -64,9 +61,6 @@ export class EdgeFeatureStore implements LDFeatureStore { this._logger.debug(`Requesting all from ${this._rootKey}.${kindKey}`); try { const storePayload = await this._getStorePayload(); - if (!storePayload) { - throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`); - } switch (namespace) { case 'features': @@ -88,7 +82,9 @@ export class EdgeFeatureStore implements LDFeatureStore { * This method is used to retrieve the environment payload from the edge * provider. If a cache is provided, it will serve from that. */ - private async _getStorePayload(): Promise> { + private async _getStorePayload(): Promise< + Exclude, undefined> + > { let payload = this._cache?.get(this._rootKey); if (payload !== undefined) { return payload;