Skip to content

Commit e2690af

Browse files
Merge branch 'master' into junaed/fssdk-11403-readme-update
2 parents 640d18f + e46273a commit e2690af

File tree

10 files changed

+131
-47
lines changed

10 files changed

+131
-47
lines changed

lib/event_processor/batch_event_processor.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor {
7474
private batchSize: number;
7575
private eventStore?: Store<EventWithId>;
7676
private eventCountInStore: Maybe<number> = undefined;
77+
private eventCountWaitPromise: Promise<unknown> = Promise.resolve();
7778
private maxEventsInStore: number = MAX_EVENTS_IN_STORE;
7879
private dispatchRepeater: Repeater;
7980
private failedEventRepeater?: Repeater;
@@ -264,15 +265,22 @@ export class BatchEventProcessor extends BaseService implements EventProcessor {
264265
}
265266
}
266267

267-
private async findEventCountInStore(): Promise<void> {
268+
private async readEventCountInStore(store: Store<EventWithId>): Promise<void> {
269+
try {
270+
const keys = await store.getKeys();
271+
this.eventCountInStore = keys.length;
272+
} catch (e) {
273+
this.logger?.error(e);
274+
}
275+
}
276+
277+
private async findEventCountInStore(): Promise<unknown> {
268278
if (this.eventStore && this.eventCountInStore === undefined) {
269-
try {
270-
const keys = await this.eventStore.getKeys();
271-
this.eventCountInStore = keys.length;
272-
} catch (e) {
273-
this.logger?.error(e);
274-
}
279+
const store = this.eventStore;
280+
this.eventCountWaitPromise = this.eventCountWaitPromise.then(() => this.readEventCountInStore(store));
281+
return this.eventCountWaitPromise;
275282
}
283+
return Promise.resolve();
276284
}
277285

278286
private async storeEvent(eventWithId: EventWithId): Promise<void> {

lib/event_processor/event_processor_factory.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ describe('getBatchEventProcessor', () => {
7979
defaultFlushInterval: 10000,
8080
defaultBatchSize: 10,
8181
eventStore: 'abc' as any,
82-
})).toThrow('Invalid event store');
82+
})).toThrow('Invalid store');
8383

8484
expect(() => getBatchEventProcessor({
8585
eventDispatcher: getMockEventDispatcher(),
8686
defaultFlushInterval: 10000,
8787
defaultBatchSize: 10,
8888
eventStore: 123 as any,
89-
})).toThrow('Invalid event store');
89+
})).toThrow('Invalid store');
9090

9191
expect(() => getBatchEventProcessor({
9292
eventDispatcher: getMockEventDispatcher(),

lib/event_processor/event_processor_factory.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@ import { ForwardingEventProcessor } from "./forwarding_event_processor";
2323
import { BatchEventProcessor, DEFAULT_MAX_BACKOFF, DEFAULT_MIN_BACKOFF, EventWithId, RetryConfig } from "./batch_event_processor";
2424
import { AsyncPrefixStore, Store, SyncPrefixStore } from "../utils/cache/store";
2525
import { Maybe } from "../utils/type";
26+
import { validateStore } from "../utils/cache/store_validator";
2627

2728
export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher';
2829

2930
export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000;
3031
export const EVENT_STORE_PREFIX = 'optly_event:';
3132

32-
export const INVALID_STORE = 'Invalid event store';
33-
export const INVALID_STORE_METHOD = 'Invalid store method %s';
34-
3533
export const getPrefixEventStore = (store: Store<string>): Store<EventWithId> => {
3634
if (store.operation === 'async') {
3735
return new AsyncPrefixStore<string, EventWithId>(
@@ -84,23 +82,6 @@ export const validateEventDispatcher = (eventDispatcher: EventDispatcher): void
8482
}
8583
}
8684

87-
const validateStore = (store: any) => {
88-
const errors = [];
89-
if (!store || typeof store !== 'object') {
90-
throw new Error(INVALID_STORE);
91-
}
92-
93-
for (const method of ['set', 'get', 'remove', 'getKeys']) {
94-
if (typeof store[method] !== 'function') {
95-
errors.push(INVALID_STORE_METHOD.replace('%s', method));
96-
}
97-
}
98-
99-
if (errors.length > 0) {
100-
throw new Error(errors.join(', '));
101-
}
102-
}
103-
10485
export const getBatchEventProcessor = (
10586
options: BatchEventProcessorFactoryOptions,
10687
EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor

lib/optimizely/index.tests.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { assert, expect } from 'chai';
1717
import sinon from 'sinon';
1818
import { sprintf } from '../utils/fns';
1919
import { NOTIFICATION_TYPES } from '../notification_center/type';
20-
import Optimizely from './';
20+
import Optimizely, { INVALID_ATTRIBUTES, INVALID_IDENTIFIER } from './';
2121
import OptimizelyUserContext from '../optimizely_user_context';
2222
import { OptimizelyDecideOption } from '../shared_types';
2323
import AudienceEvaluator from '../core/audience_evaluator';
@@ -4379,14 +4379,16 @@ describe('lib/optimizely', function() {
43794379
assert.deepEqual(userId, user.getUserId());
43804380
});
43814381

4382-
it('should return null OptimizelyUserContext when input userId is null', function() {
4383-
var user = optlyInstance.createUserContext(null);
4384-
assert.deepEqual(null, user);
4382+
it('should throw error when input userId is null', function() {
4383+
assert.throws(() => {
4384+
optlyInstance.createUserContext(null);
4385+
}, Error, INVALID_IDENTIFIER);
43854386
});
43864387

4387-
it('should return null OptimizelyUserContext when input userId is undefined', function() {
4388-
var user = optlyInstance.createUserContext(undefined);
4389-
assert.deepEqual(null, user);
4388+
it('should throw error when input userId is undefined', function() {
4389+
assert.throws(() => {
4390+
optlyInstance.createUserContext(undefined);
4391+
}, Error, INVALID_IDENTIFIER);
43904392
});
43914393

43924394
it('should create multiple instances of OptimizelyUserContext', function() {
@@ -4405,11 +4407,11 @@ describe('lib/optimizely', function() {
44054407
assert.deepEqual(user2.getUserId(), userId2);
44064408
});
44074409

4408-
it('should call the error handler for invalid user ID and return null', function() {
4410+
it('should call the error handler for invalid user ID and throw', function() {
44094411
const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({
44104412
datafileObj: testData.getTestDecideProjectConfig(),
44114413
});
4412-
assert.isNull(optlyInstance.createUserContext(1));
4414+
assert.throws(() => optlyInstance.createUserContext(1), Error, INVALID_IDENTIFIER);
44134415
sinon.assert.calledOnce(errorNotifier.notify);
44144416
// var errorMessage = errorHandler.handleError.lastCall.args[0].message;
44154417
// assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
@@ -4418,11 +4420,11 @@ describe('lib/optimizely', function() {
44184420
// assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
44194421
});
44204422

4421-
it('should call the error handler for invalid attributes and return null', function() {
4423+
it('should call the error handler for invalid attributes and throw', function() {
44224424
const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({
44234425
datafileObj: testData.getTestDecideProjectConfig(),
44244426
});
4425-
assert.isNull(optlyInstance.createUserContext('user1', 'invalid_attributes'));
4427+
assert.throws(() => optlyInstance.createUserContext('user1', 'invalid_attributes'), Error, INVALID_ATTRIBUTES);
44264428
sinon.assert.calledOnce(errorNotifier.notify);
44274429
// var errorMessage = errorHandler.handleError.lastCall.args[0].message;
44284430
// assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR'));

lib/optimizely/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ type DecisionReasons = (string | number)[];
114114

115115
export const INSTANCE_CLOSED = 'Instance closed';
116116
export const ONREADY_TIMEOUT = 'onReady timeout expired after %s ms';
117+
export const INVALID_IDENTIFIER = 'Invalid identifier';
118+
export const INVALID_ATTRIBUTES = 'Invalid attributes';
117119

118120
/**
119121
* options required to create optimizely object
@@ -1356,13 +1358,17 @@ export default class Optimizely extends BaseService implements Client {
13561358
* @param {string} userId (Optional) The user ID to be used for bucketing.
13571359
* @param {UserAttributes} attributes (Optional) user attributes.
13581360
* @return {OptimizelyUserContext|null} An OptimizelyUserContext associated with this OptimizelyClient or
1359-
* null if provided inputs are invalid
1361+
* throws if provided inputs are invalid
13601362
*/
1361-
createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext | null {
1363+
createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext {
13621364
const userIdentifier = userId ?? this.vuidManager?.getVuid();
13631365

1364-
if (userIdentifier === undefined || !this.validateInputs({ user_id: userIdentifier }, attributes)) {
1365-
return null;
1366+
if (userIdentifier === undefined || !this.validateInputs({ user_id: userIdentifier })) {
1367+
throw new Error(INVALID_IDENTIFIER);
1368+
}
1369+
1370+
if (!this.validateInputs({}, attributes)) {
1371+
throw new Error(INVALID_ATTRIBUTES);
13661372
}
13671373

13681374
const userContext = new OptimizelyUserContext({

lib/project_config/config_manager_factory.spec.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2024, Optimizely
2+
* Copyright 2024-2025, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -53,6 +53,52 @@ describe('getPollingConfigManager', () => {
5353
MockExponentialBackoff.mockClear();
5454
});
5555

56+
it('should throw an error if the passed cache is not valid', () => {
57+
expect(() => getPollingConfigManager({
58+
sdkKey: 'sdkKey',
59+
requestHandler: { makeRequest: vi.fn() },
60+
cache: 1 as any,
61+
})).toThrow('Invalid store');
62+
63+
expect(() => getPollingConfigManager({
64+
sdkKey: 'sdkKey',
65+
requestHandler: { makeRequest: vi.fn() },
66+
cache: 'abc' as any,
67+
})).toThrow('Invalid store');
68+
69+
expect(() => getPollingConfigManager({
70+
sdkKey: 'sdkKey',
71+
requestHandler: { makeRequest: vi.fn() },
72+
cache: {} as any,
73+
})).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys');
74+
75+
expect(() => getPollingConfigManager({
76+
sdkKey: 'sdkKey',
77+
requestHandler: { makeRequest: vi.fn() },
78+
cache: { set: 'abc', get: 'abc', remove: 'abc', getKeys: 'abc' } as any,
79+
})).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys');
80+
81+
const noop = () => {};
82+
83+
expect(() => getPollingConfigManager({
84+
sdkKey: 'sdkKey',
85+
requestHandler: { makeRequest: vi.fn() },
86+
cache: { set: noop, get: 'abc', remove: 'abc', getKeys: 'abc' } as any,
87+
})).toThrow('Invalid store method get, Invalid store method remove, Invalid store method getKeys');
88+
89+
expect(() => getPollingConfigManager({
90+
sdkKey: 'sdkKey',
91+
requestHandler: { makeRequest: vi.fn() },
92+
cache: { set: noop, get: noop, remove: 'abc', getKeys: 'abc' } as any,
93+
})).toThrow('Invalid store method remove, Invalid store method getKeys');
94+
95+
expect(() => getPollingConfigManager({
96+
sdkKey: 'sdkKey',
97+
requestHandler: { makeRequest: vi.fn() },
98+
cache: { set: noop, get: noop, remove: noop, getKeys: 'abc' } as any,
99+
})).toThrow('Invalid store method getKeys');
100+
});
101+
56102
it('uses a repeater with exponential backoff for the datafileManager', () => {
57103
const config = {
58104
sdkKey: 'sdkKey',

lib/project_config/config_manager_factory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { StartupLog } from "../service";
2525
import { MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './constant';
2626
import { LogLevel } from '../logging/logger'
2727
import { Store } from "../utils/cache/store";
28+
import { validateStore } from "../utils/cache/store_validator";
2829

2930
export const INVALID_CONFIG_MANAGER = "Invalid config manager";
3031

@@ -63,6 +64,10 @@ export type PollingConfigManagerFactoryOptions = PollingConfigManagerConfig & {
6364
export const getPollingConfigManager = (
6465
opt: PollingConfigManagerFactoryOptions
6566
): ProjectConfigManager => {
67+
if (opt.cache) {
68+
validateStore(opt.cache);
69+
}
70+
6671
const updateInterval = opt.updateInterval ?? DEFAULT_UPDATE_INTERVAL;
6772

6873
const backoff = new ExponentialBackoff(1000, updateInterval, 500);

lib/shared_types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export interface OptimizelyVariable {
293293
export interface Client {
294294
// TODO: In the future, will add a function to allow overriding the VUID.
295295
getVuid(): string | undefined;
296-
createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext | null;
296+
createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext;
297297
notificationCenter: NotificationCenter;
298298
activate(experimentKey: string, userId: string, attributes?: UserAttributes): string | null;
299299
track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void;

lib/utils/cache/store_validator.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
/**
3+
* Copyright 2025, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
export const INVALID_STORE = 'Invalid store';
18+
export const INVALID_STORE_METHOD = 'Invalid store method %s';
19+
20+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
21+
export const validateStore = (store: any): void => {
22+
const errors = [];
23+
if (!store || typeof store !== 'object') {
24+
throw new Error(INVALID_STORE);
25+
}
26+
27+
for (const method of ['set', 'get', 'remove', 'getKeys']) {
28+
if (typeof store[method] !== 'function') {
29+
errors.push(INVALID_STORE_METHOD.replace('%s', method));
30+
}
31+
}
32+
33+
if (errors.length > 0) {
34+
throw new Error(errors.join(', '));
35+
}
36+
}

vitest.config.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2024 Optimizely
2+
* Copyright 2024-2025, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)