diff --git a/CHANGES.txt b/CHANGES.txt index 4a335cf8..02a7bd61 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +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). + 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/package-lock.json b/package-lock.json index 65555d17..971d1486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1", + "version": "2.0.2", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index fdf8c86d..936c23bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.1", + "version": "2.0.2", "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/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/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 { 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() { } }; }, };