From 5da17c4ee87d192c641ac3e268dfb5ec2d6844a5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 19 Nov 2024 10:44:42 -0300 Subject: [PATCH 1/3] Updated the factory 'init' and 'destroy' methods to support re-initialization after destruction --- CHANGES.txt | 3 +++ package-lock.json | 4 +-- package.json | 2 +- src/readiness/readinessManager.ts | 31 +++++++++++----------- src/sdkClient/sdkClient.ts | 10 +++---- src/sdkFactory/index.ts | 1 + src/storages/inLocalStorage/index.ts | 20 +++----------- src/storages/inMemory/InMemoryStorage.ts | 10 +------ src/storages/inMemory/InMemoryStorageCS.ts | 18 ++----------- src/sync/syncTask.ts | 4 +-- 10 files changed, 33 insertions(+), 70 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1cfb08c8..b40852aa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.0.1 (XXX XX, 2024) + - Updated the factory `init` and `destroy` methods to support re-initialization after destruction, enabling repeated cycles of initialization and cleanup. This update ensures compatibility of the React SDK with React Strict Mode, where the factory's `init` and `destroy` effects are executed an extra time to validate proper resource cleanup. + 2.0.0 (November 1, 2024) - Added support for targeting rules based on large segments. - Added `factory.destroy()` method, which invokes the `destroy` method on all SDK clients created by the factory. diff --git a/package-lock.json b/package-lock.json index 40fecca7..4af6eb3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.0", + "version": "2.0.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.0.0", + "version": "2.0.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 608c3982..4cffdd54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.0", + "version": "2.0.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index e8f92dce..6f46474d 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -37,7 +37,9 @@ function segmentsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitt export function readinessManagerFactory( EventEmitter: new () => SplitIO.IEventEmitter, settings: ISettings, - splits: ISplitsEventEmitter = splitsEventEmitterFactory(EventEmitter)): IReadinessManager { + splits: ISplitsEventEmitter = splitsEventEmitterFactory(EventEmitter), + isShared?: boolean +): IReadinessManager { const readyTimeout = settings.startup.readyTimeout; @@ -66,11 +68,6 @@ export function readinessManagerFactory( gate.emit(SDK_READY_TIMED_OUT, 'Split SDK emitted SDK_READY_TIMED_OUT event.'); } - let readyTimeoutId: ReturnType; - if (readyTimeout > 0) { - if (splits.hasInit) readyTimeoutId = setTimeout(timeout, readyTimeout); - else splits.initCallbacks.push(() => { readyTimeoutId = setTimeout(timeout, readyTimeout); }); - } // emit SDK_READY and SDK_UPDATE let isReady = false; @@ -78,11 +75,19 @@ export function readinessManagerFactory( segments.on(SDK_SEGMENTS_ARRIVED, checkIsReadyOrUpdate); let isDestroyed = false; + let readyTimeoutId: ReturnType; + function __init() { + isDestroyed = false; + if (readyTimeout > 0 && !isReady) readyTimeoutId = setTimeout(timeout, readyTimeout); + } + + splits.initCallbacks.push(__init); + if (splits.hasInit) __init(); function checkIsReadyFromCache() { isReadyFromCache = true; // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted - if (!isReady) { + if (!isReady && !isDestroyed) { try { syncLastUpdate(); gate.emit(SDK_READY_FROM_CACHE); @@ -94,6 +99,7 @@ export function readinessManagerFactory( } function checkIsReadyOrUpdate(diff: any) { + if (isDestroyed) return; if (isReady) { try { syncLastUpdate(); @@ -117,16 +123,13 @@ export function readinessManagerFactory( } } - let refCount = 1; - return { splits, segments, gate, shared() { - refCount++; - return readinessManagerFactory(EventEmitter, settings, splits); + return readinessManagerFactory(EventEmitter, settings, splits, true); }, // @TODO review/remove next methods when non-recoverable errors are reworked @@ -145,13 +148,9 @@ export function readinessManagerFactory( destroy() { isDestroyed = true; syncLastUpdate(); - - segments.removeAllListeners(); - gate.removeAllListeners(); clearTimeout(readyTimeoutId); - if (refCount > 0) refCount--; - if (refCount === 0) splits.removeAllListeners(); + if (!isShared) splits.hasInit = false; }, isReady() { return isReady; }, diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index fbc4aeb9..cda5ba2e 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -56,22 +56,18 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo // Mark the SDK as destroyed immediately sdkReadinessManager.readinessManager.destroy(); - // For main client, release the SDK Key and record stat before flushing data + // For main client, cleanup the SDK Key, listeners and scheduled jobs, and record stat before flushing data if (!isSharedClient) { releaseApiKey(settings.core.authorizationKey); telemetryTracker.sessionLength(); + signalListener && signalListener.stop(); + uniqueKeysTracker && uniqueKeysTracker.stop(); } // Stop background jobs syncManager && syncManager.stop(); return __flush().then(() => { - // For main client, cleanup event listeners and scheduled jobs - if (!isSharedClient) { - signalListener && signalListener.stop(); - uniqueKeysTracker && uniqueKeysTracker.stop(); - } - // Cleanup storage return storage.destroy(); }); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 572b6bec..680b8f7f 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -126,6 +126,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA settings, destroy() { + hasInit = false; return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => { }); } }, extraProps && extraProps(ctx), lazyInit ? { init } : init()); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 93e735e8..871be592 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -7,8 +7,6 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import { SplitsCacheInLocal } from './SplitsCacheInLocal'; import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; -import { MySegmentsCacheInMemory } from '../inMemory/MySegmentsCacheInMemory'; -import { SplitsCacheInMemory } from '../inMemory/SplitsCacheInMemory'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; @@ -36,7 +34,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn return InMemoryStorageCSFactory(params); } - const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params; + const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params; const matchingKey = getMatching(settings.core.key); const keys = new KeyBuilderCS(prefix, matchingKey); const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; @@ -55,15 +53,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, - destroy() { - this.splits = new SplitsCacheInMemory(__splitFiltersValidation); - this.segments = new MySegmentsCacheInMemory(); - this.largeSegments = new MySegmentsCacheInMemory(); - this.impressions.clear(); - this.impressionCounts && this.impressionCounts.clear(); - this.events.clear(); - this.uniqueKeys?.clear(); - }, + destroy() { }, // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key). shared(matchingKey: string) { @@ -77,11 +67,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn events: this.events, telemetry: this.telemetry, - destroy() { - this.splits = new SplitsCacheInMemory(__splitFiltersValidation); - this.segments = new MySegmentsCacheInMemory(); - this.largeSegments = new MySegmentsCacheInMemory(); - } + destroy() { } }; }, }; diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index e91ce8c6..565d37ba 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -28,15 +28,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemory() : undefined, - // When using MEMORY we should clean all the caches to leave them empty - destroy() { - this.splits.clear(); - this.segments.clear(); - this.impressions.clear(); - this.impressionCounts && this.impressionCounts.clear(); - this.events.clear(); - this.uniqueKeys && this.uniqueKeys.clear(); - } + destroy() { } }; // @TODO revisit storage logic in localhost mode diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index dd4262c2..051bd9d6 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -30,16 +30,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined, uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, - // When using MEMORY we should clean all the caches to leave them empty - destroy() { - this.splits.clear(); - this.segments.clear(); - this.largeSegments.clear(); - this.impressions.clear(); - this.impressionCounts && this.impressionCounts.clear(); - this.events.clear(); - this.uniqueKeys && this.uniqueKeys.clear(); - }, + destroy() { }, // When using shared instantiation with MEMORY we reuse everything but segments (they are unique per key) shared() { @@ -52,12 +43,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag events: this.events, telemetry: this.telemetry, - // Set a new splits cache to clean it for the client without affecting other clients - destroy() { - this.splits = new SplitsCacheInMemory(__splitFiltersValidation); - this.segments.clear(); - this.largeSegments.clear(); - } + destroy() { } }; }, }; diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index 5f22f6c0..7820c060 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -68,8 +68,8 @@ export function syncTaskFactory(log: ILogger, }, stop() { - running = false; - if (timeoutID) { + if (running) { + running = false; log.debug(SYNC_TASK_STOP, [taskName]); clearTimeout(timeoutID); timeoutID = undefined; From 9afd29f19695188cf37292a365986eb5de6f5cbc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 27 Nov 2024 16:07:21 -0300 Subject: [PATCH 2/3] Fix issue with invalid characters in SplitSDKMachineName header --- CHANGES.txt | 3 +++ src/services/__tests__/decorateHeaders.spec.ts | 9 ++++++++- src/services/decorateHeaders.ts | 5 +++++ src/services/splitHttpClient.ts | 4 ++-- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4a335cf8..84820147 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.0.2 (XXX XX, 2024) + - Bugfixing - Sanitize the `SplitSDKMachineName` header value to avoid exceptions on HTTP/S requests when it contains non ISO-8859-1 characters (Related to issue https://github.com/splitio/javascript-client/issues/847). + 2.0.1 (November 25, 2024) - Bugfixing - Fixed an issue with the SDK_UPDATE event on server-side, where it was not being emitted if there was an empty segment and the SDK received a feature flag update notification. diff --git a/src/services/__tests__/decorateHeaders.spec.ts b/src/services/__tests__/decorateHeaders.spec.ts index c50d6620..e6cfb481 100644 --- a/src/services/__tests__/decorateHeaders.spec.ts +++ b/src/services/__tests__/decorateHeaders.spec.ts @@ -1,5 +1,5 @@ import { ISettings } from '../../types'; -import { decorateHeaders } from '../decorateHeaders'; +import { decorateHeaders, removeNonISO88591 } from '../decorateHeaders'; const HEADERS = { Authorization: 'Bearer SDK-KEY', @@ -48,3 +48,10 @@ describe('decorateHeaders', () => { expect(settings.log.error).toHaveBeenCalledWith('Problem adding custom headers to request decorator: Error: Unexpected error'); }); }); + +test('removeNonISO88591', () => { + expect(removeNonISO88591('')).toBe(''); + expect(removeNonISO88591('This is a test')).toBe('This is a test'); + expect(removeNonISO88591('This is a test ó \u0FFF 你')).toBe('This is a test ó '); + expect(removeNonISO88591('Emiliano’s-MacBook-Pro')).toBe('Emilianos-MacBook-Pro'); +}); diff --git a/src/services/decorateHeaders.ts b/src/services/decorateHeaders.ts index 4a95219f..1a4dac05 100644 --- a/src/services/decorateHeaders.ts +++ b/src/services/decorateHeaders.ts @@ -30,3 +30,8 @@ export function decorateHeaders(settings: ISettings, headers: Record void = () => { }, logErrorsAsInfo: boolean = false): Promise { From 603680d2135a58cac6bb408d1b958acec88ed9a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 3 Dec 2024 15:16:13 -0300 Subject: [PATCH 3/3] Prepare stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b76f5643..02a7bd61 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.0.2 (XXX XX, 2024) +2.0.2 (December 3, 2024) - Updated the factory `init` and `destroy` methods to support re-initialization after destruction. This update ensures compatibility of the React SDK with React Strict Mode, where the factory's `init` and `destroy` effects are executed an extra time to validate proper resource cleanup. - Bugfixing - Sanitize the `SplitSDKMachineName` header value to avoid exceptions on HTTP/S requests when it contains non ISO-8859-1 characters (Related to issue https://github.com/splitio/javascript-client/issues/847). diff --git a/package-lock.json b/package-lock.json index e12cd89b..971d1486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1-rc.0", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1-rc.0", + "version": "2.0.2", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 4cffdd54..936c23bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1-rc.0", + "version": "2.0.2", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js",