diff --git a/.circleci/config.yml b/.circleci/config.yml index cecd712..0545c8a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: cimg/node:12.22 + - image: cimg/node:22.2.0 steps: - checkout diff --git a/src/InspectorManager.js b/src/InspectorManager.js index 33a6aad..ce8e702 100644 --- a/src/InspectorManager.js +++ b/src/InspectorManager.js @@ -22,6 +22,9 @@ function InspectorManager(inspectors, logger) { /** * Collection of inspectors keyed by type. + * + * Inspectors are async by default. + * * @type {{[type: string]: object[]}} */ const inspectorsByType = { @@ -30,14 +33,30 @@ function InspectorManager(inspectors, logger) { [InspectorTypes.flagDetailChanged]: [], [InspectorTypes.clientIdentityChanged]: [], }; + /** + * Collection synchronous of inspectors keyed by type. + * + * @type {{[type: string]: object[]}} + */ + const synchronousInspectorsByType = { + [InspectorTypes.flagUsed]: [], + [InspectorTypes.flagDetailsChanged]: [], + [InspectorTypes.flagDetailChanged]: [], + [InspectorTypes.clientIdentityChanged]: [], + }; const safeInspectors = inspectors && inspectors.map(inspector => SafeInspector(inspector, logger)); safeInspectors && safeInspectors.forEach(safeInspector => { // Only add inspectors of supported types. - if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type)) { + if (Object.prototype.hasOwnProperty.call(inspectorsByType, safeInspector.type) && !safeInspector.synchronous) { inspectorsByType[safeInspector.type].push(safeInspector); + } else if ( + Object.prototype.hasOwnProperty.call(synchronousInspectorsByType, safeInspector.type) && + safeInspector.synchronous + ) { + synchronousInspectorsByType[safeInspector.type].push(safeInspector); } else { logger.warn(messages.invalidInspector(safeInspector.type, safeInspector.name)); } @@ -49,7 +68,9 @@ function InspectorManager(inspectors, logger) { * @param {string} type The type of the inspector to check. * @returns True if there are any inspectors of that type registered. */ - manager.hasListeners = type => inspectorsByType[type] && inspectorsByType[type].length; + manager.hasListeners = type => + (inspectorsByType[type] && inspectorsByType[type].length) || + (synchronousInspectorsByType[type] && synchronousInspectorsByType[type].length); /** * Notify registered inspectors of a flag being used. @@ -61,9 +82,13 @@ function InspectorManager(inspectors, logger) { * @param {Object} context The LDContext for the flag. */ manager.onFlagUsed = (flagKey, detail, context) => { - if (inspectorsByType[InspectorTypes.flagUsed].length) { + const type = InspectorTypes.flagUsed; + if (synchronousInspectorsByType[type].length) { + synchronousInspectorsByType[type].forEach(inspector => inspector.method(flagKey, detail, context)); + } + if (inspectorsByType[type].length) { onNextTick(() => { - inspectorsByType[InspectorTypes.flagUsed].forEach(inspector => inspector.method(flagKey, detail, context)); + inspectorsByType[type].forEach(inspector => inspector.method(flagKey, detail, context)); }); } }; @@ -76,9 +101,13 @@ function InspectorManager(inspectors, logger) { * @param {Record} flags The current flags as a Record. */ manager.onFlags = flags => { - if (inspectorsByType[InspectorTypes.flagDetailsChanged].length) { + const type = InspectorTypes.flagDetailsChanged; + if (synchronousInspectorsByType[type].length) { + synchronousInspectorsByType[type].forEach(inspector => inspector.method(flags)); + } + if (inspectorsByType[type].length) { onNextTick(() => { - inspectorsByType[InspectorTypes.flagDetailsChanged].forEach(inspector => inspector.method(flags)); + inspectorsByType[type].forEach(inspector => inspector.method(flags)); }); } }; @@ -92,9 +121,13 @@ function InspectorManager(inspectors, logger) { * @param {Object} flag An `LDEvaluationDetail` for the flag. */ manager.onFlagChanged = (flagKey, flag) => { - if (inspectorsByType[InspectorTypes.flagDetailChanged].length) { + const type = InspectorTypes.flagDetailChanged; + if (synchronousInspectorsByType[type].length) { + synchronousInspectorsByType[type].forEach(inspector => inspector.method(flagKey, flag)); + } + if (inspectorsByType[type].length) { onNextTick(() => { - inspectorsByType[InspectorTypes.flagDetailChanged].forEach(inspector => inspector.method(flagKey, flag)); + inspectorsByType[type].forEach(inspector => inspector.method(flagKey, flag)); }); } }; @@ -107,9 +140,13 @@ function InspectorManager(inspectors, logger) { * @param {Object} context The `LDContext` which is now identified. */ manager.onIdentityChanged = context => { - if (inspectorsByType[InspectorTypes.clientIdentityChanged].length) { + const type = InspectorTypes.clientIdentityChanged; + if (synchronousInspectorsByType[type].length) { + synchronousInspectorsByType[type].forEach(inspector => inspector.method(context)); + } + if (inspectorsByType[type].length) { onNextTick(() => { - inspectorsByType[InspectorTypes.clientIdentityChanged].forEach(inspector => inspector.method(context)); + inspectorsByType[type].forEach(inspector => inspector.method(context)); }); } }; diff --git a/src/SafeInspector.js b/src/SafeInspector.js index 59274bd..e172e3d 100644 --- a/src/SafeInspector.js +++ b/src/SafeInspector.js @@ -9,6 +9,7 @@ function SafeInspector(inspector, logger) { const wrapper = { type: inspector.type, name: inspector.name, + synchronous: inspector.synchronous, }; wrapper.method = (...args) => { diff --git a/src/__tests__/InspectorManager-test.js b/src/__tests__/InspectorManager-test.js index 3f37acf..d381f6f 100644 --- a/src/__tests__/InspectorManager-test.js +++ b/src/__tests__/InspectorManager-test.js @@ -28,7 +28,7 @@ describe('given an inspector manager with no registered inspectors', () => { }); }); -describe('given an inspector with callbacks of every type', () => { +describe.each([true, false])('given an inspector with callbacks of every type: synchronous: %p', synchronous => { /** * @type {AsyncQueue} */ @@ -39,6 +39,7 @@ describe('given an inspector with callbacks of every type', () => { { type: 'flag-used', name: 'my-flag-used-inspector', + synchronous, method: (flagKey, flagDetail, context) => { eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context }); }, @@ -47,6 +48,7 @@ describe('given an inspector with callbacks of every type', () => { { type: 'flag-used', name: 'my-other-flag-used-inspector', + synchronous, method: (flagKey, flagDetail, context) => { eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context }); }, @@ -54,6 +56,7 @@ describe('given an inspector with callbacks of every type', () => { { type: 'flag-details-changed', name: 'my-flag-details-inspector', + synchronous, method: details => { eventQueue.add({ type: 'flag-details-changed', @@ -64,6 +67,7 @@ describe('given an inspector with callbacks of every type', () => { { type: 'flag-detail-changed', name: 'my-flag-detail-inspector', + synchronous, method: (flagKey, flagDetail) => { eventQueue.add({ type: 'flag-detail-changed', @@ -75,6 +79,7 @@ describe('given an inspector with callbacks of every type', () => { { type: 'client-identity-changed', name: 'my-identity-inspector', + synchronous, method: context => { eventQueue.add({ type: 'client-identity-changed', @@ -85,6 +90,7 @@ describe('given an inspector with callbacks of every type', () => { // Invalid inspector shouldn't have an effect. { type: 'potato', + synchronous, name: 'my-potato-inspector', method: () => {}, }, diff --git a/src/__tests__/LDClient-inspectors-test.js b/src/__tests__/LDClient-inspectors-test.js index 25db828..cc1897c 100644 --- a/src/__tests__/LDClient-inspectors-test.js +++ b/src/__tests__/LDClient-inspectors-test.js @@ -5,12 +5,13 @@ const stubPlatform = require('./stubPlatform'); const envName = 'UNKNOWN_ENVIRONMENT_ID'; const context = { key: 'context-key' }; -describe('given a streaming client with registered inspectors', () => { +describe.each([true, false])('given a streaming client with registered inspectors, synchronous: %p', synchronous => { const eventQueue = new AsyncQueue(); const inspectors = [ { type: 'flag-used', + synchronous, method: (flagKey, flagDetail, context) => { eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context }); }, @@ -18,12 +19,14 @@ describe('given a streaming client with registered inspectors', () => { // 'flag-used registered twice. { type: 'flag-used', + synchronous, method: (flagKey, flagDetail, context) => { eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context }); }, }, { type: 'flag-details-changed', + synchronous, method: details => { eventQueue.add({ type: 'flag-details-changed', @@ -33,6 +36,7 @@ describe('given a streaming client with registered inspectors', () => { }, { type: 'flag-detail-changed', + synchronous, method: (flagKey, flagDetail) => { eventQueue.add({ type: 'flag-detail-changed', @@ -43,6 +47,7 @@ describe('given a streaming client with registered inspectors', () => { }, { type: 'client-identity-changed', + synchronous, method: context => { eventQueue.add({ type: 'client-identity-changed', diff --git a/src/index.js b/src/index.js index 8698405..df85745 100644 --- a/src/index.js +++ b/src/index.js @@ -462,8 +462,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } else { mods[data.key] = { current: newDetail }; } - handleFlagChanges(mods); // don't wait for this Promise to be resolved notifyInspectionFlagChanged(data, newFlag); + handleFlagChanges(mods); // don't wait for this Promise to be resolved } else { logger.debug(messages.debugStreamPatchIgnored(data.key)); } diff --git a/typings.d.ts b/typings.d.ts index 9d40a24..bd698f1 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -1013,6 +1013,13 @@ declare module 'launchdarkly-js-sdk-common' { */ name: string; + /** + * If `true`, then the inspector will be ran synchronously with evaluation. + * Synchronous inspectors execute inline with evaluation and care should be taken to ensure + * they have minimal performance overhead. + */ + synchronous?: boolean, + /** * This method is called when a flag is accessed via a variation method, or it can be called based on actions in * wrapper SDKs which have different methods of tracking when a flag was accessed. It is not called when a call is made @@ -1040,6 +1047,11 @@ declare module 'launchdarkly-js-sdk-common' { */ name: string; + /** + * If `true`, then the inspector will be ran synchronously with flag updates. + */ + synchronous?: boolean, + /** * This method is called when the flags in the store are replaced with new flags. It will contain all flags * regardless of if they have been evaluated. @@ -1065,6 +1077,11 @@ declare module 'launchdarkly-js-sdk-common' { */ name: string; + /** + * If `true`, then the inspector will be ran synchronously with flag updates. + */ + synchronous?: boolean, + /** * This method is called when a flag is updated. It will not be called * when all flags are updated. @@ -1088,6 +1105,11 @@ declare module 'launchdarkly-js-sdk-common' { */ name: string; + /** + * If `true`, then the inspector will be ran synchronously with identification. + */ + synchronous?: boolean, + /** * This method will be called when an identify operation completes. */