From 72f308b6a2447fceeb84abc2198247354b5c2d43 Mon Sep 17 00:00:00 2001 From: Tobias Deekens Date: Thu, 11 Apr 2024 11:17:15 +0200 Subject: [PATCH] feat: to add cache-mode to adapter (#1888) * feat: to add cache-mode to adapter * feat: add new cache module abstracting over specific caches * refactor: to make cache require adapter identifier * refactor: to write into cache module * refactor: shamefully cast the type * refactor: shamefully cast the type * refactor: one cache module to rule them all * feat: add cache mode to http and graphql adapter * test: adjust test case for cache mode * refactor: gimme some constants * refactor: to not flush flags when reconfiguring * refactor: to always flush on reconfigure * fix: to not flush initial remote fetching * refactor: to use sync import to get flags * test: add initial set of tests * test: add test case for cached flag restoring * Create chilled-phones-greet.md --- .changeset/chilled-phones-greet.md | 19 +++ packages/cache/CHANGELOG.md | 0 packages/cache/package.json | 36 +++++ packages/cache/src/cache/cache.spec.js | 116 +++++++++++++++++ packages/cache/src/cache/cache.ts | 123 ++++++++++++++++++ packages/cache/src/cache/index.ts | 4 + packages/cache/src/index.ts | 4 + packages/cache/tsconfig.json | 3 + packages/cache/tsconfig.lint.json | 3 + packages/graphql-adapter/package.json | 1 + .../src/adapter/adapter.spec.js | 42 +++++- .../graphql-adapter/src/adapter/adapter.ts | 89 +++++++------ packages/http-adapter/package.json | 1 + .../http-adapter/src/adapter/adapter.spec.js | 32 ++++- packages/http-adapter/src/adapter/adapter.ts | 72 +++++----- packages/launchdarkly-adapter/package.json | 1 + .../src/adapter/adapter.spec.js | 55 ++------ .../src/adapter/adapter.ts | 45 +++++-- .../launchdarkly-adapter/src/adapter/cache.ts | 87 ------------- .../launchdarkly-adapter/src/adapter/index.ts | 1 - packages/launchdarkly-adapter/src/index.ts | 2 +- packages/react/package.json | 1 + .../configure-adapter.spec.js | 71 ++++++++-- .../configure-adapter/configure-adapter.tsx | 6 +- packages/types/src/index.ts | 12 +- pnpm-lock.yaml | 24 ++++ readme.md | 4 +- 27 files changed, 609 insertions(+), 245 deletions(-) create mode 100644 .changeset/chilled-phones-greet.md create mode 100644 packages/cache/CHANGELOG.md create mode 100644 packages/cache/package.json create mode 100644 packages/cache/src/cache/cache.spec.js create mode 100644 packages/cache/src/cache/cache.ts create mode 100644 packages/cache/src/cache/index.ts create mode 100644 packages/cache/src/index.ts create mode 100644 packages/cache/tsconfig.json create mode 100644 packages/cache/tsconfig.lint.json delete mode 100644 packages/launchdarkly-adapter/src/adapter/cache.ts diff --git a/.changeset/chilled-phones-greet.md b/.changeset/chilled-phones-greet.md new file mode 100644 index 000000000..36832193f --- /dev/null +++ b/.changeset/chilled-phones-greet.md @@ -0,0 +1,19 @@ +--- +"@flopflip/cache": major +"@flopflip/graphql-adapter": minor +"@flopflip/http-adapter": minor +"@flopflip/launchdarkly-adapter": major +"@flopflip/react": minor +"@flopflip/types": minor +--- + +The release adds a new `cacheMode` property on the `adapterArgs` of an adapter. + +Using the `cacheMode` you can opt into an `eager` or `lazy`. The `cacheMode` allows you to define when remote flags should take affect in an application. Before `flopflip` behaved always `eager`. This remains the case when passing `eager` or `null` as the `cacheMode`. In `lazy` mode `flopflip` will not directly flush remote values and only silently put them in the cache. They would then take effect on the next render or `reconfigure`. + +In short, the `cacheMode` can be `eager` to indicate that remote values should have effect immediately. The value can also be `lazy` to indicate that values should be updated in the cache but only be applied once the adapter is configured again + +With the `cacheMode` we removed some likely unused functionality which explored similar ideas before: + +1. `unsubscribeFromCachedFlags`: This is now always the case. You can use the `lazy` `cacheMode` to indicate that you don't want flags to take immediate effect +2. `subscribeToFlagChanges`: This is now always true. You can't opt-out of the flag subscription any longer diff --git a/packages/cache/CHANGELOG.md b/packages/cache/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 000000000..35289cea0 --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,36 @@ +{ + "name": "@flopflip/cache", + "version": "13.5.2", + "description": "Caching for flipflop adapters", + "main": "dist/flopflip-cache.cjs.js", + "module": "dist/flopflip-cache.esm.js", + "files": [ + "readme.md", + "dist/**" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/tdeekens/flopflip.git", + "directory": "packages/cache" + }, + "author": "Tobias Deekens ", + "license": "MIT", + "bugs": { + "url": "https://github.com/tdeekens/flopflip/issues" + }, + "homepage": "https://github.com/tdeekens/flopflip#readme", + "keywords": [ + "feature-flags", + "feature-toggles", + "cache", + "client" + ], + "dependencies": { + "@flopflip/localstorage-cache": "13.6.0", + "@flopflip/sessionstorage-cache": "13.6.0", + "@flopflip/types": "13.6.0" + } +} diff --git a/packages/cache/src/cache/cache.spec.js b/packages/cache/src/cache/cache.spec.js new file mode 100644 index 000000000..6387fa568 --- /dev/null +++ b/packages/cache/src/cache/cache.spec.js @@ -0,0 +1,116 @@ +import { adapterIdentifiers, cacheIdentifiers } from '@flopflip/types'; + +import { + getAllCachedFlags, + getCache, + getCachedFlags, + getCachePrefix, +} from './cache'; + +const cacheKey = 'test'; + +describe('general caching', () => { + it('should allow writing values to the cache', async () => { + const flags = { + flag1: true, + }; + const cache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); + + cache.set(flags); + + expect(cache.get()).toStrictEqual(flags); + }); + + it('should allow unsetting values from the cache', async () => { + const flags = { + flag1: true, + }; + const cache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); + + cache.set(flags); + cache.unset(); + + expect(cache.get()).toBeNull(); + }); + + it('should update a referencing cache to the values cache', async () => { + const flags = { + flag1: true, + }; + const cache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); + + cache.set(flags); + + expect(sessionStorage.getItem).toHaveBeenLastCalledWith( + `${getCachePrefix(adapterIdentifiers.memory)}/${cacheKey}/flags` + ); + }); +}); + +describe('flag caching', () => { + describe('with a single adapter', () => { + it('should allow writing and getting cached flags', async () => { + const flags = { + flag1: true, + }; + const cache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); + + cache.set(flags); + + expect( + getCachedFlags(cacheIdentifiers.session, adapterIdentifiers.memory) + ).toStrictEqual(flags); + + expect(sessionStorage.getItem).toHaveBeenLastCalledWith( + `${getCachePrefix(adapterIdentifiers.memory)}/${cacheKey}/flags` + ); + }); + }); + describe('with a multiple adapters', () => { + it('should allow writing and getting cached flags for all adapters', async () => { + const memoryAdapterFlags = { + flag1: true, + }; + const localstorageAdapterFlags = { + flag2: true, + }; + + const memoryAdapterCache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); + const localstorageAdapterCache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.localstorage, + cacheKey + ); + + localstorageAdapterCache.set(localstorageAdapterFlags); + + const fakeAdapter = { + effectIds: [adapterIdentifiers.memory, adapterIdentifiers.localstorage], + }; + + expect( + getAllCachedFlags(fakeAdapter, cacheIdentifiers.session) + ).toStrictEqual({ ...memoryAdapterFlags, ...localstorageAdapterFlags }); + }); + }); +}); diff --git a/packages/cache/src/cache/cache.ts b/packages/cache/src/cache/cache.ts new file mode 100644 index 000000000..fafdf8441 --- /dev/null +++ b/packages/cache/src/cache/cache.ts @@ -0,0 +1,123 @@ +import { + cacheIdentifiers, + type TAdapter, + type TAdapterIdentifiers, + type TCacheIdentifiers, + type TFlags, +} from '@flopflip/types'; + +const FLAGS_CACHE_KEY = 'flags'; +const FLAGS_REFERENCE_CACHE_KEY = 'flags-reference'; + +function getCachePrefix(adapterIdentifiers: TAdapterIdentifiers) { + return `@flopflip/${adapterIdentifiers}-adapter`; +} + +async function importCache(cacheIdentifier: TCacheIdentifiers) { + let cacheModule; + + switch (cacheIdentifier) { + case cacheIdentifiers.local: { + cacheModule = await import('@flopflip/localstorage-cache'); + break; + } + + case cacheIdentifiers.session: { + cacheModule = await import('@flopflip/sessionstorage-cache'); + break; + } + } + + return cacheModule; +} + +async function getCache( + cacheIdentifier: TCacheIdentifiers, + adapterIdentifiers: TAdapterIdentifiers, + cacheKey?: string +) { + const cacheModule = await importCache(cacheIdentifier); + + const CACHE_PREFIX = getCachePrefix(adapterIdentifiers); + const createCache = cacheModule.default; + const flagsCachePrefix = [CACHE_PREFIX, cacheKey].filter(Boolean).join('/'); + + const flagsCache = createCache({ prefix: flagsCachePrefix }); + const referenceCache = createCache({ prefix: CACHE_PREFIX }); + + return { + set(flags: TFlags) { + const haveFlagsBeenWritten = flagsCache.set(FLAGS_CACHE_KEY, flags); + + if (haveFlagsBeenWritten) { + referenceCache.set( + FLAGS_REFERENCE_CACHE_KEY, + [flagsCachePrefix, FLAGS_CACHE_KEY].join('/') + ); + } + + return haveFlagsBeenWritten; + }, + get() { + return flagsCache.get(FLAGS_CACHE_KEY); + }, + unset() { + referenceCache.unset(FLAGS_REFERENCE_CACHE_KEY); + + return flagsCache.unset(FLAGS_CACHE_KEY); + }, + }; +} + +function getCachedFlags( + cacheIdentifier: TCacheIdentifiers, + adapterIdentifiers: TAdapterIdentifiers +): TFlags { + const CACHE_PREFIX = getCachePrefix(adapterIdentifiers); + + const cacheModule = + cacheIdentifier === cacheIdentifiers.local ? localStorage : sessionStorage; + const flagReferenceKey = [CACHE_PREFIX, FLAGS_REFERENCE_CACHE_KEY].join('/'); + + const referenceToCachedFlags = cacheModule.getItem(flagReferenceKey); + + if (referenceToCachedFlags) { + try { + const cacheKey: string = JSON.parse(referenceToCachedFlags); + const cachedFlags = cacheModule.getItem(cacheKey); + + if (cacheKey && cachedFlags) { + return JSON.parse(cachedFlags); + } + } catch (error) { + console.warn( + `@flopflip/cache: Failed to parse cached flags from ${cacheIdentifier}.` + ); + } + } + + return {}; +} + +function getAllCachedFlags( + adapter: TAdapter, + cacheIdentifier?: TCacheIdentifiers +) { + if (!cacheIdentifier) { + return {}; + } + + if (adapter.effectIds) { + return adapter.effectIds.reduce( + (defaultFlags, effectId) => ({ + ...defaultFlags, + ...getCachedFlags(cacheIdentifier, effectId), + }), + {} + ); + } + + return getCachedFlags(cacheIdentifier, adapter.id); +} + +export { getAllCachedFlags, getCache, getCachedFlags, getCachePrefix }; diff --git a/packages/cache/src/cache/index.ts b/packages/cache/src/cache/index.ts new file mode 100644 index 000000000..c5e8f0987 --- /dev/null +++ b/packages/cache/src/cache/index.ts @@ -0,0 +1,4 @@ +const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; + +export { version }; +export { getAllCachedFlags, getCache, getCachedFlags } from './cache'; diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 000000000..7c0cd1b67 --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1,4 @@ +const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; + +export { getAllCachedFlags, getCache, getCachedFlags } from './cache'; +export { version }; diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 000000000..6f83eb665 --- /dev/null +++ b/packages/cache/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json", +} diff --git a/packages/cache/tsconfig.lint.json b/packages/cache/tsconfig.lint.json new file mode 100644 index 000000000..4be720881 --- /dev/null +++ b/packages/cache/tsconfig.lint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.lint.json", +} diff --git a/packages/graphql-adapter/package.json b/packages/graphql-adapter/package.json index 5f99859e7..34cb33da0 100644 --- a/packages/graphql-adapter/package.json +++ b/packages/graphql-adapter/package.json @@ -31,6 +31,7 @@ "dependencies": { "@babel/runtime": "7.24.4", "@flopflip/adapter-utilities": "13.6.0", + "@flopflip/cache": "13.5.2", "@flopflip/localstorage-cache": "13.6.0", "@flopflip/sessionstorage-cache": "13.6.0", "@flopflip/types": "13.6.0", diff --git a/packages/graphql-adapter/src/adapter/adapter.spec.js b/packages/graphql-adapter/src/adapter/adapter.spec.js index da88908ed..89b490e21 100644 --- a/packages/graphql-adapter/src/adapter/adapter.spec.js +++ b/packages/graphql-adapter/src/adapter/adapter.spec.js @@ -206,6 +206,36 @@ describe('when configured', () => { }, }); }); + + describe('with lazy cache mode', () => { + beforeEach(async () => { + sessionStorage.getItem.mockReturnValueOnce( + JSON.stringify({ cached: true }) + ); + adapterEventHandlers = createAdapterEventHandlers(); + jest.useFakeTimers(); + configurationResult = await adapter.configure( + adapterArgs, + adapterEventHandlers + ); + }); + + it('should only flush cached but not updated flags', () => { + expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ + id: adapter.id, + flags: expect.objectContaining({ + cached: true, + }), + }); + + expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ + id: adapter.id, + flags: expect.not.objectContaining({ + updated: true, + }), + }); + }); + }); }); describe('when updating flags', () => { @@ -275,6 +305,7 @@ describe('when configured', () => { beforeEach(async () => { configurationResult = await adapter.reconfigure({ + ...adapterArgs, user, cacheIdentifier: 'session', }); @@ -296,10 +327,17 @@ describe('when configured', () => { expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalled(); }); - it('should invoke `onFlagsStateChange` with empty flags', () => { + it('should invoke `onFlagsStateChange` with all flags', () => { expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ id: adapter.id, - flags: {}, + flags: { + barFlag: false, + disabled: false, + enabled: true, + flagA1: false, + flagB: false, + fooFlag: true, + }, }); }); diff --git a/packages/graphql-adapter/src/adapter/adapter.ts b/packages/graphql-adapter/src/adapter/adapter.ts index 140132c6b..46cc6c4b9 100644 --- a/packages/graphql-adapter/src/adapter/adapter.ts +++ b/packages/graphql-adapter/src/adapter/adapter.ts @@ -3,12 +3,13 @@ import { normalizeFlag, normalizeFlags, } from '@flopflip/adapter-utilities'; +import { getCache } from '@flopflip/cache'; import { AdapterConfigurationStatus, adapterIdentifiers, AdapterInitializationStatus, AdapterSubscriptionStatus, - cacheIdentifiers, + cacheModes, type TAdapterEventHandlers, type TAdapterStatus, type TAdapterStatusChange, @@ -40,8 +41,6 @@ type TGraphQlAdapterState = { cacheIdentifier?: TCacheIdentifiers; }; -const STORAGE_SLICE = '@flopflip/graphql-adapter'; - const intialAdapterState: TAdapterStatus & TGraphQlAdapterState = { subscriptionStatus: AdapterSubscriptionStatus.Subscribed, configurationStatus: AdapterConfigurationStatus.Unconfigured, @@ -59,6 +58,7 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { '__internalConfiguredStatusChange__'; #adapterState: TAdapterStatus & TGraphQlAdapterState; + #flagPollingInternal?: ReturnType; readonly #defaultpollingIntervalMs = 1000 * 60; constructor() { @@ -75,38 +75,6 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { readonly #getIsFlagLocked = (flagName: TFlagName) => this.#adapterState.lockedFlags.has(flagName); - readonly #getCache = async (cacheIdentifier: TCacheIdentifiers) => { - let cacheModule; - - switch (cacheIdentifier) { - case cacheIdentifiers.local: { - cacheModule = await import('@flopflip/localstorage-cache'); - break; - } - - case cacheIdentifiers.session: { - cacheModule = await import('@flopflip/sessionstorage-cache'); - break; - } - } - - const createCache = cacheModule.default; - - const cache = createCache({ prefix: STORAGE_SLICE }); - - return { - set(flags: TFlags) { - return cache.set('flags', flags); - }, - get() { - return cache.get('flags'); - }, - unset() { - return cache.unset('flags'); - }, - }; - }; - readonly #didFlagsChange = (nextFlags: TFlags) => { const previousFlags = this.#adapterState.flags; @@ -143,18 +111,31 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { const pollingIntervalMs = adapterArgs.pollingIntervalMs ?? this.#defaultpollingIntervalMs; - setInterval(async () => { + if (this.#flagPollingInternal) { + clearInterval(this.#flagPollingInternal); + } + + this.#flagPollingInternal = setInterval(async () => { if (!this.#getIsAdapterUnsubscribed()) { const nextFlags = normalizeFlags(await this.#fetchFlags(adapterArgs)); if (this.#didFlagsChange(nextFlags)) { if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.graphql, + this.#adapterState.user?.key + ); cache.set(nextFlags); } this.#adapterState.flags = nextFlags; + + if (adapterArgs.cacheMode === cacheModes.lazy) { + return; + } + this.#adapterState.emitter.emit('flagsStateChange', nextFlags); } } @@ -242,7 +223,11 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { let cachedFlags; if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.graphql, + this.#adapterState.user?.key + ); cachedFlags = cache.get(); @@ -262,12 +247,19 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { this.#adapterState.flags = flags; if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.graphql, + this.#adapterState.user?.key + ); cache.set(flags); } - this.#adapterState.emitter.emit('flagsStateChange', flags); + if (adapterArgs.cacheMode !== cacheModes.lazy) { + this.#adapterState.emitter.emit('flagsStateChange', flags); + } + this.#adapterState.emitter.emit(this.#__internalConfiguredStatusChange__); this.#subscribeToFlagsChanges(adapterArgs); @@ -292,7 +284,11 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { this.#adapterState.flags = {}; if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.graphql, + this.#adapterState.user?.key + ); cache.unset(); } @@ -300,7 +296,16 @@ class GraphQlAdapter implements TGraphQlAdapterInterface { const nextUser = adapterArgs.user; this.#adapterState.user = nextUser; - this.#adapterState.emitter.emit('flagsStateChange', {}); + + const flags = normalizeFlags(await this.#fetchFlags(adapterArgs)); + + this.#adapterState.flags = flags; + + this.#adapterState.emitter.emit('flagsStateChange', flags); + + this.#adapterState.emitter.emit(this.#__internalConfiguredStatusChange__); + + this.#subscribeToFlagsChanges(adapterArgs); return Promise.resolve({ initializationStatus: AdapterInitializationStatus.Succeeded, diff --git a/packages/http-adapter/package.json b/packages/http-adapter/package.json index 3ef85a40d..edeac7363 100644 --- a/packages/http-adapter/package.json +++ b/packages/http-adapter/package.json @@ -31,6 +31,7 @@ "dependencies": { "@babel/runtime": "7.24.4", "@flopflip/adapter-utilities": "13.6.0", + "@flopflip/cache": "13.5.2", "@flopflip/localstorage-cache": "13.6.0", "@flopflip/sessionstorage-cache": "13.6.0", "@flopflip/types": "13.6.0", diff --git a/packages/http-adapter/src/adapter/adapter.spec.js b/packages/http-adapter/src/adapter/adapter.spec.js index 80e8fc569..3355fac7c 100644 --- a/packages/http-adapter/src/adapter/adapter.spec.js +++ b/packages/http-adapter/src/adapter/adapter.spec.js @@ -164,6 +164,36 @@ describe('when configured', () => { }, }); }); + + describe('with lazy cache mode', () => { + beforeEach(async () => { + sessionStorage.getItem.mockReturnValueOnce( + JSON.stringify({ cached: true }) + ); + adapterEventHandlers = createAdapterEventHandlers(); + jest.useFakeTimers(); + configurationResult = await adapter.configure( + adapterArgs, + adapterEventHandlers + ); + }); + + it('should only flush cached but not updated flags', () => { + expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ + id: adapter.id, + flags: expect.objectContaining({ + cached: true, + }), + }); + + expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ + id: adapter.id, + flags: expect.not.objectContaining({ + updated: true, + }), + }); + }); + }); }); describe('when updating flags', () => { @@ -255,7 +285,7 @@ describe('when configured', () => { expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalled(); }); - it('should invoke `onFlagsStateChange` with empty flags', () => { + it('should invoke `onFlagsStateChange` with all flags', () => { expect(adapterEventHandlers.onFlagsStateChange).toHaveBeenCalledWith({ id: adapter.id, flags: { diff --git a/packages/http-adapter/src/adapter/adapter.ts b/packages/http-adapter/src/adapter/adapter.ts index cfcc91792..47ead26bc 100644 --- a/packages/http-adapter/src/adapter/adapter.ts +++ b/packages/http-adapter/src/adapter/adapter.ts @@ -3,12 +3,13 @@ import { normalizeFlag, normalizeFlags, } from '@flopflip/adapter-utilities'; +import { getCache } from '@flopflip/cache'; import { AdapterConfigurationStatus, adapterIdentifiers, AdapterInitializationStatus, AdapterSubscriptionStatus, - cacheIdentifiers, + cacheModes, type TAdapterEventHandlers, type TAdapterStatus, type TAdapterStatusChange, @@ -40,8 +41,6 @@ type THttpAdapterState = { cacheIdentifier?: TCacheIdentifiers; }; -const STORAGE_SLICE = '@flopflip/http-adapter'; - const intialAdapterState: TAdapterStatus & THttpAdapterState = { subscriptionStatus: AdapterSubscriptionStatus.Subscribed, configurationStatus: AdapterConfigurationStatus.Unconfigured, @@ -75,38 +74,6 @@ class HttpAdapter implements THttpAdapterInterface { readonly #getIsFlagLocked = (flagName: TFlagName) => this.#adapterState.lockedFlags.has(flagName); - readonly #getCache = async (cacheIdentifier: TCacheIdentifiers) => { - let cacheModule; - - switch (cacheIdentifier) { - case cacheIdentifiers.local: { - cacheModule = await import('@flopflip/localstorage-cache'); - break; - } - - case cacheIdentifiers.session: { - cacheModule = await import('@flopflip/sessionstorage-cache'); - break; - } - } - - const createCache = cacheModule.default; - - const cache = createCache({ prefix: STORAGE_SLICE }); - - return { - set(flags: TFlags) { - return cache.set('flags', flags); - }, - get() { - return cache.get('flags'); - }, - unset() { - return cache.unset('flags'); - }, - }; - }; - readonly #didFlagsChange = (nextFlags: TFlags) => { const previousFlags = this.#adapterState.flags; @@ -137,12 +104,21 @@ class HttpAdapter implements THttpAdapterInterface { if (this.#didFlagsChange(nextFlags)) { if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.http, + this.#adapterState.user?.key + ); cache.set(nextFlags); } this.#adapterState.flags = nextFlags; + + if (adapterArgs.cacheMode === cacheModes.lazy) { + return; + } + this.#adapterState.emitter.emit('flagsStateChange', nextFlags); } } @@ -230,7 +206,11 @@ class HttpAdapter implements THttpAdapterInterface { let cachedFlags; if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.http, + this.#adapterState.user?.key + ); cachedFlags = cache.get(); @@ -250,12 +230,19 @@ class HttpAdapter implements THttpAdapterInterface { this.setConfigurationStatus(AdapterConfigurationStatus.Configured); if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.http, + this.#adapterState.user?.key + ); cache.set(flags); } - this.#adapterState.emitter.emit('flagsStateChange', flags); + if (adapterArgs.cacheMode !== cacheModes.lazy) { + this.#adapterState.emitter.emit('flagsStateChange', flags); + } + this.#adapterState.emitter.emit(this.#__internalConfiguredStatusChange__); this.#subscribeToFlagsChanges(adapterArgs); @@ -280,7 +267,11 @@ class HttpAdapter implements THttpAdapterInterface { this.#adapterState.flags = {}; if (adapterArgs.cacheIdentifier) { - const cache = await this.#getCache(adapterArgs.cacheIdentifier); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.http, + this.#adapterState.user?.key + ); cache.unset(); } @@ -294,6 +285,7 @@ class HttpAdapter implements THttpAdapterInterface { this.#adapterState.flags = flags; this.#adapterState.emitter.emit('flagsStateChange', flags); + this.#adapterState.emitter.emit(this.#__internalConfiguredStatusChange__); this.#subscribeToFlagsChanges(adapterArgs); diff --git a/packages/launchdarkly-adapter/package.json b/packages/launchdarkly-adapter/package.json index e15d368d3..13d4ec617 100644 --- a/packages/launchdarkly-adapter/package.json +++ b/packages/launchdarkly-adapter/package.json @@ -28,6 +28,7 @@ "dependencies": { "@babel/runtime": "7.24.4", "@flopflip/adapter-utilities": "13.6.0", + "@flopflip/cache": "13.5.2", "@flopflip/localstorage-cache": "13.6.0", "@flopflip/sessionstorage-cache": "13.6.0", "@flopflip/types": "13.6.0", diff --git a/packages/launchdarkly-adapter/src/adapter/adapter.spec.js b/packages/launchdarkly-adapter/src/adapter/adapter.spec.js index a92b9c763..9b0644a78 100644 --- a/packages/launchdarkly-adapter/src/adapter/adapter.spec.js +++ b/packages/launchdarkly-adapter/src/adapter/adapter.spec.js @@ -368,43 +368,6 @@ describe('when configuring', () => { }); describe('with flag updates', () => { - describe('when not `subscribeToFlagChanges`', () => { - beforeEach(async () => { - // Reset due to preivous dispatches - onFlagsStateChange.mockClear(); - client.on.mockClear(); - - onStatusStateChange = jest.fn(); - onFlagsStateChange = jest.fn(); - client = createClient({ - allFlags: jest.fn(() => flags), - variation: jest.fn(() => true), - }); - - ldClient.initialize.mockReturnValue(client); - - await adapter.configure( - { - sdk: { clientSideId }, - subscribeToFlagChanges: false, - context: userWithKey, - }, - { - onStatusStateChange, - onFlagsStateChange, - } - ); - - onFlagsStateChange.mockClear(); - - triggerFlagValueChange(client); - }); - - it('should not `dispatch` `onFlagsStateChange` action', () => { - expect(onFlagsStateChange).not.toHaveBeenCalled(); - }); - }); - describe('with `flagsUpdateDelayMs`', () => { const flagsUpdateDelayMs = 1000; @@ -549,7 +512,7 @@ describe('when configuring', () => { }); }); - describe('when unsubscribing from cached flags', () => { + describe('with lazy cache mode', () => { beforeEach(async () => { onStatusStateChange.mockClear(); onFlagsStateChange.mockClear(); @@ -571,7 +534,7 @@ describe('when configuring', () => { sdk: { clientSideId }, context: userWithKey, cacheIdentifier: 'session', - unsubscribeFromCachedFlags: true, + cacheMode: 'lazy', }, { onStatusStateChange, @@ -580,13 +543,19 @@ describe('when configuring', () => { ); }); - it('should prefer the cached version and flush flags', () => { + it('should only flush cached but not updated flags', () => { expect(onFlagsStateChange).toHaveBeenCalledWith({ id: adapter.id, - flags: { + flags: expect.objectContaining({ cached: true, - updated: false, - }, + }), + }); + + expect(onFlagsStateChange).toHaveBeenCalledWith({ + id: adapter.id, + flags: expect.not.objectContaining({ + updated: true, + }), }); }); diff --git a/packages/launchdarkly-adapter/src/adapter/adapter.ts b/packages/launchdarkly-adapter/src/adapter/adapter.ts index 3019cd1d2..448cbc5fa 100644 --- a/packages/launchdarkly-adapter/src/adapter/adapter.ts +++ b/packages/launchdarkly-adapter/src/adapter/adapter.ts @@ -4,15 +4,18 @@ import { normalizeFlag, normalizeFlags, } from '@flopflip/adapter-utilities'; +import { getCache } from '@flopflip/cache'; import { AdapterConfigurationStatus, adapterIdentifiers, AdapterInitializationStatus, AdapterSubscriptionStatus, + cacheModes, type TAdapterEventHandlers, type TAdapterStatus, type TAdapterStatusChange, type TCacheIdentifiers, + type TCacheModes, type TFlagName, type TFlags, type TFlagsChange, @@ -32,8 +35,6 @@ import mitt, { type Emitter } from 'mitt'; import warning from 'tiny-warning'; import { merge } from 'ts-deepmerge'; -import { getCache } from './cache'; - type TEmitterEvents = { flagsStateChange: TFlags; statusStateChange: Partial; @@ -152,7 +153,9 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { if (cacheIdentifier) { const cache = await getCache( cacheIdentifier, - this.#adapterState.context?.key + adapterIdentifiers.launchdarkly, + // NOTE: LDContextCommon is part of the type which we never use. + this.#adapterState.context?.key as string ); const cachedFlags: TFlags = cache.get(); @@ -165,9 +168,10 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { flags, throwOnInitializationFailure, cacheIdentifier, + cacheMode, }: Pick< TLaunchDarklyAdapterArgs, - 'flags' | 'throwOnInitializationFailure' | 'cacheIdentifier' + 'flags' | 'throwOnInitializationFailure' | 'cacheIdentifier' | 'cacheMode' >): Promise<{ flagsFromSdk?: TFlags; initializationStatus: AdapterInitializationStatus; @@ -207,7 +211,14 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { const flags = this.#withoutUnsubscribedOrLockedFlags(normalizedFlags); - this.updateFlags(flags); + this.#updateFlagsInAdapterState(flags); + + if (cacheMode !== cacheModes.lazy) { + this.#adapterState.emitter.emit( + 'flagsStateChange', + this.#adapterState.flags + ); + } } this.setConfigurationStatus(AdapterConfigurationStatus.Configured); @@ -258,10 +269,12 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { flagsFromSdk, flagsUpdateDelayMs, cacheIdentifier, + cacheMode, }: { flagsFromSdk: TFlags; flagsUpdateDelayMs?: number; cacheIdentifier?: TCacheIdentifiers; + cacheMode?: TCacheModes; }) => { for (const flagName in flagsFromSdk) { // Dispatch whenever a configured flag value changes @@ -295,6 +308,10 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { this.#updateFlagsInAdapterState(updatedFlags); const flushFlagsUpdate = () => { + if (cacheMode === cacheModes.lazy) { + return; + } + this.#adapterState.emitter.emit( 'flagsStateChange', this.#adapterState.flags @@ -363,7 +380,6 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { sdk, context, flags, - subscribeToFlagChanges = true, throwOnInitializationFailure = false, flagsUpdateDelayMs, } = adapterArgs; @@ -372,14 +388,16 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { this.#adapterState.context = this.#ensureContext(context); if (adapterArgs.cacheIdentifier) { - const cache = await getCache(adapterArgs.cacheIdentifier, context.key); + const cache = await getCache( + adapterArgs.cacheIdentifier, + adapterIdentifiers.launchdarkly, + context.key as string + ); cachedFlags = cache.get(); if (cachedFlags) { - this.#updateFlagsInAdapterState(cachedFlags, { - unsubscribeFlags: adapterArgs.unsubscribeFromCachedFlags, - }); + this.#updateFlagsInAdapterState(cachedFlags); this.#adapterState.flags = cachedFlags; this.#adapterState.emitter.emit('flagsStateChange', cachedFlags); } @@ -395,12 +413,14 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { flags, throwOnInitializationFailure, cacheIdentifier: adapterArgs.cacheIdentifier, + cacheMode: adapterArgs.cacheMode, }).then(({ flagsFromSdk, initializationStatus }) => { - if (subscribeToFlagChanges && flagsFromSdk) { + if (flagsFromSdk) { this.#setupFlagSubcription({ flagsFromSdk, flagsUpdateDelayMs, cacheIdentifier: adapterArgs.cacheIdentifier, + cacheMode: adapterArgs.cacheMode, }); } @@ -425,7 +445,8 @@ class LaunchDarklyAdapter implements TLaunchDarklyAdapterInterface { if (adapterArgs.cacheIdentifier) { const cache = await getCache( adapterArgs.cacheIdentifier, - this.#adapterState.context?.key + adapterIdentifiers.launchdarkly, + this.#adapterState.context?.key as string ); cache.unset(); diff --git a/packages/launchdarkly-adapter/src/adapter/cache.ts b/packages/launchdarkly-adapter/src/adapter/cache.ts deleted file mode 100644 index 7e8708b65..000000000 --- a/packages/launchdarkly-adapter/src/adapter/cache.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - cacheIdentifiers, - type TCacheIdentifiers, - type TFlags, -} from '@flopflip/types'; -import { type LDContext } from 'launchdarkly-js-client-sdk'; - -const CACHE_PREFIX = '@flopflip/launchdarkly-adapter'; -const FLAGS_KEY = 'flags'; -const FLAGS_REFERENCE_KEY = 'flags-reference'; - -async function importCache(cacheIdentifier: TCacheIdentifiers) { - let cacheModule; - - switch (cacheIdentifier) { - case cacheIdentifiers.local: { - cacheModule = await import('@flopflip/localstorage-cache'); - break; - } - - case cacheIdentifiers.session: { - cacheModule = await import('@flopflip/sessionstorage-cache'); - break; - } - } - - return cacheModule; -} - -async function getCache( - cacheIdentifier: TCacheIdentifiers, - cacheKey: LDContext['key'] -) { - const cacheModule = await importCache(cacheIdentifier); - - const createCache = cacheModule.default; - const flagsCachePrefix = [CACHE_PREFIX, cacheKey].filter(Boolean).join('/'); - - const flagsCache = createCache({ prefix: flagsCachePrefix }); - const referenceCache = createCache({ prefix: CACHE_PREFIX }); - - return { - set(flags: TFlags) { - const haveFlagsBeenWritten = flagsCache.set(FLAGS_KEY, flags); - - if (haveFlagsBeenWritten) { - referenceCache.set( - FLAGS_REFERENCE_KEY, - [flagsCachePrefix, FLAGS_KEY].join('/') - ); - } - - return haveFlagsBeenWritten; - }, - get() { - return flagsCache.get(FLAGS_KEY); - }, - unset() { - referenceCache.unset(FLAGS_REFERENCE_KEY); - - return flagsCache.unset(FLAGS_KEY); - }, - }; -} - -function getCachedFlags(cacheIdentifier: TCacheIdentifiers): TFlags { - const cacheModule = - cacheIdentifier === cacheIdentifiers.local ? localStorage : sessionStorage; - const flagReferenceKey = [CACHE_PREFIX, FLAGS_REFERENCE_KEY].join('/'); - - const referenceToCachedFlags = cacheModule.getItem(flagReferenceKey); - - if (referenceToCachedFlags) { - try { - const cacheKey: string = JSON.parse(referenceToCachedFlags); - const cachedFlags = cacheModule.getItem(cacheKey); - - if (cacheKey && cachedFlags) { - return JSON.parse(cachedFlags); - } - } catch (error) {} - } - - return {}; -} - -export { CACHE_PREFIX, getCache, getCachedFlags }; diff --git a/packages/launchdarkly-adapter/src/adapter/index.ts b/packages/launchdarkly-adapter/src/adapter/index.ts index 3bcb1f5f0..0756571aa 100644 --- a/packages/launchdarkly-adapter/src/adapter/index.ts +++ b/packages/launchdarkly-adapter/src/adapter/index.ts @@ -1,2 +1 @@ export { default } from './adapter'; -export { getCachedFlags } from './cache'; diff --git a/packages/launchdarkly-adapter/src/index.ts b/packages/launchdarkly-adapter/src/index.ts index a2e894920..234e89f89 100644 --- a/packages/launchdarkly-adapter/src/index.ts +++ b/packages/launchdarkly-adapter/src/index.ts @@ -1,4 +1,4 @@ const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; export { version }; -export { default, getCachedFlags } from './adapter'; +export { default } from './adapter'; diff --git a/packages/react/package.json b/packages/react/package.json index af577a928..39e65c159 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@babel/runtime": "7.24.4", + "@flopflip/cache": "13.5.2", "@flopflip/types": "13.6.0", "@types/react-is": "17.0.7", "lodash": "4.17.21", diff --git a/packages/react/src/components/configure-adapter/configure-adapter.spec.js b/packages/react/src/components/configure-adapter/configure-adapter.spec.js index ec857b334..3ed1e3f6c 100644 --- a/packages/react/src/components/configure-adapter/configure-adapter.spec.js +++ b/packages/react/src/components/configure-adapter/configure-adapter.spec.js @@ -1,9 +1,11 @@ +import { getCache } from '@flopflip/cache'; import { fireEvent, render as rtlRender, screen, waitFor, } from '@flopflip/test-utils'; +import { adapterIdentifiers, cacheIdentifiers } from '@flopflip/types'; import React, { useContext } from 'react'; import useAdapterReconfiguration from '../../hooks/use-adapter-reconfiguration'; @@ -11,6 +13,7 @@ import AdapterContext from '../adapter-context'; import ConfigureAdapter, { AdapterStates } from './configure-adapter'; const createAdapter = () => ({ + id: adapterIdentifiers.memory, getIsConfigurationStatus: jest.fn(() => false), configure: jest.fn(() => Promise.resolve()), reconfigure: jest.fn(() => Promise.resolve()), @@ -241,20 +244,68 @@ describe('when adapter configuration should not be deferred', () => { }); describe('when providing default flags', () => { - it('should notify parent about the default flag state', async () => { - const adapter = createAdapter(); - const defaultFlags = { - flagName: true, - }; - const props = { children: , defaultFlags }; + describe('without cached flags', () => { + it('should notify parent about the default flag state', async () => { + const adapter = createAdapter(); + const defaultFlags = { + flagName: true, + }; + const props = { children: , defaultFlags }; + + const { waitUntilStatus, mergedRenderProps } = render({ props, adapter }); + + expect(mergedRenderProps.onFlagsStateChange).toHaveBeenCalledWith({ + flags: defaultFlags, + }); - const { waitUntilStatus, mergedRenderProps } = render({ props, adapter }); + await waitUntilStatus(); + }); + }); + describe('with cached flags', () => { + const cachedFlags = { + cachedFlag: true, + }; + const cacheKey = 'test'; + let cache; + + beforeEach(async () => { + cache = await getCache( + cacheIdentifiers.session, + adapterIdentifiers.memory, + cacheKey + ); - expect(mergedRenderProps.onFlagsStateChange).toHaveBeenCalledWith({ - flags: defaultFlags, + cache.set(cachedFlags); }); - await waitUntilStatus(); + it('should notify parent about the cached and default flag state', async () => { + const adapter = createAdapter(); + const defaultFlags = { + defaultFlag: true, + }; + const props = { + children: , + defaultFlags, + adapterArgs: { + cacheIdentifier: cacheIdentifiers.session, + clientSideId: 'foo-clientSideId', + user: { + key: 'foo-user-key', + }, + }, + }; + + const { waitUntilStatus, mergedRenderProps } = render({ props, adapter }); + + expect(mergedRenderProps.onFlagsStateChange).toHaveBeenCalledWith({ + flags: { + ...defaultFlags, + ...cachedFlags, + }, + }); + + await waitUntilStatus(); + }); }); }); diff --git a/packages/react/src/components/configure-adapter/configure-adapter.tsx b/packages/react/src/components/configure-adapter/configure-adapter.tsx index 80d39edcf..f418fbbd4 100644 --- a/packages/react/src/components/configure-adapter/configure-adapter.tsx +++ b/packages/react/src/components/configure-adapter/configure-adapter.tsx @@ -1,3 +1,4 @@ +import { getAllCachedFlags } from '@flopflip/cache'; import { AdapterConfigurationStatus, AdapterInitializationStatus, @@ -437,7 +438,10 @@ function ConfigureAdapter(props: TProps) { ] = useAdapterStateRef(); useDefaultFlagsEffect({ adapter: props.adapter, - defaultFlags: props.defaultFlags, + defaultFlags: { + ...props.defaultFlags, + ...getAllCachedFlags(props.adapter, props.adapterArgs.cacheIdentifier), + }, onFlagsStateChange: props.onFlagsStateChange, onStatusStateChange: props.onStatusStateChange, shouldDeferAdapterConfiguration: props.shouldDeferAdapterConfiguration, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index aa709857b..d8f003bde 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -37,6 +37,11 @@ export type TAdapterStatus = { configurationStatus: AdapterConfigurationStatus; subscriptionStatus: AdapterSubscriptionStatus; }; +export const cacheModes = { + eager: 'eager', + lazy: 'lazy', +} as const; +export type TCacheModes = (typeof cacheModes)[keyof typeof cacheModes]; export type TAdaptersStatus = Record; export type TAdapterStatusChange = { id?: TAdapterIdentifiers; @@ -52,6 +57,8 @@ export type TBaseAdapterArgs< TAdditionalUserProperties = TDefaultAdditionalUserProperties, > = { user: TUser; + cacheIdentifier?: TCacheIdentifiers; + cacheMode?: TCacheModes; }; export type TLaunchDarklyContextArgs = { context: LDContext }; export type TLaunchDarklyAdapterArgs = TLaunchDarklyContextArgs & { @@ -60,11 +67,10 @@ export type TLaunchDarklyAdapterArgs = TLaunchDarklyContextArgs & { clientOptions?: TLDOptions; }; flags?: TFlags; - subscribeToFlagChanges?: boolean; throwOnInitializationFailure?: boolean; flagsUpdateDelayMs?: number; cacheIdentifier?: TCacheIdentifiers; - unsubscribeFromCachedFlags?: boolean; + cacheMode?: TCacheModes; }; export type TGraphQlAdapterArgs< TAdditionalUserProperties = TDefaultAdditionalUserProperties, @@ -80,7 +86,6 @@ export type TGraphQlAdapterArgs< parseFlags?: ( fetchedFlags: TFetchedFlags ) => TParsedFlags; - cacheIdentifier?: TCacheIdentifiers; }; export type THttpAdapterArgs< TAdditionalUserProperties = TDefaultAdditionalUserProperties, @@ -91,7 +96,6 @@ export type THttpAdapterArgs< adapterArgs: TPassedAdapterArgs ) => Promise; pollingIntervalMs?: number; - cacheIdentifier?: TCacheIdentifiers; }; export type TLocalStorageAdapterArgs< TAdditionalUserProperties = TDefaultAdditionalUserProperties, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c59dee67..570afbba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,18 @@ importers: specifier: 5.4.3 version: 5.4.3 + packages/cache: + dependencies: + '@flopflip/localstorage-cache': + specifier: 13.6.0 + version: link:../localstorage-cache + '@flopflip/sessionstorage-cache': + specifier: 13.6.0 + version: link:../sessionstorage-cache + '@flopflip/types': + specifier: 13.6.0 + version: link:../types + packages/combine-adapters: dependencies: '@babel/runtime': @@ -284,6 +296,9 @@ importers: '@flopflip/adapter-utilities': specifier: 13.6.0 version: link:../adapter-utilities + '@flopflip/cache': + specifier: 13.5.2 + version: link:../cache '@flopflip/localstorage-cache': specifier: 13.6.0 version: link:../localstorage-cache @@ -315,6 +330,9 @@ importers: '@flopflip/adapter-utilities': specifier: 13.6.0 version: link:../adapter-utilities + '@flopflip/cache': + specifier: 13.5.2 + version: link:../cache '@flopflip/localstorage-cache': specifier: 13.6.0 version: link:../localstorage-cache @@ -346,6 +364,9 @@ importers: '@flopflip/adapter-utilities': specifier: 13.6.0 version: link:../adapter-utilities + '@flopflip/cache': + specifier: 13.5.2 + version: link:../cache '@flopflip/localstorage-cache': specifier: 13.6.0 version: link:../localstorage-cache @@ -439,6 +460,9 @@ importers: '@babel/runtime': specifier: 7.24.4 version: 7.24.4 + '@flopflip/cache': + specifier: 13.5.2 + version: link:../cache '@flopflip/types': specifier: 13.6.0 version: link:../types diff --git a/readme.md b/readme.md index fdefdea2b..7703347bf 100644 --- a/readme.md +++ b/readme.md @@ -305,7 +305,9 @@ _1. The `@flopflip/launchdarkly-adapter` accepts_ - `sdk.clientSideId`: The client side id of LaunchDarkly - `sdk.clientOptions`: additional options to be passed to the underlying SDK - `flags`: defaulting to `null` to subscribe only to specific flags. Helpful when not wanting to subscribe to all flags to utilise LaunchDarkly's automatic flag archiving functionality -- `subscribeToFlagChanges`: defaulting to `true` to disable real-time updates to flags once initially fetched +- `cacheMode`: defaulting to `null` to change application of cached flags + - The value can be `eager` to indicate that remote values should have effect immediately + - The value can be `lazy` to indicate that values should be updated in the cache but only be applied once the adapter is configured again - `throwOnInitializationFailure`: defaulting to `false` to indicate if the adapter just re-throw an error during initialization - `flagsUpdateDelayMs`: defaulting to `0` to debounce the flag update subscription