Skip to content

Commit

Permalink
feat: Allow for synchronous inspectors. (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
kinyoklion authored Jun 18, 2024
1 parent 98a1733 commit 7e490f4
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: cimg/node:12.22
- image: cimg/node:22.2.0
steps:
- checkout

Expand Down
57 changes: 47 additions & 10 deletions src/InspectorManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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));
}
Expand All @@ -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.
Expand All @@ -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));
});
}
};
Expand All @@ -76,9 +101,13 @@ function InspectorManager(inspectors, logger) {
* @param {Record<string, Object>} flags The current flags as a Record<string, LDEvaluationDetail>.
*/
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));
});
}
};
Expand All @@ -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));
});
}
};
Expand All @@ -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));
});
}
};
Expand Down
1 change: 1 addition & 0 deletions src/SafeInspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function SafeInspector(inspector, logger) {
const wrapper = {
type: inspector.type,
name: inspector.name,
synchronous: inspector.synchronous,
};

wrapper.method = (...args) => {
Expand Down
8 changes: 7 additions & 1 deletion src/__tests__/InspectorManager-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand All @@ -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 });
},
Expand All @@ -47,13 +48,15 @@ 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 });
},
},
{
type: 'flag-details-changed',
name: 'my-flag-details-inspector',
synchronous,
method: details => {
eventQueue.add({
type: 'flag-details-changed',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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: () => {},
},
Expand Down
7 changes: 6 additions & 1 deletion src/__tests__/LDClient-inspectors-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@ 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 });
},
},
// '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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
22 changes: 22 additions & 0 deletions typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
*/
Expand Down

0 comments on commit 7e490f4

Please sign in to comment.