diff --git a/package.json b/package.json index 5fc19a0..f3517e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk", - "version": "3.4.0", + "version": "3.5.0", "description": "Eppo SDK for client-side JavaScript applications", "main": "dist/index.js", "files": [ @@ -61,4 +61,4 @@ "dependencies": { "@eppo/js-client-sdk-common": "4.0.0" } -} +} \ No newline at end of file diff --git a/src/index.spec.ts b/src/index.spec.ts index 971bb4c..668faeb 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -191,6 +191,7 @@ describe('EppoJSClient E2E test', () => { apiKey, baseUrl, assignmentLogger: mockLogger, + forceReinitialize: true, }); }); @@ -335,6 +336,7 @@ describe('EppoJSClient E2E test', () => { apiKey, baseUrl, assignmentLogger: mockLogger, + forceReinitialize: true, }); }); @@ -494,6 +496,61 @@ describe('initialization options', () => { expect(callCount).toBe(2); }); + it('do not reinitialize if already initialized', async () => { + let callCount = 0; + + global.fetch = jest.fn(() => { + callCount += 1; + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(mockConfigResponse), + }); + }) as jest.Mock; + + await init({ + apiKey, + baseUrl, + assignmentLogger: mockLogger, + }); + + await init({ + apiKey, + baseUrl, + assignmentLogger: mockLogger, + }); + + expect(callCount).toBe(1); + }); + + it('force reinitialize', async () => { + let callCount = 0; + + global.fetch = jest.fn(() => { + callCount += 1; + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(mockConfigResponse), + }); + }) as jest.Mock; + + await init({ + apiKey, + baseUrl, + assignmentLogger: mockLogger, + }); + + await init({ + apiKey, + baseUrl, + assignmentLogger: mockLogger, + forceReinitialize: true, + }); + + expect(callCount).toBe(2); + }); + it('polls after successful init if configured to do so', async () => { let callCount = 0; @@ -723,6 +780,7 @@ describe('initialization options', () => { apiKey, baseUrl, assignmentLogger: mockLogger, + forceReinitialize: true, }); expect(fetchCallCount).toBe(1); @@ -735,6 +793,7 @@ describe('initialization options', () => { baseUrl: 'https://thisisabaddomainforthistest.com', assignmentLogger: mockLogger, useExpiredCache: true, + forceReinitialize: true, }); // Should serve assignment from cache before fetch even fails @@ -754,6 +813,7 @@ describe('initialization options', () => { baseUrl: 'https://thisisabaddomainforthistest.com', assignmentLogger: mockLogger, useExpiredCache: true, + forceReinitialize: true, }), ).rejects.toThrow(); }); @@ -854,6 +914,7 @@ describe('initialization options', () => { baseUrl, assignmentLogger: mockLogger, updateOnFetch, + forceReinitialize: true, }); expect(fetchCallCount).toBe(1); @@ -871,6 +932,7 @@ describe('initialization options', () => { assignmentLogger: mockLogger, updateOnFetch, useExpiredCache: true, + forceReinitialize: true, }); // Should serve assignment from cache before fetch completes @@ -900,6 +962,7 @@ describe('initialization options', () => { assignmentLogger: mockLogger, updateOnFetch, maxCacheAgeSeconds: fetchResolveDelayMs * 10, + forceReinitialize: true, }); // No fetch will have been kicked off because of valid cache; previously fetched values will be served @@ -942,6 +1005,7 @@ describe('initialization options', () => { apiKey, baseUrl, assignmentLogger: mockLogger, + forceReinitialize: true, }); }); @@ -955,6 +1019,7 @@ describe('initialization options', () => { apiKey, baseUrl, assignmentLogger: mockLogger, + forceReinitialize: true, }); expect(getInstance().getStringAssignment(flagKey, 'subject', {}, 'default-value')).toBe( 'default-value', diff --git a/src/index.ts b/src/index.ts index bf192da..599ac6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,6 +113,11 @@ export interface IClientConfig { * than the default storage provided by the SDK. */ persistentStore?: IAsyncStore; + + /** + * Force reinitialize the SDK if it is already initialized. + */ + forceReinitialize?: boolean; } export interface IClientConfigSync { @@ -381,8 +386,23 @@ export async function init(config: IClientConfig): Promise { validation.validateNotBlank(config.apiKey, 'API key required'); let initializationError: Error | undefined; const instance = EppoJSClient.instance; - const { apiKey, persistentStore, baseUrl, maxCacheAgeSeconds, updateOnFetch } = config; + const { apiKey, persistentStore, baseUrl, maxCacheAgeSeconds, updateOnFetch, forceReinitialize } = + config; try { + if (EppoJSClient.initialized) { + if (forceReinitialize) { + applicationLogger.warn( + 'Eppo SDK is already initialized, reinitializing since forceReinitialize is true.', + ); + EppoJSClient.initialized = false; + } else { + applicationLogger.warn( + 'Eppo SDK is already initialized, skipping reinitialization since forceReinitialize is false.', + ); + return instance; + } + } + // If any existing instances; ensure they are not polling instance.stopPolling(); // Set up assignment logger and cache