diff --git a/lib/exception_messages.ts b/lib/exception_messages.ts index f17fa2821..731607ff8 100644 --- a/lib/exception_messages.ts +++ b/lib/exception_messages.ts @@ -31,7 +31,6 @@ export const DATAFILE_MANAGER_STOPPED = 'Datafile manager stopped before it coul export const DATAFILE_MANAGER_FAILED_TO_START = 'Datafile manager failed to start'; export const FAILED_TO_FETCH_DATAFILE = 'Failed to fetch datafile'; export const FAILED_TO_STOP = 'Failed to stop'; -export const YOU_MUST_PROVIDE_DATAFILE_IN_SSR = 'You must provide datafile in SSR'; export const YOU_MUST_PROVIDE_AT_LEAST_ONE_OF_SDKKEY_OR_DATAFILE = 'You must provide at least one of sdkKey or datafile'; export const RETRY_CANCELLED = 'Retry cancelled'; export const REQUEST_TIMEOUT = 'Request timeout'; diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index dc6e2b96b..98eaedcd0 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -51,6 +51,7 @@ const getMockOdpEventManager = () => { getState: vi.fn(), updateConfig: vi.fn(), sendEvent: vi.fn(), + makeDisposable: vi.fn(), }; }; diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts index 5ced36a08..2e66f20c2 100644 --- a/lib/optimizely/index.spec.ts +++ b/lib/optimizely/index.spec.ts @@ -16,13 +16,10 @@ import { describe, it, expect, vi } from 'vitest'; import Optimizely from '.'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; -import * as logger from '../plugins/logger'; import * as jsonSchemaValidator from '../utils/json_schema_validator'; -import { LOG_LEVEL } from '../common_exports'; import { createNotificationCenter } from '../notification_center'; import testData from '../tests/test_data'; import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; -import { LoggerFacade } from '../modules/logging'; import { createProjectConfig } from '../project_config/project_config'; import { getMockLogger } from '../tests/mock/mock_logger'; @@ -39,12 +36,12 @@ describe('Optimizely', () => { const notificationCenter = createNotificationCenter({ logger, errorHandler }); - it('should pass ssr to the project config manager', () => { + it('should pass disposable option to the project config manager', () => { const projectConfigManager = getMockProjectConfigManager({ initConfig: createProjectConfig(testData.getTestProjectConfig()), }); - - vi.spyOn(projectConfigManager, 'setSsr'); + + vi.spyOn(projectConfigManager, 'makeDisposable'); const instance = new Optimizely({ clientEngine: 'node-sdk', @@ -54,16 +51,16 @@ describe('Optimizely', () => { logger, notificationCenter, eventProcessor, - isSsr: true, + disposable: true, isValidInstance: true, }); - expect(projectConfigManager.setSsr).toHaveBeenCalledWith(true); + expect(projectConfigManager.makeDisposable).toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(instance.getProjectConfig()).toBe(projectConfigManager.config); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expect(projectConfigManager.isSsr).toBe(true); + expect(projectConfigManager.disposable).toBe(true); }); }); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 34fa116f6..9c169cd9e 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -176,7 +176,10 @@ export default class Optimizely implements Client { this.updateOdpSettings(); }); - this.projectConfigManager.setSsr(config.isSsr) + if(config.disposable) { + this.projectConfigManager.makeDisposable(); + } + this.projectConfigManager.start(); const projectConfigManagerRunningPromise = this.projectConfigManager.onRunning(); diff --git a/lib/project_config/polling_datafile_manager.spec.ts b/lib/project_config/polling_datafile_manager.spec.ts index 3efae54d7..80dfdb4e0 100644 --- a/lib/project_config/polling_datafile_manager.spec.ts +++ b/lib/project_config/polling_datafile_manager.spec.ts @@ -470,6 +470,25 @@ describe('PollingDatafileManager', () => { expect(repeater.stop).toHaveBeenCalled(); }); + it('stops repeater after successful initialization if disposable is true', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + manager.makeDisposable(); + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(repeater.stop).toHaveBeenCalled(); + }); + it('saves the datafile in cache', async () => { const repeater = getMockRepeater(); const requestHandler = getMockRequestHandler(); diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index bde704029..99d3f9612 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -89,6 +89,11 @@ export class PollingDatafileManager extends BaseService implements DatafileManag return; } + // If disposable, reset the retry count to 5 + if(this.disposable) { + this.initRetryRemaining = 5; + } + super.start(); this.state = ServiceState.Starting; this.setDatafileFromCacheIfAvailable(); @@ -162,7 +167,8 @@ export class PollingDatafileManager extends BaseService implements DatafileManag if (datafile) { this.handleDatafile(datafile); // if autoUpdate is off, don't need to sync datafile any more - if (!this.autoUpdate) { + // if disposable, stop the repeater after the first successful fetch + if (!this.autoUpdate || this.disposable) { this.repeater.stop(); } } diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 7bed978ef..e7b8b73ff 100644 --- a/lib/project_config/project_config_manager.spec.ts +++ b/lib/project_config/project_config_manager.spec.ts @@ -165,17 +165,6 @@ describe('ProjectConfigManagerImpl', () => { await manager.onRunning(); expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); }); - - it('should not start datafileManager if isSsr is true and return correct config', () => { - const datafileManager = getMockDatafileManager({}); - vi.spyOn(datafileManager, 'start'); - const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); - manager.setSsr(true); - manager.start(); - - expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); - expect(datafileManager.start).not.toHaveBeenCalled(); - }); }); describe('when datafile is invalid', () => { @@ -409,16 +398,6 @@ describe('ProjectConfigManagerImpl', () => { expect(logger.error).toHaveBeenCalled(); }); - it('should reject onRunning() and log error if isSsr is true and datafile is not provided', async () =>{ - const logger = getMockLogger(); - const manager = new ProjectConfigManagerImpl({ logger, datafileManager: getMockDatafileManager({})}); - manager.setSsr(true); - manager.start(); - - await expect(manager.onRunning()).rejects.toThrow(); - expect(logger.error).toHaveBeenCalled(); - }); - it('should reject onRunning() and log error if the datafile version is not supported', async () => { const logger = getMockLogger(); const datafile = testData.getUnsupportedVersionConfig(); diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index 81ee87b78..9fb020fa4 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -26,7 +26,6 @@ import { DATAFILE_MANAGER_FAILED_TO_START, DATAFILE_MANAGER_STOPPED, YOU_MUST_PROVIDE_AT_LEAST_ONE_OF_SDKKEY_OR_DATAFILE, - YOU_MUST_PROVIDE_DATAFILE_IN_SSR, } from '../exception_messages'; interface ProjectConfigManagerConfig { @@ -40,7 +39,6 @@ interface ProjectConfigManagerConfig { export interface ProjectConfigManager extends Service { setLogger(logger: LoggerFacade): void; - setSsr(isSsr?: boolean): void; getConfig(): ProjectConfig | undefined; getOptimizelyConfig(): OptimizelyConfig | undefined; onUpdate(listener: Consumer): Fn; @@ -60,7 +58,6 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf public jsonSchemaValidator?: Transformer; public datafileManager?: DatafileManager; private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); - private isSsr = false; constructor(config: ProjectConfigManagerConfig) { super(); @@ -77,17 +74,8 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf this.state = ServiceState.Starting; - if(this.isSsr) { - // If isSsr is true, we don't need to poll for datafile updates - this.datafileManager = undefined - } - if (!this.datafile && !this.datafileManager) { - const errorMessage = this.isSsr - ? YOU_MUST_PROVIDE_DATAFILE_IN_SSR - : YOU_MUST_PROVIDE_AT_LEAST_ONE_OF_SDKKEY_OR_DATAFILE; - - this.handleInitError(new Error(errorMessage)); + this.handleInitError(new Error(YOU_MUST_PROVIDE_AT_LEAST_ONE_OF_SDKKEY_OR_DATAFILE)); return; } @@ -95,6 +83,10 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf this.handleNewDatafile(this.datafile, true); } + if(this.disposable) { + this.datafileManager?.makeDisposable(); + } + this.datafileManager?.start(); // This handles the case where the datafile manager starts successfully. The @@ -227,13 +219,4 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf this.stopPromise.reject(err); }); } - - /** - * Set the isSsr flag to indicate if the project config manager is being used in a server side rendering environment - * @param {Boolean} isSsr - * @returns {void} - */ - setSsr(isSsr: boolean): void { - this.isSsr = isSsr; - } } diff --git a/lib/service.ts b/lib/service.ts index 459488027..2d0877bee 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -51,6 +51,7 @@ export interface Service { // either by failing to start or stop. // It will resolve if the service is stopped successfully. onTerminated(): Promise; + makeDisposable(): void; } export abstract class BaseService implements Service { @@ -59,7 +60,7 @@ export abstract class BaseService implements Service { protected stopPromise: ResolvablePromise; protected logger?: LoggerFacade; protected startupLogs: StartupLog[]; - + protected disposable = false; constructor(startupLogs: StartupLog[] = []) { this.state = ServiceState.New; this.startPromise = resolvablePromise(); @@ -71,6 +72,10 @@ export abstract class BaseService implements Service { this.stopPromise.promise.catch(() => {}); } + makeDisposable(): void { + this.disposable = true; + } + setLogger(logger: LoggerFacade): void { this.logger = logger; } diff --git a/lib/shared_types.ts b/lib/shared_types.ts index b2ebad540..59dd3adf9 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -263,10 +263,10 @@ export interface OptimizelyOptions { sdkKey?: string; userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; - isSsr?:boolean; odpManager?: OdpManager; notificationCenter: DefaultNotificationCenter; vuidManager?: VuidManager + disposable?:boolean; } /** @@ -384,9 +384,9 @@ export interface Config { defaultDecideOptions?: OptimizelyDecideOption[]; clientEngine?: string; clientVersion?: string; - isSsr?: boolean; odpManager?: OdpManager; vuidManager?: VuidManager; + disposable?:boolean; } export type OptimizelyExperimentsMap = { diff --git a/lib/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts index b76f71e2d..65c6268ab 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -26,12 +26,12 @@ type MockOpt = { export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigManager => { return { - isSsr: false, + disposable: false, config: opt.initConfig, - start: () => {}, - setSsr: function(isSsr:boolean) { - this.isSsr = isSsr; + makeDisposable(){ + this.disposable = true; }, + start: () => {}, onRunning: () => opt.onRunning || Promise.resolve(), stop: () => {}, onTerminated: () => opt.onTerminated || Promise.resolve(),