diff --git a/lib/event_processor/event_processor_factory.spec.ts b/lib/event_processor/event_processor_factory.spec.ts index 31b1b62ed..9aaa97f55 100644 --- a/lib/event_processor/event_processor_factory.spec.ts +++ b/lib/event_processor/event_processor_factory.spec.ts @@ -79,14 +79,14 @@ describe('getBatchEventProcessor', () => { defaultFlushInterval: 10000, defaultBatchSize: 10, eventStore: 'abc' as any, - })).toThrow('Invalid event store'); + })).toThrow('Invalid store'); expect(() => getBatchEventProcessor({ eventDispatcher: getMockEventDispatcher(), defaultFlushInterval: 10000, defaultBatchSize: 10, eventStore: 123 as any, - })).toThrow('Invalid event store'); + })).toThrow('Invalid store'); expect(() => getBatchEventProcessor({ eventDispatcher: getMockEventDispatcher(), diff --git a/lib/event_processor/event_processor_factory.ts b/lib/event_processor/event_processor_factory.ts index dd50c72f2..393ce436a 100644 --- a/lib/event_processor/event_processor_factory.ts +++ b/lib/event_processor/event_processor_factory.ts @@ -23,15 +23,13 @@ import { ForwardingEventProcessor } from "./forwarding_event_processor"; import { BatchEventProcessor, DEFAULT_MAX_BACKOFF, DEFAULT_MIN_BACKOFF, EventWithId, RetryConfig } from "./batch_event_processor"; import { AsyncPrefixStore, Store, SyncPrefixStore } from "../utils/cache/store"; import { Maybe } from "../utils/type"; +import { validateStore } from "../utils/cache/store_validator"; export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000; export const EVENT_STORE_PREFIX = 'optly_event:'; -export const INVALID_STORE = 'Invalid event store'; -export const INVALID_STORE_METHOD = 'Invalid store method %s'; - export const getPrefixEventStore = (store: Store): Store => { if (store.operation === 'async') { return new AsyncPrefixStore( @@ -84,23 +82,6 @@ export const validateEventDispatcher = (eventDispatcher: EventDispatcher): void } } -const validateStore = (store: any) => { - const errors = []; - if (!store || typeof store !== 'object') { - throw new Error(INVALID_STORE); - } - - for (const method of ['set', 'get', 'remove', 'getKeys']) { - if (typeof store[method] !== 'function') { - errors.push(INVALID_STORE_METHOD.replace('%s', method)); - } - } - - if (errors.length > 0) { - throw new Error(errors.join(', ')); - } -} - export const getBatchEventProcessor = ( options: BatchEventProcessorFactoryOptions, EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor diff --git a/lib/project_config/config_manager_factory.spec.ts b/lib/project_config/config_manager_factory.spec.ts index 1ad4dc689..7def4f9a8 100644 --- a/lib/project_config/config_manager_factory.spec.ts +++ b/lib/project_config/config_manager_factory.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024, Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,52 @@ describe('getPollingConfigManager', () => { MockExponentialBackoff.mockClear(); }); + it('should throw an error if the passed cache is not valid', () => { + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: 1 as any, + })).toThrow('Invalid store'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: 'abc' as any, + })).toThrow('Invalid store'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: {} as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: 'abc', get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + const noop = () => {}; + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: noop, remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: noop, remove: noop, getKeys: 'abc' } as any, + })).toThrow('Invalid store method getKeys'); + }); + it('uses a repeater with exponential backoff for the datafileManager', () => { const config = { sdkKey: 'sdkKey', diff --git a/lib/project_config/config_manager_factory.ts b/lib/project_config/config_manager_factory.ts index 89c60593c..e7d21aeea 100644 --- a/lib/project_config/config_manager_factory.ts +++ b/lib/project_config/config_manager_factory.ts @@ -25,6 +25,7 @@ import { StartupLog } from "../service"; import { MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './constant'; import { LogLevel } from '../logging/logger' import { Store } from "../utils/cache/store"; +import { validateStore } from "../utils/cache/store_validator"; export const INVALID_CONFIG_MANAGER = "Invalid config manager"; @@ -63,6 +64,10 @@ export type PollingConfigManagerFactoryOptions = PollingConfigManagerConfig & { export const getPollingConfigManager = ( opt: PollingConfigManagerFactoryOptions ): ProjectConfigManager => { + if (opt.cache) { + validateStore(opt.cache); + } + const updateInterval = opt.updateInterval ?? DEFAULT_UPDATE_INTERVAL; const backoff = new ExponentialBackoff(1000, updateInterval, 500); diff --git a/lib/utils/cache/store_validator.ts b/lib/utils/cache/store_validator.ts new file mode 100644 index 000000000..949bb25c3 --- /dev/null +++ b/lib/utils/cache/store_validator.ts @@ -0,0 +1,36 @@ + +/** + * Copyright 2025, 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. + */ +export const INVALID_STORE = 'Invalid store'; +export const INVALID_STORE_METHOD = 'Invalid store method %s'; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const validateStore = (store: any): void => { + const errors = []; + if (!store || typeof store !== 'object') { + throw new Error(INVALID_STORE); + } + + for (const method of ['set', 'get', 'remove', 'getKeys']) { + if (typeof store[method] !== 'function') { + errors.push(INVALID_STORE_METHOD.replace('%s', method)); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } +} diff --git a/vitest.config.mts b/vitest.config.mts index 584eeb60d..1bce36eb0 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,5 +1,5 @@ /** - * Copyright 2024 Optimizely + * Copyright 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.