diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 7da3ffb7b..002d42ed5 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -33,9 +33,7 @@ import Optimizely from './optimizely'; import { IUserAgentParser } from './core/odp/user_agent_parser'; import { getUserAgentParser } from './plugins/odp/user_agent_parser/index.browser'; import * as commonExports from './common_exports'; -import { VuidManager } from './plugins/vuid_manager'; -import BrowserAsyncStorageCache from './plugins/key_value_cache/browserAsyncStorageCache'; -import { VuidManagerOptions } from './plugins/vuid_manager'; +import { vuidManager } from './plugins/vuid_manager/index.browser'; const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -136,11 +134,6 @@ const createInstance = function(config: Config): Client | null { const { clientEngine, clientVersion } = config; - const cache = new BrowserAsyncStorageCache(); - const vuidManagerOptions: VuidManagerOptions = { - enableVuid: config.vuidOptions?.enableVuid || false, - } - const optimizelyOptions: OptimizelyOptions = { clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, ...config, @@ -154,7 +147,8 @@ const createInstance = function(config: Config): Client | null { isValidInstance, odpManager: odpExplicitlyOff ? undefined : BrowserOdpManager.createInstance({ logger, odpOptions: config.odpOptions, clientEngine, clientVersion }), - vuidManager: new VuidManager(cache, vuidManagerOptions, logger), + vuidOptions: config.vuidOptions, + vuidManager, }; const optimizely = new Optimizely(optimizelyOptions); diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index 1cc9e2886..2af169f88 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -28,8 +28,7 @@ import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/react_native_http_polling_datafile_manager'; import { BrowserOdpManager } from './plugins/odp_manager/index.browser'; import * as commonExports from './common_exports'; -import { VuidManager, VuidManagerOptions } from './plugins/vuid_manager'; -import ReactNativeAsyncStorageCache from './plugins/key_value_cache/reactNativeAsyncStorageCache'; +import { vuidManager } from './plugins/vuid_manager/index.react_native'; import 'fast-text-encoding'; import 'react-native-get-random-values'; @@ -109,11 +108,6 @@ const createInstance = function (config: Config): Client | null { const { clientEngine, clientVersion } = config; - const cache = new ReactNativeAsyncStorageCache(); - const vuidManagerOptions: VuidManagerOptions = { - enableVuid: config.vuidOptions?.enableVuid || false, - } - const optimizelyOptions = { clientEngine: enums.REACT_NATIVE_JS_CLIENT_ENGINE, ...config, @@ -133,7 +127,8 @@ const createInstance = function (config: Config): Client | null { isValidInstance: isValidInstance, odpManager: odpExplicitlyOff ? undefined : BrowserOdpManager.createInstance({ logger, odpOptions: config.odpOptions, clientEngine, clientVersion }), - vuidManager: new VuidManager(cache, vuidManagerOptions, logger), + vuidOptions: config.vuidOptions, + vuidManager, }; // If client engine is react, convert it to react native. diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index afc221b22..42a4dc20d 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -37,6 +37,7 @@ import { FeatureVariableValue, OptimizelyDecision, Client, + VuidOptions, } from '../shared_types'; import { newErrorDecision } from '../optimizely_decision'; import OptimizelyUserContext from '../optimizely_user_context'; @@ -98,6 +99,7 @@ export default class Optimizely implements Client { private eventProcessor: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; protected odpManager?: IOdpManager; + private vuidOptions?: VuidOptions; protected vuidManager?: IVuidManager; public notificationCenter: NotificationCenter; @@ -114,6 +116,7 @@ export default class Optimizely implements Client { this.isOptimizelyConfigValid = config.isValidInstance; this.logger = config.logger; this.odpManager = config.odpManager; + this.vuidOptions = config.vuidOptions; this.vuidManager = config.vuidManager; let decideOptionsArray = config.defaultDecideOptions ?? []; @@ -183,7 +186,7 @@ export default class Optimizely implements Client { projectConfigManagerReadyPromise, eventProcessorStartedPromise, config.odpManager ? config.odpManager.onReady() : Promise.resolve(), - config.vuidManager ? config.vuidManager?.initialize() : Promise.resolve(), + config.vuidManager ? config.vuidManager.configure(this.vuidOptions ?? { enableVuid: false }) : Promise.resolve(), ]).then(promiseResults => { // Only return status from project config promise because event processor promise does not return any status. return promiseResults[0]; diff --git a/lib/plugins/vuid_manager/index.browser.ts b/lib/plugins/vuid_manager/index.browser.ts new file mode 100644 index 000000000..722a037d5 --- /dev/null +++ b/lib/plugins/vuid_manager/index.browser.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { VuidManager } from "."; +import BrowserAsyncStorageCache from "../key_value_cache/browserAsyncStorageCache"; + +export const vuidManager = new VuidManager(new BrowserAsyncStorageCache()); diff --git a/lib/plugins/vuid_manager/index.react_native.ts b/lib/plugins/vuid_manager/index.react_native.ts new file mode 100644 index 000000000..5b0d68bb9 --- /dev/null +++ b/lib/plugins/vuid_manager/index.react_native.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { VuidManager } from "."; +import ReactNativeAsyncStorageCache from "../key_value_cache/reactNativeAsyncStorageCache"; + +export const vuidManager = new VuidManager(new ReactNativeAsyncStorageCache()); diff --git a/lib/plugins/vuid_manager/index.ts b/lib/plugins/vuid_manager/index.ts index 135aab5a2..5bb366581 100644 --- a/lib/plugins/vuid_manager/index.ts +++ b/lib/plugins/vuid_manager/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../../modules/logging'; +import { LogHandler } from '../../modules/logging'; import { uuid } from '../../utils/fns'; import PersistentKeyValueCache from '../key_value_cache/persistentKeyValueCache'; @@ -37,7 +37,8 @@ export interface IVuidManager { * Initialize the VuidManager * @returns Promise that resolves when the VuidManager is initialized */ - initialize(): Promise; + configure(options: VuidManagerOptions): Promise; + setLogger(logger: LogHandler): void; } /** @@ -48,7 +49,7 @@ export class VuidManager implements IVuidManager { * Handler for recording execution logs * @private */ - private readonly logger: LogHandler; + private logger?: LogHandler; /** * Prefix used as part of the VUID format @@ -97,25 +98,39 @@ export class VuidManager implements IVuidManager { */ private readonly cache: PersistentKeyValueCache; - constructor(cache: PersistentKeyValueCache, options: VuidManagerOptions, logger: LogHandler) { + private waitPromise: Promise = Promise.resolve(); + + constructor(cache: PersistentKeyValueCache, logger?: LogHandler) { this.cache = cache; - this._vuidEnabled = options.enableVuid; + this.logger = logger; + } + + setLogger(logger: LogHandler): void { this.logger = logger; } /** - * Initialize the VuidManager - * @returns Promise that resolves when the VuidManager is initialized + * Configures the VuidManager + * @returns Promise that resolves when the VuidManager is configured */ - async initialize(): Promise { - if (!this.vuidEnabled) { - await this.cache.remove(this._keyForVuid); - return; + async configure(options: VuidManagerOptions): Promise { + const configureFn = async () => { + this._vuidEnabled = options.enableVuid; + + if (!this.vuidEnabled) { + await this.cache.remove(this._keyForVuid); + this._vuid = undefined; + return; + } + + if (!this._vuid) { + await this.load(this.cache); + } } - if (!this._vuid) { - await this.load(this.cache); - } + this.waitPromise = this.waitPromise.then(configureFn, configureFn); + this.waitPromise.catch(() => {}); + return this.waitPromise; } /** diff --git a/lib/shared_types.ts b/lib/shared_types.ts index f6c8da63b..9af9ee96c 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -305,6 +305,7 @@ export interface OptimizelyOptions { userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; odpManager?: IOdpManager; + vuidOptions?: VuidOptions, vuidManager?: IVuidManager; notificationCenter: NotificationCenterImpl; } diff --git a/tests/vuidManager.spec.ts b/tests/vuidManager.spec.ts index 41d26db56..2bc246a7c 100644 --- a/tests/vuidManager.spec.ts +++ b/tests/vuidManager.spec.ts @@ -20,6 +20,7 @@ import { VuidManager } from '../lib/plugins/vuid_manager'; import PersistentKeyValueCache from '../lib/plugins/key_value_cache/persistentKeyValueCache'; import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { LogHandler } from '../lib/modules/logging/models'; +import { resolvablePromise } from '../lib/utils/promise/resolvablePromise'; describe('VuidManager', () => { let mockCache: PersistentKeyValueCache; @@ -41,7 +42,7 @@ describe('VuidManager', () => { }); it('should make a VUID', async () => { - const manager = new VuidManager(instance(mockCache), { enableVuid: true }, instance(mockLogger)); + const manager = new VuidManager(instance(mockCache)); const vuid = manager['makeVuid'](); @@ -56,40 +57,68 @@ describe('VuidManager', () => { expect(VuidManager.isVuid('123')).toBe(false); }); - it('should handle no valid optimizely-vuid in the cache', async () => { - when(mockCache.get(anyString())).thenResolve(undefined); - const manager = new VuidManager(instance(mockCache), { enableVuid: true }, instance(mockLogger)); - - await manager.initialize(); - - verify(mockCache.get(anyString())).once(); - verify(mockCache.set(anyString(), anything())).once(); - expect(VuidManager.isVuid(manager.vuid)).toBe(true); - }); - - it('should create a new vuid if old VUID from cache is not valid', async () => { - when(mockCache.get(anyString())).thenResolve('vuid-not-valid'); - const manager = new VuidManager(instance(mockCache), { enableVuid: true }, instance(mockLogger)); - await manager.initialize(); - - verify(mockCache.get(anyString())).once(); - verify(mockCache.set(anyString(), anything())).once(); - expect(VuidManager.isVuid(manager.vuid)).toBe(true); + describe('when configure with enableVuid = true', () => { + it('should handle no valid optimizely-vuid in the cache', async () => { + when(mockCache.get(anyString())).thenResolve(undefined); + const manager = new VuidManager(instance(mockCache)) + await manager.configure({ enableVuid: true }); + + verify(mockCache.get(anyString())).once(); + verify(mockCache.set(anyString(), anything())).once(); + expect(VuidManager.isVuid(manager.vuid)).toBe(true); + }); + + it('should create a new vuid if old VUID from cache is not valid', async () => { + when(mockCache.get(anyString())).thenResolve('vuid-not-valid'); + const manager = new VuidManager(instance(mockCache)); + await manager.configure({ enableVuid: true }); + + verify(mockCache.get(anyString())).once(); + verify(mockCache.set(anyString(), anything())).once(); + expect(VuidManager.isVuid(manager.vuid)).toBe(true); + }); + + it('should never call remove when enableVuid is true', async () => { + const manager = new VuidManager(instance(mockCache)); + await manager.configure({ enableVuid: true }); + + verify(mockCache.remove(anyString())).never(); + expect(VuidManager.isVuid(manager.vuid)).toBe(true); + }); }); it('should call remove when vuid is disabled', async () => { - const manager = new VuidManager(instance(mockCache), { enableVuid: false }, instance(mockLogger)); - await manager.initialize(); + const manager = new VuidManager(instance(mockCache)); + await manager.configure({ enableVuid: false }); verify(mockCache.remove(anyString())).once(); expect(manager.vuid).toBeUndefined(); }); - it('should never call remove when enableVuid is true', async () => { - const manager = new VuidManager(instance(mockCache), { enableVuid: true }, instance(mockLogger)); - await manager.initialize(); + it('should sequence configure calls', async() => { + const mockCache = mock(); + when(mockCache.contains(anyString())).thenResolve(true); + when(mockCache.get(anyString())).thenResolve(''); + + const removePromise = resolvablePromise(); + when(mockCache.remove(anyString())).thenReturn(removePromise.promise); + when(mockCache.set(anyString(), anything())).thenResolve(); + + const manager = new VuidManager(instance(mockCache)); + + // this should try to remove vuid, which should stay pending + manager.configure({ enableVuid: false }); + + // this should try to get the vuid from store + manager.configure({ enableVuid: true }); + verify(mockCache.get(anyString())).never(); + + removePromise.resolve(true); + //ensure micro task queue is exhaused + for(let i = 0; i < 100; i++) { + await Promise.resolve(); + } - verify(mockCache.remove(anyString())).never(); - expect(VuidManager.isVuid(manager.vuid)).toBe(true); + verify(mockCache.get(anyString())).once() }); });