Skip to content

Commit e954789

Browse files
Merge branch 'SDKS-8407_baseline' into breaking_changes_baseline
2 parents 2cbfe21 + 411ce8f commit e954789

34 files changed

+238
-283
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
2.0.0 (October XX, 2024)
22
- Added support for targeting rules based on large segments.
33
- Added `factory.destroy()` method, which invokes the `destroy` method on all SDK clients created by the factory.
4+
- Updated the handling of timers and async operations inside an `init` factory method to enable lazy initialization of the SDK in standalone mode. This update is intended for the React SDK.
45
- Bugfixing - Fixed an issue with the server-side polling manager that caused dangling timers when the SDK was destroyed before it was ready.
56
- BREAKING CHANGES:
67
- Updated default flag spec version to 1.2.

src/readiness/__tests__/readinessManager.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,14 @@ test('READINESS MANAGER / Segment updates should not be propagated', (done) => {
157157
});
158158
});
159159

160-
describe('READINESS MANAGER / Timeout ready event', () => {
160+
describe('READINESS MANAGER / Timeout event', () => {
161161
let readinessManager: IReadinessManager;
162162
let timeoutCounter: number;
163163

164164
beforeEach(() => {
165165
// Schedule timeout to be fired before SDK_READY
166166
readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);
167+
readinessManager.init(); // Start the timeout
167168
timeoutCounter = 0;
168169

169170
readinessManager.gate.on(SDK_READY_TIMED_OUT, () => {
@@ -212,6 +213,7 @@ test('READINESS MANAGER / Cancel timeout if ready fired', (done) => {
212213
let sdkReadyTimedoutCalled = false;
213214

214215
const readinessManager = readinessManagerFactory(EventEmitter, settingsWithTimeout);
216+
readinessManager.init(); // Start the timeout
215217

216218
readinessManager.gate.on(SDK_READY_TIMED_OUT, () => { sdkReadyTimedoutCalled = true; });
217219
readinessManager.gate.once(SDK_READY, () => { sdkReadyCalled = true; });

src/readiness/readinessManager.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ function splitsEventEmitterFactory(EventEmitter: new () => IEventEmitter): ISpli
77
const splitsEventEmitter = objectAssign(new EventEmitter(), {
88
splitsArrived: false,
99
splitsCacheLoaded: false,
10+
initialized: false,
11+
initCallbacks: []
1012
});
1113

1214
// `isSplitKill` condition avoids an edge-case of wrongly emitting SDK_READY if:
@@ -56,16 +58,17 @@ export function readinessManagerFactory(
5658
// emit SDK_READY_TIMED_OUT
5759
let hasTimedout = false;
5860

59-
function timeout() {
60-
if (hasTimedout) return;
61+
function timeout() { // eslint-disable-next-line no-use-before-define
62+
if (hasTimedout || isReady) return;
6163
hasTimedout = true;
6264
syncLastUpdate();
6365
gate.emit(SDK_READY_TIMED_OUT, 'Split SDK emitted SDK_READY_TIMED_OUT event.');
6466
}
6567

6668
let readyTimeoutId: ReturnType<typeof setTimeout>;
6769
if (readyTimeout > 0) {
68-
readyTimeoutId = setTimeout(timeout, readyTimeout);
70+
if (splits.initialized) readyTimeoutId = setTimeout(timeout, readyTimeout);
71+
else splits.initCallbacks.push(() => { readyTimeoutId = setTimeout(timeout, readyTimeout); });
6972
}
7073

7174
// emit SDK_READY and SDK_UPDATE
@@ -132,6 +135,12 @@ export function readinessManagerFactory(
132135
// tracking and evaluations, while keeping event listeners to emit SDK_READY_TIMED_OUT event
133136
setDestroyed() { isDestroyed = true; },
134137

138+
init() {
139+
if (splits.initialized) return;
140+
splits.initialized = true;
141+
splits.initCallbacks.forEach(cb => cb());
142+
},
143+
135144
destroy() {
136145
isDestroyed = true;
137146
syncLastUpdate();

src/readiness/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface ISplitsEventEmitter extends IEventEmitter {
1212
once(event: ISplitsEvent, listener: (...args: any[]) => void): this;
1313
splitsArrived: boolean
1414
splitsCacheLoaded: boolean
15+
initialized: boolean,
16+
initCallbacks: (() => void)[]
1517
}
1618

1719
/** Segments data emitter */
@@ -59,6 +61,7 @@ export interface IReadinessManager {
5961
timeout(): void,
6062
setDestroyed(): void,
6163
destroy(): void,
64+
init(): void,
6265

6366
/** for client-side */
6467
shared(): IReadinessManager,

src/sdkClient/__tests__/sdkClientMethodCS.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ const storageMock = {
1313
})
1414
};
1515

16-
const partialSdkReadinessManagers: { sdkStatus: jest.Mock, readinessManager: { destroy: jest.Mock } }[] = [];
16+
const partialSdkReadinessManagers: { sdkStatus: jest.Mock, readinessManager: { init: jest.Mock, destroy: jest.Mock } }[] = [];
1717

1818
const sdkReadinessManagerMock = {
1919
sdkStatus: jest.fn(),
20-
readinessManager: { destroy: jest.fn() },
20+
readinessManager: { init: jest.fn(), destroy: jest.fn() },
2121
shared: jest.fn(() => {
2222
partialSdkReadinessManagers.push({
2323
sdkStatus: jest.fn(),
24-
readinessManager: { destroy: jest.fn() },
24+
readinessManager: { init: jest.fn(), destroy: jest.fn() },
2525
});
2626
return partialSdkReadinessManagers[partialSdkReadinessManagers.length - 1];
2727
})
@@ -45,7 +45,7 @@ const params = {
4545
signalListener: { stop: jest.fn() },
4646
settings: settingsWithKey,
4747
telemetryTracker: telemetryTrackerFactory(),
48-
clients: {}
48+
clients: {},
4949
};
5050

5151
const invalidAttributes = [

src/sdkClient/sdkClientMethodCS.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl
7575
validKey
7676
);
7777

78-
sharedSyncManager && sharedSyncManager.start();
79-
8078
log.info(NEW_SHARED_CLIENT);
8179
} else {
8280
log.debug(RETRIEVE_CLIENT_EXISTING);

src/sdkFactory/index.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
2323
const { settings, platform, storageFactory, splitApiFactory, extraProps,
2424
syncManagerFactory, SignalListener, impressionsObserverFactory,
2525
integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory,
26-
filterAdapterFactory } = params;
26+
filterAdapterFactory, lazyInit } = params;
2727
const { log, sync: { impressionsMode } } = settings;
2828

2929
// @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid SDK Key, etc.
3030
// On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization.
3131

32-
// We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
33-
validateAndTrackApiKey(log, settings.core.authorizationKey);
32+
// initialization
33+
let hasInit = false;
34+
const initCallbacks: (() => void)[] = [];
35+
36+
function whenInit(cb: () => void) {
37+
if (hasInit) cb();
38+
else initCallbacks.push(cb);
39+
}
3440

3541
const sdkReadinessManager = sdkReadinessManagerFactory(platform.EventEmitter, settings);
3642
const readiness = sdkReadinessManager.readinessManager;
@@ -67,8 +73,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
6773
strategy = strategyDebugFactory(observer);
6874
}
6975

70-
const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, integrationsManager, storage.telemetry);
71-
const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry);
76+
const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, whenInit, integrationsManager, storage.telemetry);
77+
const eventTracker = eventTrackerFactory(settings, storage.events, whenInit, integrationsManager, storage.telemetry);
7278

7379
// splitApi is used by SyncManager and Browser signal listener
7480
const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
@@ -85,8 +91,21 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
8591
const clientMethod = sdkClientMethodFactory(ctx);
8692
const managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
8793

88-
syncManager && syncManager.start();
89-
signalListener && signalListener.start();
94+
95+
function init() {
96+
if (hasInit) return;
97+
hasInit = true;
98+
99+
// We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
100+
validateAndTrackApiKey(log, settings.core.authorizationKey);
101+
readiness.init();
102+
uniqueKeysTracker && uniqueKeysTracker.start();
103+
syncManager && syncManager.start();
104+
signalListener && signalListener.start();
105+
106+
initCallbacks.forEach((cb) => cb());
107+
initCallbacks.length = 0;
108+
}
90109

91110
log.info(NEW_FACTORY);
92111

@@ -107,7 +126,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
107126
settings,
108127

109128
destroy() {
110-
return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => {});
129+
return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => { });
111130
}
112-
}, extraProps && extraProps(ctx));
131+
}, extraProps && extraProps(ctx), lazyInit ? { init } : init());
113132
}

src/sdkFactory/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export interface ISdkFactoryContextAsync extends ISdkFactoryContext {
6868
* Object parameter with the modules required to create an SDK factory instance
6969
*/
7070
export interface ISdkFactoryParams {
71+
// If true, the `sdkFactory` is pure (no side effects), and the SDK instance includes a `init` method to run initialization side effects
72+
lazyInit?: boolean,
7173

7274
// The settings must be already validated
7375
settings: ISettings,

src/storages/AbstractSegmentsCacheSync.ts renamed to src/storages/AbstractMySegmentsCacheSync.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
/* eslint-disable no-unused-vars */
31
import { IMySegmentsResponse } from '../dtos/types';
42
import { MySegmentsData } from '../sync/polling/types';
53
import { ISegmentsCacheSync } from './types';
@@ -8,18 +6,11 @@ import { ISegmentsCacheSync } from './types';
86
* This class provides a skeletal implementation of the ISegmentsCacheSync interface
97
* to minimize the effort required to implement this interface.
108
*/
11-
export abstract class AbstractSegmentsCacheSync implements ISegmentsCacheSync {
12-
/**
13-
* For server-side synchronizer: add `segmentKeys` list of keys to `name` segment.
14-
* For client-side synchronizer: add `name` segment to the cache. `segmentKeys` is undefined.
15-
*/
16-
abstract addToSegment(name: string, segmentKeys?: string[]): boolean
9+
export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync {
1710

18-
/**
19-
* For server-side synchronizer: remove `segmentKeys` list of keys from `name` segment.
20-
* For client-side synchronizer: remove `name` segment from the cache. `segmentKeys` is undefined.
21-
*/
22-
abstract removeFromSegment(name: string, segmentKeys?: string[]): boolean
11+
protected abstract addSegment(name: string): boolean
12+
protected abstract removeSegment(name: string): boolean
13+
protected abstract setChangeNumber(changeNumber?: number): boolean | void
2314

2415
/**
2516
* For server-side synchronizer: check if `key` is in `name` segment.
@@ -34,11 +25,10 @@ export abstract class AbstractSegmentsCacheSync implements ISegmentsCacheSync {
3425
this.resetSegments({});
3526
}
3627

37-
/**
38-
* For server-side synchronizer: add the given list of segments to the cache, with an empty list of keys. The segments that already exist are not modified.
39-
* For client-side synchronizer: the method is not used.
40-
*/
41-
registerSegments(names: string[]): boolean { return false; }
28+
29+
// No-op. Not used in client-side.
30+
registerSegments(): boolean { return false; }
31+
update() { return false; }
4232

4333
/**
4434
* For server-side synchronizer: get the list of segments to fetch changes.
@@ -52,31 +42,26 @@ export abstract class AbstractSegmentsCacheSync implements ISegmentsCacheSync {
5242
*/
5343
abstract getKeysCount(): number
5444

55-
/**
56-
* For server-side synchronizer: change number of `name` segment.
57-
* For client-side synchronizer: change number of mySegments.
58-
*/
59-
abstract setChangeNumber(name?: string, changeNumber?: number): boolean | void
6045
abstract getChangeNumber(name: string): number
6146

6247
/**
6348
* For server-side synchronizer: the method is not used.
6449
* For client-side synchronizer: it resets or updates the cache.
6550
*/
6651
resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean {
67-
this.setChangeNumber(undefined, segmentsData.cn);
52+
this.setChangeNumber(segmentsData.cn);
6853

6954
const { added, removed } = segmentsData as MySegmentsData;
7055

7156
if (added && removed) {
7257
let isDiff = false;
7358

7459
added.forEach(segment => {
75-
isDiff = this.addToSegment(segment) || isDiff;
60+
isDiff = this.addSegment(segment) || isDiff;
7661
});
7762

7863
removed.forEach(segment => {
79-
isDiff = this.removeFromSegment(segment) || isDiff;
64+
isDiff = this.removeSegment(segment) || isDiff;
8065
});
8166

8267
return isDiff;
@@ -97,11 +82,11 @@ export abstract class AbstractSegmentsCacheSync implements ISegmentsCacheSync {
9782

9883
// Slowest path => add and/or remove segments
9984
for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
100-
this.removeFromSegment(storedSegmentKeys[removeIndex]);
85+
this.removeSegment(storedSegmentKeys[removeIndex]);
10186
}
10287

10388
for (let addIndex = index; addIndex < names.length; addIndex++) {
104-
this.addToSegment(names[addIndex]);
89+
this.addSegment(names[addIndex]);
10590
}
10691

10792
return true;

src/storages/inLocalStorage/MySegmentsCacheInLocal.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ILogger } from '../../logger/types';
22
import { isNaNNumber } from '../../utils/lang';
3-
import { AbstractSegmentsCacheSync } from '../AbstractSegmentsCacheSync';
3+
import { AbstractMySegmentsCacheSync } from '../AbstractMySegmentsCacheSync';
44
import type { MySegmentsKeyBuilder } from '../KeyBuilderCS';
55
import { LOG_PREFIX, DEFINED } from './constants';
66

7-
export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync {
7+
export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
88

99
private readonly keys: MySegmentsKeyBuilder;
1010
private readonly log: ILogger;
@@ -16,7 +16,7 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync {
1616
// There is not need to flush segments cache like splits cache, since resetSegments receives the up-to-date list of active segments
1717
}
1818

19-
addToSegment(name: string): boolean {
19+
protected addSegment(name: string): boolean {
2020
const segmentKey = this.keys.buildSegmentNameKey(name);
2121

2222
try {
@@ -29,7 +29,7 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync {
2929
}
3030
}
3131

32-
removeFromSegment(name: string): boolean {
32+
protected removeSegment(name: string): boolean {
3333
const segmentKey = this.keys.buildSegmentNameKey(name);
3434

3535
try {
@@ -61,7 +61,7 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync {
6161
return 1;
6262
}
6363

64-
setChangeNumber(name?: string, changeNumber?: number) {
64+
protected setChangeNumber(changeNumber?: number) {
6565
try {
6666
if (changeNumber) localStorage.setItem(this.keys.buildTillKey(), changeNumber + '');
6767
else localStorage.removeItem(this.keys.buildTillKey());

0 commit comments

Comments
 (0)