diff --git a/sdk/js/__tests__/Client.spec.js b/sdk/js/__tests__/Client.spec.js index 83673b8db..5e27cfe4a 100644 --- a/sdk/js/__tests__/Client.spec.js +++ b/sdk/js/__tests__/Client.spec.js @@ -56,6 +56,40 @@ describe('DevCycleClient tests', () => { expect(client.config).toStrictEqual(testConfig) }) + it('should use bootstrapped config when set, not fetch a new one, initialize SDK', async () => { + const client = new DevCycleClient( + 'test_sdk_key', + { user_id: 'user1' }, + { bootstrapConfig: testConfig }, + ) + await client.onInitialized + expect(getConfigJson_mock).not.toBeCalled() + expect(client.config).toStrictEqual(testConfig) + expect(client.user.user_id).toEqual('user1') + }) + + it('should use bootstrapped config when set, ignore cached config', async () => { + getConfigJson_mock.mockImplementation(() => { + return Promise.resolve({ ...testConfig, cached: true }) + }) + const spy = jest.spyOn(window.localStorage.__proto__, 'getItem') + const client = new DevCycleClient('test_sdk_key', { user_id: 'user1' }) + await client.onInitialized + // one call to get anon ID, one to get cached config + expect(spy).toHaveBeenCalledTimes(2) + // construct another client to test if it reads from the cache populated by the initialization of the first + // client + const client2 = new DevCycleClient( + 'test_sdk_key', + { user_id: 'user1' }, + { bootstrapConfig: testConfig }, + ) + await client2.onInitialized + // one more call to get anon ID + expect(spy).toHaveBeenCalledTimes(3) + expect(client2.config).toStrictEqual(testConfig) + }) + it('should establish a streaming connection if available', async () => { getConfigJson_mock.mockImplementation(() => { return Promise.resolve({ diff --git a/sdk/js/src/Client.ts b/sdk/js/src/Client.ts index 61c5d0c29..890dd31b7 100644 --- a/sdk/js/src/Client.ts +++ b/sdk/js/src/Client.ts @@ -43,6 +43,7 @@ type variableEvaluatedHandler = ( export type DevCycleOptionsWithDeferredInitialization = DevCycleOptions & { deferInitialization: true + bootstrapConfig?: never } export const isDeferredOptions = ( @@ -132,6 +133,10 @@ export class DevCycleClient< throw new Error('User must be provided to initialize SDK') } this.clientInitialization(user) + } else if (this.options.bootstrapConfig) { + throw new Error( + 'bootstrapConfig option can not be combined with deferred initialization!', + ) } if (!options?.reactNative && typeof window !== 'undefined') { @@ -171,7 +176,9 @@ export class DevCycleClient< storedAnonymousId, ) - await this.getConfigCache(this.user) + if (!this.options.bootstrapConfig) { + await this.getConfigCache(this.user) + } // set up requestConsolidator and hook up callback methods this.requestConsolidator = new ConfigRequestConsolidator( @@ -189,7 +196,15 @@ export class DevCycleClient< ) try { - await this.requestConsolidator.queue(this.user) + if (!this.options.bootstrapConfig) { + await this.requestConsolidator.queue(this.user) + } else { + this.handleConfigReceived( + this.options.bootstrapConfig, + this.user, + Date.now(), + ) + } } catch (err) { this.eventEmitter.emitInitialized(false) this.eventEmitter.emitError(err) @@ -727,6 +742,7 @@ export class DevCycleClient< user, this.options.configCacheTTL, ) + if (cachedConfig) { this.config = cachedConfig this.isConfigCached = true diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index 465a1b3a5..2b9161f98 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -5,6 +5,8 @@ import { VariableValue, DVCJSON, DVCCustomDataJSON, + ConfigBody, + BucketedUserConfig, } from '@devcycle/types' export type DVCVariableValue = VariableValue @@ -103,6 +105,14 @@ export interface DevCycleOptions { * Controls the maximum size the event queue can grow to until events are dropped. Defaults to `1000`. */ maxEventQueueSize?: number + + /** + * A full configuration payload to boostrap the SDK with. This will immediately initialize the SDK with + * the provided data and prevent a server roundtrip to fetch a new config. This option can be used for passing + * in a config that was prefetched in a server-side rendered environment, or to implement a custom caching + * system. + */ + bootstrapConfig?: BucketedUserConfig } export interface DevCycleUser { diff --git a/sdk/react/src/initializeDevCycleClient.ts b/sdk/react/src/initializeDevCycleClient.ts index 032e4472c..2af4249df 100644 --- a/sdk/react/src/initializeDevCycleClient.ts +++ b/sdk/react/src/initializeDevCycleClient.ts @@ -10,6 +10,7 @@ const initializeDevCycleClient = ( return initializeDevCycle(sdkKey, { ...options, deferInitialization: true, // make typescript happy + bootstrapConfig: undefined, }) } return initializeDevCycle(sdkKey, user, options)