From 97ea230cd95bb86b5714fe9ed958a95edb475a4a Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Tue, 19 Aug 2025 13:02:11 +0100 Subject: [PATCH 01/15] feat: Add debug override capabilities for plugins This update introduces a new interface for plugins to register debug capabilities, allowing for temporary flag value overrides during local development. --- src/index.js | 31 +++++++++++- src/plugins.js | 17 +++++++ typings.d.ts | 129 +++++++++++++++++++++++++++++++------------------ 3 files changed, 128 insertions(+), 49 deletions(-) diff --git a/src/index.js b/src/index.js index 86d6752..99b9823 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,12 @@ const { checkContext, getContextKeys } = require('./context'); const { InspectorTypes, InspectorManager } = require('./InspectorManager'); const timedPromise = require('./timedPromise'); const createHookRunner = require('./HookRunner'); -const { getPluginHooks, registerPlugins, createPluginEnvironment } = require('./plugins'); +const { + getPluginHooks, + registerPlugins, + registerPluginsForDebugOverride, + createPluginEnvironment, +} = require('./plugins'); const changeEvent = 'change'; const internalChangeEvent = 'internal-change'; const highTimeoutThreshold = 5; @@ -880,6 +885,30 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { registerPlugins(logger, pluginEnvironment, client, plugins); + function setOverride(key, value) { + const data = { key, value }; + const mods = {}; + const oldFlag = flags[data.key]; + const newFlag = utils.extend({}, data); + delete newFlag['key']; + flags[data.key] = newFlag; + const newDetail = getFlagDetail(newFlag); + if (oldFlag) { + mods[data.key] = { previous: oldFlag.value, current: newDetail }; + } else { + mods[data.key] = { current: newDetail }; + } + notifyInspectionFlagChanged(data, newFlag); + handleFlagChanges(mods); // don't wait for this Promise to be resolved + } + + const debugOverride = { + setOverride: setOverride, + }; + + // Register plugins for debug override capabilities + registerPluginsForDebugOverride(logger, debugOverride, plugins); + return { client: client, // The client object containing all public methods. options: options, // The validated configuration object, including all defaults. diff --git a/src/plugins.js b/src/plugins.js index 561635b..8bd8b10 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -56,6 +56,22 @@ function registerPlugins(logger, environmentMetadata, client, plugins) { }); } +/** + * Registers plugins for debug override capabilities + * @param {{ error: (message: string) => void }} logger - The logger instance + * @param {Object} debugOverride - The debug override interface object + * @param {Array<{registerDebug?: (debugOverride: object) => void}>} plugins - Array of plugin objects that may implement registerDebug + */ +function registerPluginsForDebugOverride(logger, debugOverride, plugins) { + plugins.forEach(plugin => { + try { + plugin.registerDebug?.(debugOverride); + } catch (error) { + logger.error(`Exception thrown registering debug override with plugin ${getPluginName(logger, plugin)}.`); + } + }); +} + /** * Creates a plugin environment object * @param {{userAgent: string, version: string}} platform - The platform object @@ -105,5 +121,6 @@ function createPluginEnvironment(platform, env, options) { module.exports = { getPluginHooks, registerPlugins, + registerPluginsForDebugOverride, createPluginEnvironment, }; diff --git a/typings.d.ts b/typings.d.ts index 6484457..cd0267c 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -149,7 +149,6 @@ declare module 'launchdarkly-js-sdk-common' { readonly metricValue?: number; } - /** * Interface for extending SDK functionality via hooks. */ @@ -174,10 +173,7 @@ declare module 'launchdarkly-js-sdk-common' { * return {...data, "my-new-field": /*my data/*} * ``` */ - beforeEvaluation?( - hookContext: EvaluationSeriesContext, - data: EvaluationSeriesData, - ): EvaluationSeriesData; + beforeEvaluation?(hookContext: EvaluationSeriesContext, data: EvaluationSeriesData): EvaluationSeriesData; /** * This method is called during the execution of the variation method @@ -199,7 +195,7 @@ declare module 'launchdarkly-js-sdk-common' { afterEvaluation?( hookContext: EvaluationSeriesContext, data: EvaluationSeriesData, - detail: LDEvaluationDetail, + detail: LDEvaluationDetail ): EvaluationSeriesData; /** @@ -237,7 +233,7 @@ declare module 'launchdarkly-js-sdk-common' { afterIdentify?( hookContext: IdentifySeriesContext, data: IdentifySeriesData, - result: IdentifySeriesResult, + result: IdentifySeriesResult ): IdentifySeriesData; /** @@ -263,8 +259,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the SDK that is running the plugin. - */ + * Metadata about the SDK that is running the plugin. + */ export interface LDPluginSdkMetadata { /** * The name of the SDK. @@ -288,8 +284,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the application where the LaunchDarkly SDK is running. - */ + * Metadata about the application where the LaunchDarkly SDK is running. + */ export interface LDPluginApplicationMetadata { /** * A unique identifier representing the application where the LaunchDarkly SDK is running. @@ -313,8 +309,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the environment where the plugin is running. - */ + * Metadata about the environment where the plugin is running. + */ export interface LDPluginEnvironmentMetadata { /** * Metadata about the SDK that is running the plugin. @@ -334,36 +330,73 @@ declare module 'launchdarkly-js-sdk-common' { readonly clientSideId: string; } -/** - * Interface for plugins to the LaunchDarkly SDK. - */ -export interface LDPlugin { /** - * Get metadata about the plugin. + * Interface for plugins to the LaunchDarkly SDK. */ - getMetadata(): LDPluginMetadata; + export interface LDPlugin { + /** + * Get metadata about the plugin. + */ + getMetadata(): LDPluginMetadata; - /** - * Registers the plugin with the SDK. Called once during SDK initialization. - * - * The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate - * actions to ensure the SDK is ready before sending track events or evaluating flags. - * - * @param client The SDK client instance. - * @param environmentMetadata Information about the environment where the plugin is running. - */ - register(client: LDClientBase, environmentMetadata: LDPluginEnvironmentMetadata): void; + /** + * Registers the plugin with the SDK. Called once during SDK initialization. + * + * The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate + * actions to ensure the SDK is ready before sending track events or evaluating flags. + * + * @param client The SDK client instance. + * @param environmentMetadata Information about the environment where the plugin is running. + */ + register(client: LDClientBase, environmentMetadata: LDPluginEnvironmentMetadata): void; + + /** + * Gets a list of hooks that the plugin wants to register. + * + * This method will be called once during SDK initialization before the register method is called. + * + * If the plugin does not need to register any hooks, this method doesn't need to be implemented. + * @param metadata + */ + getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[]; + + /** + * An optional function called if the plugin wants to register debug capabilities. + * This method allows plugins to receive a debug override interface for + * temporarily overriding flag values during development and testing. + * + * @param debugOverride The debug override interface instance + */ + registerDebug?(debugOverride: LDDebugOverride): void; + } /** - * Gets a list of hooks that the plugin wants to register. - * - * This method will be called once during SDK initialization before the register method is called. - * - * If the plugin does not need to register any hooks, this method doesn't need to be implemented. - * @param metadata + * Debug interface for plugins that need to override flag values during development. + * This interface provides methods to temporarily override flag values that take + * precedence over the actual flag values from LaunchDarkly. These overrides are + * useful for testing, development, and debugging scenarios. */ - getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[]; -} + export interface LDDebugOverride { + /** + * Set an override value for a flag that takes precedence over the real flag value. + * + * @param flagKey The flag key. + * @param value The override value. + */ + setOverride(flagKey: string, value: LDFlagValue): void; + + /** + * Remove an override value for a flag, reverting to the real flag value. + * + * @param flagKey The flag key. + */ + removeOverride(flagKey: string): void; + + /** + * Clear all override values, reverting all flags to their real values. + */ + clearAllOverrides(): void; + } /** * LaunchDarkly initialization options that are supported by all variants of the JS client. @@ -1071,13 +1104,13 @@ export interface LDPlugin { * Changing the current context also causes all feature flag values to be reloaded. Until that has * finished, calls to {@link variation} will still return flag values for the previous context. You can * use a callback or a Promise to determine when the new flag values are available. - * - * It is possible that the identify call will fail. In that case, when using a callback, the callback will receive - * an error value. While the SDK will continue to function, the developer will need to be aware that - * calls to {@link variation} will still return flag values for the previous context. - * - * When using a promise, it is important that you handle the rejection case; - * otherwise it will become an unhandled Promise rejection, which is a serious error on some platforms. + * + * It is possible that the identify call will fail. In that case, when using a callback, the callback will receive + * an error value. While the SDK will continue to function, the developer will need to be aware that + * calls to {@link variation} will still return flag values for the previous context. + * + * When using a promise, it is important that you handle the rejection case; + * otherwise it will become an unhandled Promise rejection, which is a serious error on some platforms. * * @param context * The context properties. Must contain at least the `key` property. @@ -1394,7 +1427,7 @@ export interface LDPlugin { * Synchronous inspectors execute inline with evaluation and care should be taken to ensure * they have minimal performance overhead. */ - synchronous?: boolean, + synchronous?: boolean; /** * This method is called when a flag is accessed via a variation method, or it can be called based on actions in @@ -1426,7 +1459,7 @@ export interface LDPlugin { /** * If `true`, then the inspector will be ran synchronously with flag updates. */ - synchronous?: boolean, + synchronous?: boolean; /** * This method is called when the flags in the store are replaced with new flags. It will contain all flags @@ -1456,7 +1489,7 @@ export interface LDPlugin { /** * If `true`, then the inspector will be ran synchronously with flag updates. */ - synchronous?: boolean, + synchronous?: boolean; /** * This method is called when a flag is updated. It will not be called @@ -1484,7 +1517,7 @@ export interface LDPlugin { /** * If `true`, then the inspector will be ran synchronously with identification. */ - synchronous?: boolean, + synchronous?: boolean; /** * This method will be called when an identify operation completes. From 3cf00c08ace0ed8e38639b06109bc9e4aa26aff8 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Tue, 19 Aug 2025 13:51:09 +0100 Subject: [PATCH 02/15] feat: Enhance debug override functionality with removal and clearing options This update adds two new methods, `removeOverride` and `clearAllOverrides`, to the debug override capabilities, allowing for more flexible management of flag value overrides during local development. It also improves the `setOverride` method to prevent race condition by checking if the same value is already set. --- src/index.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/index.js b/src/index.js index 99b9823..9e4faff 100644 --- a/src/index.js +++ b/src/index.js @@ -889,21 +889,59 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const data = { key, value }; const mods = {}; const oldFlag = flags[data.key]; + + // Check if we're actually changing the value + if (oldFlag && oldFlag.value === value) { + logger.debug(`setOverride: No change needed for ${key}, value already ${value}`); + return; + } + const newFlag = utils.extend({}, data); delete newFlag['key']; flags[data.key] = newFlag; const newDetail = getFlagDetail(newFlag); + if (oldFlag) { mods[data.key] = { previous: oldFlag.value, current: newDetail }; } else { mods[data.key] = { current: newDetail }; } + notifyInspectionFlagChanged(data, newFlag); handleFlagChanges(mods); // don't wait for this Promise to be resolved } + function removeOverride(key) { + if (flags[key]) { + const mods = {}; + const oldFlag = flags[key]; + if (oldFlag && !oldFlag.deleted) { + mods[key] = { previous: oldFlag.value }; + } + flags[key] = { deleted: true }; + notifyInspectionFlagChanged({ key }, flags[key]); + handleFlagChanges(mods); // don't wait for this Promise to be resolved + } + } + + function clearAllOverrides() { + const mods = {}; + Object.keys(flags).forEach(key => { + const oldFlag = flags[key]; + if (oldFlag && !oldFlag.deleted) { + mods[key] = { previous: oldFlag.value }; + } + flags[key] = { deleted: true }; + }); + if (Object.keys(mods).length > 0) { + handleFlagChanges(mods); // don't wait for this Promise to be resolved + } + } + const debugOverride = { setOverride: setOverride, + removeOverride: removeOverride, + clearAllOverrides: clearAllOverrides, }; // Register plugins for debug override capabilities From 9441c92b9b3e441e23728a55ce301d86914d808b Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Wed, 20 Aug 2025 10:40:45 +0100 Subject: [PATCH 03/15] feat: Implement getAllOverrides method and enhance flag handling This update introduces the `getAllOverrides` method to retrieve all currently active flag overrides. It also refines the handling of flag overrides in the `setOverride`, `removeOverride`, and `clearAllOverrides` methods, ensuring that the current effective values are accurately managed. --- src/index.js | 79 +++++++++++++++++++++++++++++++++++++++------------- typings.d.ts | 10 +++++++ 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index 9e4faff..9dad079 100644 --- a/src/index.js +++ b/src/index.js @@ -82,6 +82,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const requestor = Requestor(platform, options, environment); let flags = {}; + let flagOverrides = {}; let useLocalStorage; let streamActive; let streamForcedState = options.streaming; @@ -335,7 +336,16 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { let detail; let flag; - if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { + // Check for override first + if ( + flagOverrides && + utils.objectHasOwnProperty(flagOverrides, key) && + flagOverrides[key] && + !flagOverrides[key].deleted + ) { + flag = flagOverrides[key]; + detail = getFlagDetail(flag); + } else if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { flag = flags[key]; detail = getFlagDetail(flag); if (flag.value === null || flag.value === undefined) { @@ -885,63 +895,92 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { registerPlugins(logger, pluginEnvironment, client, plugins); + function getCurrentFlagValue(key) { + const currentOverride = flagOverrides[key]; + if (currentOverride) { + return currentOverride.value; + } + const currentFlag = flags[key]; + if (currentFlag) { + return currentFlag.value; + } + return undefined; + } + function setOverride(key, value) { const data = { key, value }; const mods = {}; - const oldFlag = flags[data.key]; + + // Get the current effective value (either override or real flag) + const currentValue = getCurrentFlagValue(key); // Check if we're actually changing the value - if (oldFlag && oldFlag.value === value) { + if (currentValue === value) { logger.debug(`setOverride: No change needed for ${key}, value already ${value}`); return; } const newFlag = utils.extend({}, data); delete newFlag['key']; - flags[data.key] = newFlag; + flagOverrides[data.key] = newFlag; // Store in overrides, not flags const newDetail = getFlagDetail(newFlag); - if (oldFlag) { - mods[data.key] = { previous: oldFlag.value, current: newDetail }; - } else { - mods[data.key] = { current: newDetail }; - } + mods[data.key] = { previous: currentValue, current: newDetail }; notifyInspectionFlagChanged(data, newFlag); handleFlagChanges(mods); // don't wait for this Promise to be resolved } function removeOverride(key) { - if (flags[key]) { + if (flagOverrides[key]) { const mods = {}; - const oldFlag = flags[key]; - if (oldFlag && !oldFlag.deleted) { - mods[key] = { previous: oldFlag.value }; + const oldOverride = flagOverrides[key]; + const realFlag = flags[key]; + + if (oldOverride && !oldOverride.deleted) { + mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; } - flags[key] = { deleted: true }; - notifyInspectionFlagChanged({ key }, flags[key]); + + delete flagOverrides[key]; // Remove the override + notifyInspectionFlagChanged({ key }, realFlag); handleFlagChanges(mods); // don't wait for this Promise to be resolved } } function clearAllOverrides() { const mods = {}; - Object.keys(flags).forEach(key => { - const oldFlag = flags[key]; - if (oldFlag && !oldFlag.deleted) { - mods[key] = { previous: oldFlag.value }; + Object.keys(flagOverrides).forEach(key => { + const oldOverride = flagOverrides[key]; + const realFlag = flags[key]; + + if (oldOverride && !oldOverride.deleted) { + mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; } - flags[key] = { deleted: true }; }); + + flagOverrides = {}; // Clear all overrides + if (Object.keys(mods).length > 0) { handleFlagChanges(mods); // don't wait for this Promise to be resolved } } + function getAllOverrides() { + const result = {}; + Object.keys(flagOverrides).forEach(key => { + const override = flagOverrides[key]; + if (override && !override.deleted) { + result[key] = override.value; + } + }); + return result; + } + const debugOverride = { setOverride: setOverride, removeOverride: removeOverride, clearAllOverrides: clearAllOverrides, + getAllOverrides: getAllOverrides, }; // Register plugins for debug override capabilities diff --git a/typings.d.ts b/typings.d.ts index cd0267c..f258bec 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -396,6 +396,16 @@ declare module 'launchdarkly-js-sdk-common' { * Clear all override values, reverting all flags to their real values. */ clearAllOverrides(): void; + + /** + * Get all currently active flag overrides. + * + * @returns + * An object containing all active overrides as key-value pairs, + * where keys are flag keys and values are the overridden flag values. + * Returns an empty object if no overrides are active. + */ + getAllOverrides(): LDFlagSet; } /** From 697798adcb2f2572dc26ea40ad621fa53dae000a Mon Sep 17 00:00:00 2001 From: Subhav Gautam <9058689+zubhav@users.noreply.github.com.> Date: Wed, 20 Aug 2025 12:42:15 +0100 Subject: [PATCH 04/15] Add flag store facade for centralized flag access --- package.json | 4 +-- src/index.js | 77 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 1913303..d9e8268 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "launchdarkly-js-test-helpers": "1.1.0", "prettier": "1.19.1", "readline-sync": "^1.4.9", - "typescript": "~5.4.5", - "typedoc": "^0.25.13" + "typedoc": "^0.25.13", + "typescript": "~5.4.5" }, "dependencies": { "base64-js": "^1.3.0", diff --git a/src/index.js b/src/index.js index 9dad079..684e4ac 100644 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,7 @@ const highTimeoutThreshold = 5; // For definitions of the API in the platform object, see stubPlatform.js in the test code. function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { + console.log('🟢 LOCAL JS-SDK-COMMON v5.7.1 - LINKED VERSION ACTIVE 🟢'); const logger = createLogger(); const emitter = EventEmitter(logger); const initializationStateTracker = InitializationStateTracker(emitter); @@ -83,6 +84,47 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { let flags = {}; let flagOverrides = {}; + + // Central flag store facade - single source of truth for all flag access + const flagStore = { + get(key) { + // Check overrides first, then real flags + const override = flagOverrides && flagOverrides[key]; + if (override && !override.deleted) { + return override; + } + + const real = flags && flags[key]; + if (real && !real.deleted) { + return real; + } + + return null; + }, + + getAll() { + const allKeys = new Set([...Object.keys(flags || {}), ...Object.keys(flagOverrides || {})]); + + const result = {}; + allKeys.forEach(key => { + const flag = this.get(key); + if (flag) { + result[key] = flag; + } + }); + return result; + }, + + exists(key) { + return this.get(key) !== null; + }, + + // Helper to get all keys that have flags (real or override) + getAllKeys() { + return new Set([...Object.keys(flags || {}), ...Object.keys(flagOverrides || {})]); + }, + }; + let useLocalStorage; let streamActive; let streamForcedState = options.streaming; @@ -334,19 +376,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) { let detail; - let flag; - - // Check for override first - if ( - flagOverrides && - utils.objectHasOwnProperty(flagOverrides, key) && - flagOverrides[key] && - !flagOverrides[key].deleted - ) { - flag = flagOverrides[key]; - detail = getFlagDetail(flag); - } else if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { - flag = flags[key]; + + const flag = flagStore.get(key); + + if (flag) { detail = getFlagDetail(flag); if (flag.value === null || flag.value === undefined) { detail.value = defaultValue; @@ -388,21 +421,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function allFlags() { const results = {}; - if (!flags) { - return results; - } + const allFlags = flagStore.getAll(); - for (const key in flags) { - if (utils.objectHasOwnProperty(flags, key) && !flags[key].deleted) { - results[key] = variationDetailInternal( - key, - null, - !options.sendEventsOnlyForVariation, - false, - true, - false - ).value; - } + for (const key in allFlags) { + results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true, false).value; } return results; @@ -927,6 +949,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { mods[data.key] = { previous: currentValue, current: newDetail }; + // Do we need this? notifyInspectionFlagChanged(data, newFlag); handleFlagChanges(mods); // don't wait for this Promise to be resolved } From 8a7f2ffa442bc1f063066dd24d1462f1d19caec1 Mon Sep 17 00:00:00 2001 From: Subhav Gautam <9058689+zubhav@users.noreply.github.com.> Date: Wed, 20 Aug 2025 12:46:41 +0100 Subject: [PATCH 05/15] Refactor setOverride to use flagStore.get method --- src/index.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/index.js b/src/index.js index 684e4ac..b840191 100644 --- a/src/index.js +++ b/src/index.js @@ -917,26 +917,12 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { registerPlugins(logger, pluginEnvironment, client, plugins); - function getCurrentFlagValue(key) { - const currentOverride = flagOverrides[key]; - if (currentOverride) { - return currentOverride.value; - } - const currentFlag = flags[key]; - if (currentFlag) { - return currentFlag.value; - } - return undefined; - } - function setOverride(key, value) { const data = { key, value }; const mods = {}; - // Get the current effective value (either override or real flag) - const currentValue = getCurrentFlagValue(key); + const currentValue = flagStore.get(key); - // Check if we're actually changing the value if (currentValue === value) { logger.debug(`setOverride: No change needed for ${key}, value already ${value}`); return; From bfbc9a91055a4475ab7387dda9aa25b9be25e4a6 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Wed, 20 Aug 2025 20:45:25 +0100 Subject: [PATCH 06/15] fix: add objectHasOwnProperty checks to flagStore Ensures consistent property validation. --- src/index.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index b840191..da4cea2 100644 --- a/src/index.js +++ b/src/index.js @@ -89,14 +89,17 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const flagStore = { get(key) { // Check overrides first, then real flags - const override = flagOverrides && flagOverrides[key]; - if (override && !override.deleted) { - return override; + if ( + flagOverrides && + utils.objectHasOwnProperty(flagOverrides, key) && + flagOverrides[key] && + !flagOverrides[key].deleted + ) { + return flagOverrides[key]; } - const real = flags && flags[key]; - if (real && !real.deleted) { - return real; + if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { + return flags[key]; } return null; @@ -115,11 +118,13 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { return result; }, + // Do we need this? exists(key) { return this.get(key) !== null; }, // Helper to get all keys that have flags (real or override) + // Do we need this? getAllKeys() { return new Set([...Object.keys(flags || {}), ...Object.keys(flagOverrides || {})]); }, From 3086a445c90718c9ed5d083a5139943c0555b71d Mon Sep 17 00:00:00 2001 From: Subhav Gautam <9058689+zubhav@users.noreply.github.com.> Date: Wed, 20 Aug 2025 22:21:48 +0100 Subject: [PATCH 07/15] Refactor flag handling for better readability --- src/index.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index da4cea2..cfe6997 100644 --- a/src/index.js +++ b/src/index.js @@ -89,12 +89,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const flagStore = { get(key) { // Check overrides first, then real flags - if ( - flagOverrides && - utils.objectHasOwnProperty(flagOverrides, key) && - flagOverrides[key] && - !flagOverrides[key].deleted - ) { + if (flagOverrides && utils.objectHasOwnProperty(flagOverrides, key) && flagOverrides[key]) { return flagOverrides[key]; } @@ -951,9 +946,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const oldOverride = flagOverrides[key]; const realFlag = flags[key]; - if (oldOverride && !oldOverride.deleted) { - mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; - } + // Always create change event since we're removing an override + mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; delete flagOverrides[key]; // Remove the override notifyInspectionFlagChanged({ key }, realFlag); @@ -967,9 +961,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const oldOverride = flagOverrides[key]; const realFlag = flags[key]; - if (oldOverride && !oldOverride.deleted) { - mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; - } + mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; }); flagOverrides = {}; // Clear all overrides @@ -983,7 +975,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const result = {}; Object.keys(flagOverrides).forEach(key => { const override = flagOverrides[key]; - if (override && !override.deleted) { + if (override) { result[key] = override.value; } }); From 28e6e42dc033dcef3699c78aadfa5b3bfb020c92 Mon Sep 17 00:00:00 2001 From: Subhav Gautam <9058689+zubhav@users.noreply.github.com.> Date: Wed, 20 Aug 2025 22:24:34 +0100 Subject: [PATCH 08/15] Remove unnecessary comments and code in initialize function --- src/index.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/index.js b/src/index.js index cfe6997..c34ae6d 100644 --- a/src/index.js +++ b/src/index.js @@ -113,16 +113,9 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { return result; }, - // Do we need this? exists(key) { return this.get(key) !== null; }, - - // Helper to get all keys that have flags (real or override) - // Do we need this? - getAllKeys() { - return new Set([...Object.keys(flags || {}), ...Object.keys(flagOverrides || {})]); - }, }; let useLocalStorage; @@ -930,14 +923,13 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const newFlag = utils.extend({}, data); delete newFlag['key']; - flagOverrides[data.key] = newFlag; // Store in overrides, not flags + flagOverrides[data.key] = newFlag; const newDetail = getFlagDetail(newFlag); mods[data.key] = { previous: currentValue, current: newDetail }; - // Do we need this? notifyInspectionFlagChanged(data, newFlag); - handleFlagChanges(mods); // don't wait for this Promise to be resolved + handleFlagChanges(mods); } function removeOverride(key) { @@ -946,12 +938,11 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const oldOverride = flagOverrides[key]; const realFlag = flags[key]; - // Always create change event since we're removing an override mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; - delete flagOverrides[key]; // Remove the override + delete flagOverrides[key]; notifyInspectionFlagChanged({ key }, realFlag); - handleFlagChanges(mods); // don't wait for this Promise to be resolved + handleFlagChanges(mods); } } @@ -964,10 +955,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; }); - flagOverrides = {}; // Clear all overrides + flagOverrides = {}; if (Object.keys(mods).length > 0) { - handleFlagChanges(mods); // don't wait for this Promise to be resolved + handleFlagChanges(mods); } } From 74f44ed74ebccb2b2ca978ea39e3d27c4f493d39 Mon Sep 17 00:00:00 2001 From: Subhav Gautam <9058689+zubhav@users.noreply.github.com.> Date: Wed, 20 Aug 2025 23:52:15 +0100 Subject: [PATCH 09/15] Update handling of flag changes to not wait for Promise --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index c34ae6d..e091c26 100644 --- a/src/index.js +++ b/src/index.js @@ -942,7 +942,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { delete flagOverrides[key]; notifyInspectionFlagChanged({ key }, realFlag); - handleFlagChanges(mods); + handleFlagChanges(mods); // don't wait for this Promise to be resolved } } @@ -958,7 +958,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { flagOverrides = {}; if (Object.keys(mods).length > 0) { - handleFlagChanges(mods); + handleFlagChanges(mods); // don't wait for this Promise to be resolved } } From dd1a828658da39d91db7a2711f7c6c09f35b39d3 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Sat, 30 Aug 2025 22:35:20 +0100 Subject: [PATCH 10/15] test: add tests for debug override functionality Includes expanding plugin tests to check for registration with debug override capabilities. --- src/__tests__/LDClient-debugOverride-test.js | 342 +++++++++++++++++++ src/__tests__/LDClient-plugins-test.js | 68 +++- src/index.js | 5 - test-types.ts | 50 +-- 4 files changed, 433 insertions(+), 32 deletions(-) create mode 100644 src/__tests__/LDClient-debugOverride-test.js diff --git a/src/__tests__/LDClient-debugOverride-test.js b/src/__tests__/LDClient-debugOverride-test.js new file mode 100644 index 0000000..8b4ffdd --- /dev/null +++ b/src/__tests__/LDClient-debugOverride-test.js @@ -0,0 +1,342 @@ +const { initialize } = require('../index'); +const stubPlatform = require('./stubPlatform'); +const { respondJson } = require('./mockHttp'); +const { makeBootstrap } = require('./testUtils'); + +// Mock the logger functions +const mockLogger = () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}); + +// Define a basic Plugin structure for tests +const createTestPlugin = (name = 'Test Plugin') => { + const plugin = { + getMetadata: jest.fn().mockReturnValue({ name }), + register: jest.fn(), + getHooks: jest.fn().mockReturnValue([]), + registerDebug: jest.fn(), + }; + + return plugin; +}; + +// Helper to initialize the client for tests +async function withClient(initialContext, configOverrides = {}, plugins = [], testFn) { + const platform = stubPlatform.defaults(); + const server = platform.testing.http.newServer(); + const logger = mockLogger(); + + // Disable streaming and event sending unless overridden + const defaults = { + baseUrl: server.url, + streaming: false, + sendEvents: false, + useLdd: false, + logger: logger, + plugins: plugins, + }; + const config = { ...defaults, ...configOverrides }; + const { client, start } = initialize('env', initialContext, config, platform); + + server.byDefault(respondJson({})); + start(); + + try { + await client.waitForInitialization(10); + await testFn(client, logger, platform); + } finally { + await client.close(); + server.close(); + } +} + +describe('LDDebugOverride', () => { + describe('setOverride method', () => { + it('should set override value returned by variation method', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + expect(client.variation('test-flag', 'default')).toBe('default'); + + debugOverrideInterface.setOverride('test-flag', 'override-value'); + expect(client.variation('test-flag', 'default')).toBe('override-value'); + }); + }); + + it('should override values taking precedence over real flag values from bootstrap', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + const flags = makeBootstrap({ 'existing-flag': { value: 'real-value', version: 1 } }); + + await withClient({ key: 'user-key', kind: 'user' }, { bootstrap: flags }, [mockPlugin], async client => { + expect(client.variation('existing-flag', 'default')).toBe('real-value'); + + debugOverrideInterface.setOverride('existing-flag', 'override-value'); + expect(client.variation('existing-flag', 'default')).toBe('override-value'); + + debugOverrideInterface.removeOverride('existing-flag'); + expect(client.variation('existing-flag', 'default')).toBe('real-value'); + }); + }); + }); + + describe('removeOverride method', () => { + it('should remove individual override and revert to default', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('test-flag', 'override-value'); + expect(client.variation('test-flag', 'default')).toBe('override-value'); + + debugOverrideInterface.removeOverride('test-flag'); + expect(client.variation('test-flag', 'default')).toBe('default'); + }); + }); + + it('should remove only the specified override leaving others intact', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('flag1', 'value1'); + debugOverrideInterface.setOverride('flag2', 'value2'); + debugOverrideInterface.setOverride('flag3', 'value3'); + + debugOverrideInterface.removeOverride('flag2'); + + expect(client.variation('flag1', 'default')).toBe('value1'); + expect(client.variation('flag2', 'default')).toBe('default'); + expect(client.variation('flag3', 'default')).toBe('value3'); + + const allOverrides = debugOverrideInterface.getAllOverrides(); + expect(allOverrides).toEqual({ + flag1: 'value1', + flag3: 'value3', + }); + }); + }); + + it('should handle removing non-existent override without throwing error', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('existing-flag', 'value'); + + // Should not throw error + expect(() => { + debugOverrideInterface.removeOverride('non-existent-flag'); + }).not.toThrow(); + + // Existing override should remain + expect(client.variation('existing-flag', 'default')).toBe('value'); + const allOverrides = debugOverrideInterface.getAllOverrides(); + expect(allOverrides).toEqual({ 'existing-flag': 'value' }); + }); + }); + + it('should be callable multiple times on same flag key safely', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('test-flag', 'value'); + + debugOverrideInterface.removeOverride('test-flag'); + expect(client.variation('test-flag', 'default')).toBe('default'); + + // Removing again should not cause issues + expect(() => { + debugOverrideInterface.removeOverride('test-flag'); + }).not.toThrow(); + + expect(debugOverrideInterface.getAllOverrides()).toEqual({}); + }); + }); + }); + + describe('clearAllOverrides method', () => { + it('should clear all overrides and revert all flags to their default values', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('flag1', 'value1'); + debugOverrideInterface.setOverride('flag2', 'value2'); + + debugOverrideInterface.clearAllOverrides(); + expect(client.variation('flag1', 'default')).toBe('default'); + expect(client.variation('flag2', 'default')).toBe('default'); + expect(debugOverrideInterface.getAllOverrides()).toEqual({}); + }); + }); + + it('should operate safely when no overrides exist', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + // Should not throw error when no overrides exist + expect(() => { + debugOverrideInterface.clearAllOverrides(); + }).not.toThrow(); + + expect(debugOverrideInterface.getAllOverrides()).toEqual({}); + }); + }); + }); + + describe('getAllOverrides method', () => { + it('should return all current overrides', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + debugOverrideInterface.setOverride('test-flag', 'override-value'); + + const allOverrides = debugOverrideInterface.getAllOverrides(); + expect(allOverrides).toEqual({ 'test-flag': 'override-value' }); + }); + }); + + it('should return empty object when no overrides have been set', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + const allOverrides = debugOverrideInterface.getAllOverrides(); + expect(allOverrides).toEqual({}); + expect(typeof allOverrides).toBe('object'); + expect(Array.isArray(allOverrides)).toBe(false); + }); + }); + + it('should return immutable copy not reference to internal state', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('test-flag', 'original-value'); + + const overrides1 = debugOverrideInterface.getAllOverrides(); + const overrides2 = debugOverrideInterface.getAllOverrides(); + + // Should be different objects + expect(overrides1).not.toBe(overrides2); + + // Modifying returned object should not affect internal state + overrides1['new-flag'] = 'new-value'; + delete overrides1['test-flag']; + + expect(client.variation('test-flag', 'default')).toBe('original-value'); + expect(client.variation('new-flag', 'default')).toBe('default'); + + const overrides3 = debugOverrideInterface.getAllOverrides(); + expect(overrides3).toEqual({ 'test-flag': 'original-value' }); + }); + }); + + it('should maintain consistency across different operations', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + // Test consistency through various operations + expect(debugOverrideInterface.getAllOverrides()).toEqual({}); + + debugOverrideInterface.setOverride('flag1', 'value1'); + expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag1: 'value1' }); + + debugOverrideInterface.setOverride('flag2', 'value2'); + expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag1: 'value1', flag2: 'value2' }); + + debugOverrideInterface.removeOverride('flag1'); + expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag2: 'value2' }); + + debugOverrideInterface.setOverride('flag2', 'updated-value2'); + expect(debugOverrideInterface.getAllOverrides()).toEqual({ flag2: 'updated-value2' }); + + debugOverrideInterface.clearAllOverrides(); + expect(debugOverrideInterface.getAllOverrides()).toEqual({}); + }); + }); + }); + + describe('integration with client methods', () => { + it('should work correctly with variationDetail method', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async client => { + debugOverrideInterface.setOverride('test-flag', 'override-value'); + + const detail = client.variationDetail('test-flag', 'default'); + expect(detail.value).toBe('override-value'); + }); + }); + + it('should include overrides in allFlags method output', async () => { + let debugOverrideInterface; + const mockPlugin = createTestPlugin('test-plugin'); + mockPlugin.registerDebug.mockImplementation(debugOverride => { + debugOverrideInterface = debugOverride; + }); + + const flags = makeBootstrap({ 'real-flag': { value: 'real-value', version: 1 } }); + + await withClient({ key: 'user-key', kind: 'user' }, { bootstrap: flags }, [mockPlugin], async client => { + debugOverrideInterface.setOverride('override-flag', 'override-value'); + debugOverrideInterface.setOverride('real-flag', 'overridden-real-value'); + + const allFlags = client.allFlags(); + expect(allFlags['real-flag']).toBe('overridden-real-value'); + expect(allFlags['override-flag']).toBe('override-value'); + }); + }); + }); +}); diff --git a/src/__tests__/LDClient-plugins-test.js b/src/__tests__/LDClient-plugins-test.js index fcc690b..411405a 100644 --- a/src/__tests__/LDClient-plugins-test.js +++ b/src/__tests__/LDClient-plugins-test.js @@ -21,11 +21,19 @@ const createTestHook = (name = 'Test Hook') => ({ }); // Define a basic Plugin structure for tests -const createTestPlugin = (name = 'Test Plugin', hooks = []) => ({ - getMetadata: jest.fn().mockReturnValue({ name }), - register: jest.fn(), - getHooks: jest.fn().mockReturnValue(hooks), -}); +const createTestPlugin = (name = 'Test Plugin', hooks = [], includeDebug = false) => { + const plugin = { + getMetadata: jest.fn().mockReturnValue({ name }), + register: jest.fn(), + getHooks: jest.fn().mockReturnValue(hooks), + }; + + if (includeDebug) { + plugin.registerDebug = jest.fn(); + } + + return plugin; +}; // Helper to initialize the client for tests async function withClient(initialContext, configOverrides = {}, plugins = [], testFn) { @@ -212,3 +220,53 @@ it('passes correct environmentMetadata without optional fields', async () => { } ); }); + +it('registers plugins and calls registerDebug when a plugin implements it', async () => { + const mockPlugin = createTestPlugin('test-plugin', [], true); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + expect(mockPlugin.register).toHaveBeenCalled(); + + // Verify that registerDebug was called + expect(mockPlugin.registerDebug).toHaveBeenCalledTimes(1); + }); +}); + +it('registers plugins but does not call registerDebug when a plugin does not implement it', async () => { + const mockPlugin = createTestPlugin('test-plugin', [], false); + + await withClient({ key: 'user-key', kind: 'user' }, {}, [mockPlugin], async () => { + expect(mockPlugin.register).toHaveBeenCalled(); + + // Verify that registerDebug was not called + expect(mockPlugin.registerDebug).toBeUndefined(); + }); +}); + +it('registers multiple plugins and calls registerDebug selectively', async () => { + const mockPluginWithDebug1 = createTestPlugin('test-plugin-with-debug-1', [], true); + const mockPluginWithDebug2 = createTestPlugin('test-plugin-with-debug-2', [], true); + const mockPluginWithoutDebug1 = createTestPlugin('test-plugin-without-debug-1', [], false); + const mockPluginWithoutDebug2 = createTestPlugin('test-plugin-without-debug-2', [], false); + + await withClient( + { key: 'user-key', kind: 'user' }, + {}, + [mockPluginWithDebug1, mockPluginWithoutDebug1, mockPluginWithDebug2, mockPluginWithoutDebug2], + async () => { + // Verify all plugins were registered + expect(mockPluginWithDebug1.register).toHaveBeenCalled(); + expect(mockPluginWithDebug2.register).toHaveBeenCalled(); + expect(mockPluginWithoutDebug1.register).toHaveBeenCalled(); + expect(mockPluginWithoutDebug2.register).toHaveBeenCalled(); + + // Verify that registerDebug was called only on plugins that implement it + expect(mockPluginWithDebug1.registerDebug).toHaveBeenCalledTimes(1); + expect(mockPluginWithDebug2.registerDebug).toHaveBeenCalledTimes(1); + + // Verify that registerDebug was not called on plugins that don't implement it + expect(mockPluginWithoutDebug1.registerDebug).toBeUndefined(); + expect(mockPluginWithoutDebug2.registerDebug).toBeUndefined(); + } + ); +}); diff --git a/src/index.js b/src/index.js index e091c26..73261f2 100644 --- a/src/index.js +++ b/src/index.js @@ -38,7 +38,6 @@ const highTimeoutThreshold = 5; // For definitions of the API in the platform object, see stubPlatform.js in the test code. function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { - console.log('🟢 LOCAL JS-SDK-COMMON v5.7.1 - LINKED VERSION ACTIVE 🟢'); const logger = createLogger(); const emitter = EventEmitter(logger); const initializationStateTracker = InitializationStateTracker(emitter); @@ -112,10 +111,6 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { }); return result; }, - - exists(key) { - return this.get(key) !== null; - }, }; let useLocalStorage; diff --git a/test-types.ts b/test-types.ts index b27906f..30afa54 100644 --- a/test-types.ts +++ b/test-types.ts @@ -1,4 +1,3 @@ - // This file exists only so that we can run the TypeScript compiler in the CI build // to validate our typings.d.ts file. @@ -18,14 +17,14 @@ var user: ld.LDContext = { country: 'us', anonymous: true, custom: { - 'a': 's', - 'b': true, - 'c': 3, - 'd': [ 'x', 'y' ], - 'e': [ true, false ], - 'f': [ 1, 2 ] + a: 's', + b: true, + c: 3, + d: ['x', 'y'], + e: [true, false], + f: [1, 2], }, - privateAttributeNames: [ 'name', 'email' ] + privateAttributeNames: ['name', 'email'], }; const hook: ld.Hook = { getMetadata: () => ({ @@ -35,26 +34,33 @@ const hook: ld.Hook = { beforeEvaluation(hookContext: ld.EvaluationSeriesContext, data: ld.EvaluationSeriesData): ld.EvaluationSeriesData { return data; }, - afterEvaluation(hookContext: ld.EvaluationSeriesContext, data: ld.EvaluationSeriesData, detail: ld.LDEvaluationDetail): ld.EvaluationSeriesData { + afterEvaluation( + hookContext: ld.EvaluationSeriesContext, + data: ld.EvaluationSeriesData, + detail: ld.LDEvaluationDetail + ): ld.EvaluationSeriesData { return data; }, beforeIdentify(hookContext: ld.IdentifySeriesContext, data: ld.IdentifySeriesData): ld.IdentifySeriesData { return data; }, - afterIdentify(hookContext: ld.IdentifySeriesContext, data: ld.IdentifySeriesData, result: ld.IdentifySeriesResult): ld.IdentifySeriesData { + afterIdentify( + hookContext: ld.IdentifySeriesContext, + data: ld.IdentifySeriesData, + result: ld.IdentifySeriesResult + ): ld.IdentifySeriesData { return data; }, - afterTrack(hookContext: ld.TrackSeriesContext): void { - } + afterTrack(hookContext: ld.TrackSeriesContext): void {}, }; const plugin: ld.LDPlugin = { getMetadata: () => ({ name: 'plugin', }), - register(client: ld.LDClientBase, environmentMetadata: ld.LDPluginEnvironmentMetadata): void { - }, + register(client: ld.LDClientBase, environmentMetadata: ld.LDPluginEnvironmentMetadata): void {}, + registerDebug(debugOverride: ld.LDDebugOverride): void {}, getHooks(metadata: ld.LDPluginEnvironmentMetadata): ld.Hook[] { return []; @@ -63,31 +69,31 @@ const plugin: ld.LDPlugin = { var logger: ld.LDLogger = ld.commonBasicLogger({ level: 'info' }); var allBaseOptions: ld.LDOptionsBase = { - bootstrap: { }, + bootstrap: {}, baseUrl: '', eventsUrl: '', streamUrl: '', streaming: true, useReport: true, sendLDHeaders: true, - requestHeaderTransform: (x) => x, + requestHeaderTransform: x => x, evaluationReasons: true, sendEvents: true, allAttributesPrivate: true, - privateAttributes: [ 'x' ], + privateAttributes: ['x'], sendEventsOnlyForVariation: true, flushInterval: 1, streamReconnectDelay: 1, logger: logger, application: { version: 'version', - id: 'id' + id: 'id', }, - hooks: [ hook ], - plugins: [ plugin ] + hooks: [hook], + plugins: [plugin], }; -var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile +var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile client.waitUntilReady().then(() => {}); client.waitForInitialization(5).then(() => {}); @@ -104,7 +110,7 @@ client.flush().then(() => {}); var boolFlagValue: ld.LDFlagValue = client.variation('key', false); var numberFlagValue: ld.LDFlagValue = client.variation('key', 2); var stringFlagValue: ld.LDFlagValue = client.variation('key', 'default'); -var jsonFlagValue: ld.LDFlagValue = client.variation('key', [ 'a', 'b' ]); +var jsonFlagValue: ld.LDFlagValue = client.variation('key', ['a', 'b']); var detail: ld.LDEvaluationDetail = client.variationDetail('key', 'default'); var detailValue: ld.LDFlagValue = detail.value; From ce4a44f6440a7f731804389a56b43152617baccd Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Sun, 31 Aug 2025 22:28:18 +0100 Subject: [PATCH 11/15] style: revert code formatting changes --- test-types.ts | 50 +++++++------- typings.d.ts | 179 +++++++++++++++++++++++++------------------------- 2 files changed, 113 insertions(+), 116 deletions(-) diff --git a/test-types.ts b/test-types.ts index 30afa54..e0bbc73 100644 --- a/test-types.ts +++ b/test-types.ts @@ -1,3 +1,4 @@ + // This file exists only so that we can run the TypeScript compiler in the CI build // to validate our typings.d.ts file. @@ -17,14 +18,14 @@ var user: ld.LDContext = { country: 'us', anonymous: true, custom: { - a: 's', - b: true, - c: 3, - d: ['x', 'y'], - e: [true, false], - f: [1, 2], + 'a': 's', + 'b': true, + 'c': 3, + 'd': [ 'x', 'y' ], + 'e': [ true, false ], + 'f': [ 1, 2 ] }, - privateAttributeNames: ['name', 'email'], + privateAttributeNames: [ 'name', 'email' ] }; const hook: ld.Hook = { getMetadata: () => ({ @@ -34,34 +35,27 @@ const hook: ld.Hook = { beforeEvaluation(hookContext: ld.EvaluationSeriesContext, data: ld.EvaluationSeriesData): ld.EvaluationSeriesData { return data; }, - afterEvaluation( - hookContext: ld.EvaluationSeriesContext, - data: ld.EvaluationSeriesData, - detail: ld.LDEvaluationDetail - ): ld.EvaluationSeriesData { + afterEvaluation(hookContext: ld.EvaluationSeriesContext, data: ld.EvaluationSeriesData, detail: ld.LDEvaluationDetail): ld.EvaluationSeriesData { return data; }, beforeIdentify(hookContext: ld.IdentifySeriesContext, data: ld.IdentifySeriesData): ld.IdentifySeriesData { return data; }, - afterIdentify( - hookContext: ld.IdentifySeriesContext, - data: ld.IdentifySeriesData, - result: ld.IdentifySeriesResult - ): ld.IdentifySeriesData { + afterIdentify(hookContext: ld.IdentifySeriesContext, data: ld.IdentifySeriesData, result: ld.IdentifySeriesResult): ld.IdentifySeriesData { return data; }, - afterTrack(hookContext: ld.TrackSeriesContext): void {}, + afterTrack(hookContext: ld.TrackSeriesContext): void { + } }; const plugin: ld.LDPlugin = { getMetadata: () => ({ name: 'plugin', }), - register(client: ld.LDClientBase, environmentMetadata: ld.LDPluginEnvironmentMetadata): void {}, + register(client: ld.LDClientBase, environmentMetadata: ld.LDPluginEnvironmentMetadata): void { + }, registerDebug(debugOverride: ld.LDDebugOverride): void {}, - getHooks(metadata: ld.LDPluginEnvironmentMetadata): ld.Hook[] { return []; }, @@ -69,31 +63,31 @@ const plugin: ld.LDPlugin = { var logger: ld.LDLogger = ld.commonBasicLogger({ level: 'info' }); var allBaseOptions: ld.LDOptionsBase = { - bootstrap: {}, + bootstrap: { }, baseUrl: '', eventsUrl: '', streamUrl: '', streaming: true, useReport: true, sendLDHeaders: true, - requestHeaderTransform: x => x, + requestHeaderTransform: (x) => x, evaluationReasons: true, sendEvents: true, allAttributesPrivate: true, - privateAttributes: ['x'], + privateAttributes: [ 'x' ], sendEventsOnlyForVariation: true, flushInterval: 1, streamReconnectDelay: 1, logger: logger, application: { version: 'version', - id: 'id', + id: 'id' }, - hooks: [hook], - plugins: [plugin], + hooks: [ hook ], + plugins: [ plugin ] }; -var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile +var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile client.waitUntilReady().then(() => {}); client.waitForInitialization(5).then(() => {}); @@ -110,7 +104,7 @@ client.flush().then(() => {}); var boolFlagValue: ld.LDFlagValue = client.variation('key', false); var numberFlagValue: ld.LDFlagValue = client.variation('key', 2); var stringFlagValue: ld.LDFlagValue = client.variation('key', 'default'); -var jsonFlagValue: ld.LDFlagValue = client.variation('key', ['a', 'b']); +var jsonFlagValue: ld.LDFlagValue = client.variation('key', [ 'a', 'b' ]); var detail: ld.LDEvaluationDetail = client.variationDetail('key', 'default'); var detailValue: ld.LDFlagValue = detail.value; diff --git a/typings.d.ts b/typings.d.ts index f258bec..d39d7d9 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -74,7 +74,7 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Meta-data about a hook implementation. + * Metadata about a hook implementation. */ export interface HookMetadata { /** @@ -173,7 +173,10 @@ declare module 'launchdarkly-js-sdk-common' { * return {...data, "my-new-field": /*my data/*} * ``` */ - beforeEvaluation?(hookContext: EvaluationSeriesContext, data: EvaluationSeriesData): EvaluationSeriesData; + beforeEvaluation?( + hookContext: EvaluationSeriesContext, + data: EvaluationSeriesData, + ): EvaluationSeriesData; /** * This method is called during the execution of the variation method @@ -195,7 +198,7 @@ declare module 'launchdarkly-js-sdk-common' { afterEvaluation?( hookContext: EvaluationSeriesContext, data: EvaluationSeriesData, - detail: LDEvaluationDetail + detail: LDEvaluationDetail, ): EvaluationSeriesData; /** @@ -233,7 +236,7 @@ declare module 'launchdarkly-js-sdk-common' { afterIdentify?( hookContext: IdentifySeriesContext, data: IdentifySeriesData, - result: IdentifySeriesResult + result: IdentifySeriesResult, ): IdentifySeriesData; /** @@ -247,7 +250,7 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Meta-data about a plugin implementation. + * Metadata about a plugin implementation. * * May be used in logs and analytics to identify the plugin. */ @@ -259,8 +262,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the SDK that is running the plugin. - */ + * Metadata about the SDK that is running the plugin. + */ export interface LDPluginSdkMetadata { /** * The name of the SDK. @@ -284,8 +287,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the application where the LaunchDarkly SDK is running. - */ + * Metadata about the application where the LaunchDarkly SDK is running. + */ export interface LDPluginApplicationMetadata { /** * A unique identifier representing the application where the LaunchDarkly SDK is running. @@ -309,8 +312,8 @@ declare module 'launchdarkly-js-sdk-common' { } /** - * Metadata about the environment where the plugin is running. - */ + * Metadata about the environment where the plugin is running. + */ export interface LDPluginEnvironmentMetadata { /** * Metadata about the SDK that is running the plugin. @@ -330,83 +333,83 @@ declare module 'launchdarkly-js-sdk-common' { readonly clientSideId: string; } +/** + * Interface for plugins to the LaunchDarkly SDK. + */ +export interface LDPlugin { /** - * Interface for plugins to the LaunchDarkly SDK. + * Get metadata about the plugin. */ - export interface LDPlugin { - /** - * Get metadata about the plugin. - */ - getMetadata(): LDPluginMetadata; + getMetadata(): LDPluginMetadata; - /** - * Registers the plugin with the SDK. Called once during SDK initialization. - * - * The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate - * actions to ensure the SDK is ready before sending track events or evaluating flags. - * - * @param client The SDK client instance. - * @param environmentMetadata Information about the environment where the plugin is running. - */ - register(client: LDClientBase, environmentMetadata: LDPluginEnvironmentMetadata): void; + /** + * Registers the plugin with the SDK. Called once during SDK initialization. + * + * The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate + * actions to ensure the SDK is ready before sending track events or evaluating flags. + * + * @param client The SDK client instance. + * @param environmentMetadata Information about the environment where the plugin is running. + */ + register(client: LDClientBase, environmentMetadata: LDPluginEnvironmentMetadata): void; - /** - * Gets a list of hooks that the plugin wants to register. - * - * This method will be called once during SDK initialization before the register method is called. - * - * If the plugin does not need to register any hooks, this method doesn't need to be implemented. - * @param metadata - */ - getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[]; + /** + * Gets a list of hooks that the plugin wants to register. + * + * This method will be called once during SDK initialization before the register method is called. + * + * If the plugin does not need to register any hooks, this method doesn't need to be implemented. + * @param metadata + */ + getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[]; - /** - * An optional function called if the plugin wants to register debug capabilities. - * This method allows plugins to receive a debug override interface for - * temporarily overriding flag values during development and testing. - * - * @param debugOverride The debug override interface instance - */ - registerDebug?(debugOverride: LDDebugOverride): void; - } + /** + * An optional function called if the plugin wants to register debug capabilities. + * This method allows plugins to receive a debug override interface for + * temporarily overriding flag values during development and testing. + * + * @param debugOverride The debug override interface instance + */ + registerDebug?(debugOverride: LDDebugOverride): void; +} +/** + * Debug interface for plugins that need to override flag values during development. + * This interface provides methods to temporarily override flag values that take + * precedence over the actual flag values from LaunchDarkly. These overrides are + * useful for testing, development, and debugging scenarios. + */ +export interface LDDebugOverride { /** - * Debug interface for plugins that need to override flag values during development. - * This interface provides methods to temporarily override flag values that take - * precedence over the actual flag values from LaunchDarkly. These overrides are - * useful for testing, development, and debugging scenarios. + * Set an override value for a flag that takes precedence over the real flag value. + * + * @param flagKey The flag key. + * @param value The override value. */ - export interface LDDebugOverride { - /** - * Set an override value for a flag that takes precedence over the real flag value. - * - * @param flagKey The flag key. - * @param value The override value. - */ - setOverride(flagKey: string, value: LDFlagValue): void; + setOverride(flagKey: string, value: LDFlagValue): void; - /** - * Remove an override value for a flag, reverting to the real flag value. - * - * @param flagKey The flag key. - */ - removeOverride(flagKey: string): void; + /** + * Remove an override value for a flag, reverting to the real flag value. + * + * @param flagKey The flag key. + */ + removeOverride(flagKey: string): void; - /** - * Clear all override values, reverting all flags to their real values. - */ - clearAllOverrides(): void; + /** + * Clear all override values, reverting all flags to their real values. + */ + clearAllOverrides(): void; - /** - * Get all currently active flag overrides. - * - * @returns - * An object containing all active overrides as key-value pairs, - * where keys are flag keys and values are the overridden flag values. - * Returns an empty object if no overrides are active. - */ - getAllOverrides(): LDFlagSet; - } + /** + * Get all currently active flag overrides. + * + * @returns + * An object containing all active overrides as key-value pairs, + * where keys are flag keys and values are the overridden flag values. + * Returns an empty object if no overrides are active. + */ + getAllOverrides(): LDFlagSet; +} /** * LaunchDarkly initialization options that are supported by all variants of the JS client. @@ -1114,13 +1117,13 @@ declare module 'launchdarkly-js-sdk-common' { * Changing the current context also causes all feature flag values to be reloaded. Until that has * finished, calls to {@link variation} will still return flag values for the previous context. You can * use a callback or a Promise to determine when the new flag values are available. - * - * It is possible that the identify call will fail. In that case, when using a callback, the callback will receive - * an error value. While the SDK will continue to function, the developer will need to be aware that - * calls to {@link variation} will still return flag values for the previous context. - * - * When using a promise, it is important that you handle the rejection case; - * otherwise it will become an unhandled Promise rejection, which is a serious error on some platforms. + * + * It is possible that the identify call will fail. In that case, when using a callback, the callback will receive + * an error value. While the SDK will continue to function, the developer will need to be aware that + * calls to {@link variation} will still return flag values for the previous context. + * + * When using a promise, it is important that you handle the rejection case; + * otherwise it will become an unhandled Promise rejection, which is a serious error on some platforms. * * @param context * The context properties. Must contain at least the `key` property. @@ -1437,7 +1440,7 @@ declare module 'launchdarkly-js-sdk-common' { * Synchronous inspectors execute inline with evaluation and care should be taken to ensure * they have minimal performance overhead. */ - synchronous?: boolean; + synchronous?: boolean, /** * This method is called when a flag is accessed via a variation method, or it can be called based on actions in @@ -1469,7 +1472,7 @@ declare module 'launchdarkly-js-sdk-common' { /** * If `true`, then the inspector will be ran synchronously with flag updates. */ - synchronous?: boolean; + synchronous?: boolean, /** * This method is called when the flags in the store are replaced with new flags. It will contain all flags @@ -1499,7 +1502,7 @@ declare module 'launchdarkly-js-sdk-common' { /** * If `true`, then the inspector will be ran synchronously with flag updates. */ - synchronous?: boolean; + synchronous?: boolean, /** * This method is called when a flag is updated. It will not be called @@ -1527,7 +1530,7 @@ declare module 'launchdarkly-js-sdk-common' { /** * If `true`, then the inspector will be ran synchronously with identification. */ - synchronous?: boolean; + synchronous?: boolean, /** * This method will be called when an identify operation completes. From c40f020ac8270f6f79155653565e226f7f609e96 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Mon, 1 Sep 2025 13:11:50 +0100 Subject: [PATCH 12/15] fix: restore missing allFlags implementation logic --- src/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.js b/src/index.js index 73261f2..34d905d 100644 --- a/src/index.js +++ b/src/index.js @@ -411,6 +411,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const allFlags = flagStore.getAll(); + if (!allFlags) { + return results; + } + for (const key in allFlags) { results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation, false, true, false).value; } From 2cbe74ca93be19ea0328ba21a6f3c7d346c66a81 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Wed, 3 Sep 2025 16:07:47 +0100 Subject: [PATCH 13/15] refactor: add lazy initialization for flagOverrides with optimizations Initialize flagOverrides as undefined until needed, optimize getAll() iteration, and simplify setOverride() logic. Improves performance for common case where no overrides are used while maintaining full compatibility. --- src/index.js | 71 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/index.js b/src/index.js index 34d905d..7f91a75 100644 --- a/src/index.js +++ b/src/index.js @@ -82,7 +82,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const requestor = Requestor(platform, options, environment); let flags = {}; - let flagOverrides = {}; + let flagOverrides; // Central flag store facade - single source of truth for all flag access const flagStore = { @@ -100,15 +100,26 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { }, getAll() { - const allKeys = new Set([...Object.keys(flags || {}), ...Object.keys(flagOverrides || {})]); - const result = {}; - allKeys.forEach(key => { + + // Add all flags first + for (const key in flags) { const flag = this.get(key); if (flag) { result[key] = flag; } - }); + } + + // Override with any flagOverrides (they take precedence) + if (flagOverrides) { + for (const key in flagOverrides) { + const override = this.get(key); + if (override) { + result[key] = override; + } + } + } + return result; }, }; @@ -910,42 +921,56 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { registerPlugins(logger, pluginEnvironment, client, plugins); function setOverride(key, value) { - const data = { key, value }; const mods = {}; - const currentValue = flagStore.get(key); + const currentFlag = flagStore.get(key); + const currentValue = currentFlag ? currentFlag.value : null; if (currentValue === value) { logger.debug(`setOverride: No change needed for ${key}, value already ${value}`); return; } - const newFlag = utils.extend({}, data); - delete newFlag['key']; - flagOverrides[data.key] = newFlag; + const newFlag = { value }; + if (!flagOverrides) { + flagOverrides = {}; + } + flagOverrides[key] = newFlag; const newDetail = getFlagDetail(newFlag); - mods[data.key] = { previous: currentValue, current: newDetail }; + mods[key] = { previous: currentValue, current: newDetail }; - notifyInspectionFlagChanged(data, newFlag); + notifyInspectionFlagChanged({ key }, newFlag); handleFlagChanges(mods); } function removeOverride(key) { - if (flagOverrides[key]) { - const mods = {}; - const oldOverride = flagOverrides[key]; - const realFlag = flags[key]; + if (!flagOverrides || !flagOverrides[key]) { + return; // No override to remove + } - mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; + const mods = {}; + const oldOverride = flagOverrides[key]; + const realFlag = flags[key]; - delete flagOverrides[key]; - notifyInspectionFlagChanged({ key }, realFlag); - handleFlagChanges(mods); // don't wait for this Promise to be resolved + mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; + + delete flagOverrides[key]; + + // If no more overrides, reset to undefined + if (Object.keys(flagOverrides).length === 0) { + flagOverrides = undefined; } + + notifyInspectionFlagChanged({ key }, realFlag); + handleFlagChanges(mods); // don't wait for this Promise to be resolved } function clearAllOverrides() { + if (!flagOverrides) { + return; // No overrides to clear + } + const mods = {}; Object.keys(flagOverrides).forEach(key => { const oldOverride = flagOverrides[key]; @@ -954,7 +979,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; }); - flagOverrides = {}; + flagOverrides = undefined; // Reset to undefined instead of empty object if (Object.keys(mods).length > 0) { handleFlagChanges(mods); // don't wait for this Promise to be resolved @@ -962,6 +987,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } function getAllOverrides() { + if (!flagOverrides) { + return {}; // No overrides set + } + const result = {}; Object.keys(flagOverrides).forEach(key => { const override = flagOverrides[key]; From 40a34b97af85003b760c4cdda484c42dc3b8388d Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Wed, 3 Sep 2025 17:35:49 +0100 Subject: [PATCH 14/15] feat: implement FlagStore for centralized flag management Introduced a new FlagStore module to encapsulate flag management logic. --- src/FlagStore.js | 98 ++++++++++++++++++++++++++++++++++++++++++ src/index.js | 109 ++++++++++++++++------------------------------- 2 files changed, 135 insertions(+), 72 deletions(-) create mode 100644 src/FlagStore.js diff --git a/src/FlagStore.js b/src/FlagStore.js new file mode 100644 index 0000000..3413980 --- /dev/null +++ b/src/FlagStore.js @@ -0,0 +1,98 @@ +const utils = require('./utils'); + +function FlagStore() { + let flags = {}; + let flagOverrides; + + function get(key) { + // Check overrides first, then real flags + if (flagOverrides && utils.objectHasOwnProperty(flagOverrides, key) && flagOverrides[key]) { + return flagOverrides[key]; + } + + if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { + return flags[key]; + } + + return null; + } + + function getFlagsWithOverrides() { + const result = {}; + + // Add all flags first + for (const key in flags) { + const flag = get(key); + if (flag) { + result[key] = flag; + } + } + + // Override with any flagOverrides (they take precedence) + if (flagOverrides) { + for (const key in flagOverrides) { + const override = get(key); + if (override) { + result[key] = override; + } + } + } + + return result; + } + + function setFlags(newFlags) { + flags = { ...newFlags }; + } + + function setOverride(key, value) { + if (!flagOverrides) { + flagOverrides = {}; + } + flagOverrides[key] = { value }; + } + + function removeOverride(key) { + if (!flagOverrides || !flagOverrides[key]) { + return; // No override to remove + } + + delete flagOverrides[key]; + + // If no more overrides, reset to undefined for performance + if (Object.keys(flagOverrides).length === 0) { + flagOverrides = undefined; + } + } + + function clearAllOverrides() { + if (!flagOverrides) { + return {}; // No overrides to clear, return empty object for consistency + } + + const clearedOverrides = { ...flagOverrides }; + flagOverrides = undefined; // Reset to undefined + return clearedOverrides; + } + + function getFlags() { + return flags; + } + + function getFlagOverrides() { + return flagOverrides || {}; + } + + return { + clearAllOverrides, + get, + getFlagOverrides, + getFlags, + getFlagsWithOverrides, + removeOverride, + setFlags, + setOverride, + }; +} + +module.exports = FlagStore; diff --git a/src/index.js b/src/index.js index 7f91a75..e05fec6 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ const { checkContext, getContextKeys } = require('./context'); const { InspectorTypes, InspectorManager } = require('./InspectorManager'); const timedPromise = require('./timedPromise'); const createHookRunner = require('./HookRunner'); +const FlagStore = require('./FlagStore'); const { getPluginHooks, registerPlugins, @@ -81,48 +82,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const requestor = Requestor(platform, options, environment); - let flags = {}; - let flagOverrides; - - // Central flag store facade - single source of truth for all flag access - const flagStore = { - get(key) { - // Check overrides first, then real flags - if (flagOverrides && utils.objectHasOwnProperty(flagOverrides, key) && flagOverrides[key]) { - return flagOverrides[key]; - } - - if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) { - return flags[key]; - } - - return null; - }, - - getAll() { - const result = {}; - - // Add all flags first - for (const key in flags) { - const flag = this.get(key); - if (flag) { - result[key] = flag; - } - } - - // Override with any flagOverrides (they take precedence) - if (flagOverrides) { - for (const key in flagOverrides) { - const override = this.get(key); - if (override) { - result[key] = override; - } - } - } - - return result; - }, - }; + const flagStore = FlagStore(); let useLocalStorage; let streamActive; @@ -235,7 +195,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function notifyInspectionFlagsChanged() { if (inspectorManager.hasListeners(InspectorTypes.flagDetailsChanged)) { inspectorManager.onFlags( - Object.entries(flags) + Object.entries(flagStore.getFlagsWithOverrides()) .map(([key, value]) => ({ key, detail: getFlagDetail(value) })) .reduce((acc, cur) => { // eslint-disable-next-line no-param-reassign @@ -279,7 +239,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { default: defaultValue, creationDate: now.getTime(), }; - const flag = flags[key]; + const flag = flagStore.getFlags()[key]; if (flag) { event.version = flag.flagVersion ? flag.flagVersion : flag.version; event.trackEvents = flag.trackEvents; @@ -309,7 +269,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { if (stateProvider) { // We're being controlled by another client instance, so only that instance is allowed to change the context logger.warn(messages.identifyDisabled()); - return utils.wrapPromiseCallback(Promise.resolve(utils.transformVersionedValuesToValues(flags)), onDone); + return utils.wrapPromiseCallback( + Promise.resolve(utils.transformVersionedValuesToValues(flagStore.getFlagsWithOverrides())), + onDone + ); } let afterIdentify; const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve(); @@ -420,7 +383,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function allFlags() { const results = {}; - const allFlags = flagStore.getAll(); + const allFlags = flagStore.getFlagsWithOverrides(); if (!allFlags) { return results; @@ -525,6 +488,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { // If both the flag and the patch have a version property, then the patch version must be // greater than the flag version for us to accept the patch. If either one has no version // then the patch always succeeds. + const flags = flagStore.getFlags(); const oldFlag = flags[data.key]; if (!oldFlag || !oldFlag.version || !data.version || oldFlag.version < data.version) { logger.debug(messages.debugStreamPatch(data.key)); @@ -532,6 +496,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { const newFlag = utils.extend({}, data); delete newFlag['key']; flags[data.key] = newFlag; + flagStore.setFlags(flags); const newDetail = getFlagDetail(newFlag); if (oldFlag) { mods[data.key] = { previous: oldFlag.value, current: newDetail }; @@ -549,6 +514,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { if (!data) { return; } + const flags = flagStore.getFlags(); if (!flags[data.key] || flags[data.key].version < data.version) { logger.debug(messages.debugStreamDelete(data.key)); const mods = {}; @@ -556,6 +522,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { mods[data.key] = { previous: flags[data.key].value }; } flags[data.key] = { version: data.version, deleted: true }; + flagStore.setFlags(flags); notifyInspectionFlagChanged(data, flags[data.key]); handleFlagChanges(mods); // don't wait for this Promise to be resolved } else { @@ -582,6 +549,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { return Promise.resolve(); } + const flags = flagStore.getFlags(); for (const key in flags) { if (utils.objectHasOwnProperty(flags, key) && flags[key]) { if (newFlags[key] && !utils.deepEquals(newFlags[key].value, flags[key].value)) { @@ -597,7 +565,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } } - flags = { ...newFlags }; + flagStore.setFlags({ ...newFlags }); notifyInspectionFlagsChanged(); @@ -620,7 +588,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { }); emitter.emit(changeEvent, changeEventParams); - emitter.emit(internalChangeEvent, flags); + emitter.emit(internalChangeEvent, flagStore.getFlagsWithOverrides()); // By default, we send feature evaluation events whenever we have received new flag values - // the client has in effect evaluated these flags just by receiving them. This can be suppressed @@ -635,7 +603,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } if (useLocalStorage && persistentFlagStore) { - return persistentFlagStore.saveFlags(flags); + return persistentFlagStore.saveFlags(flagStore.getFlags()); } else { return Promise.resolve(); } @@ -706,7 +674,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { if (typeof options.bootstrap === 'object') { // Set the flags as soon as possible before we get into any async code, so application code can read // them even if the ready event has not yet fired. - flags = readFlagsFromBootstrap(options.bootstrap); + flagStore.setFlags(readFlagsFromBootstrap(options.bootstrap)); } if (stateProvider) { @@ -758,7 +726,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function finishInitWithLocalStorage() { return persistentFlagStore.loadFlags().then(storedFlags => { if (storedFlags === null || storedFlags === undefined) { - flags = {}; + flagStore.setFlags({}); return requestor .fetchFlagSettings(ident.getContext(), hash) .then(requestedFlags => replaceAllFlags(requestedFlags || {})) @@ -771,7 +739,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { // We're reading the flags from local storage. Signal that we're ready, // then update localStorage for the next page load. We won't signal changes or update // the in-memory flags unless you subscribe for changes - flags = storedFlags; + flagStore.setFlags(storedFlags); utils.onNextTick(signalSuccessfulInit); return requestor @@ -786,14 +754,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { return requestor .fetchFlagSettings(ident.getContext(), hash) .then(requestedFlags => { - flags = requestedFlags || {}; + flagStore.setFlags(requestedFlags || {}); notifyInspectionFlagsChanged(); // Note, we don't need to call updateSettings here because local storage and change events are not relevant signalSuccessfulInit(); }) .catch(err => { - flags = {}; + flagStore.setFlags({}); signalFailedInit(err); }); } @@ -801,7 +769,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function initFromStateProvider(state) { environment = state.environment; ident.setContext(state.context); - flags = { ...state.flags }; + flagStore.setFlags({ ...state.flags }); utils.onNextTick(signalSuccessfulInit); } @@ -840,7 +808,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } const finishClose = () => { closed = true; - flags = {}; + flagStore.setFlags({}); }; const p = Promise.resolve() .then(() => { @@ -860,7 +828,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { function getFlagsInternal() { // used by Electron integration - return flags; + return flagStore.getFlagsWithOverrides(); } function waitForInitializationWithTimeout(timeout) { @@ -931,11 +899,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { return; } - const newFlag = { value }; - if (!flagOverrides) { - flagOverrides = {}; - } - flagOverrides[key] = newFlag; + flagStore.setOverride(key, value); + const newFlag = flagStore.get(key); const newDetail = getFlagDetail(newFlag); mods[key] = { previous: currentValue, current: newDetail }; @@ -945,33 +910,31 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } function removeOverride(key) { - if (!flagOverrides || !flagOverrides[key]) { + const flagOverrides = flagStore.getFlagOverrides(); + if (!flagOverrides[key]) { return; // No override to remove } const mods = {}; const oldOverride = flagOverrides[key]; + const flags = flagStore.getFlags(); const realFlag = flags[key]; mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; - delete flagOverrides[key]; - - // If no more overrides, reset to undefined - if (Object.keys(flagOverrides).length === 0) { - flagOverrides = undefined; - } - + flagStore.removeOverride(key); notifyInspectionFlagChanged({ key }, realFlag); handleFlagChanges(mods); // don't wait for this Promise to be resolved } function clearAllOverrides() { - if (!flagOverrides) { + const flagOverrides = flagStore.getFlagOverrides(); + if (Object.keys(flagOverrides).length === 0) { return; // No overrides to clear } const mods = {}; + const flags = flagStore.getFlags(); Object.keys(flagOverrides).forEach(key => { const oldOverride = flagOverrides[key]; const realFlag = flags[key]; @@ -979,7 +942,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { mods[key] = { previous: oldOverride.value, current: realFlag ? getFlagDetail(realFlag) : undefined }; }); - flagOverrides = undefined; // Reset to undefined instead of empty object + flagStore.clearAllOverrides(); if (Object.keys(mods).length > 0) { handleFlagChanges(mods); // don't wait for this Promise to be resolved @@ -987,6 +950,8 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) { } function getAllOverrides() { + const flagOverrides = flagStore.getFlagOverrides(); + if (!flagOverrides) { return {}; // No overrides set } From 5d306fcdfc7d67e61227331859dd51d7e7445996 Mon Sep 17 00:00:00 2001 From: Pranjal Jately Date: Thu, 4 Sep 2025 09:55:46 +0100 Subject: [PATCH 15/15] docs: enhance typings and FlagStore documentation Added experimental annotations to LDPlugin and LDDebugOverride interfaces in typings.d.ts. Add JSDoc comments to all FlagStore functions --- src/FlagStore.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ typings.d.ts | 6 ++++++ 2 files changed, 52 insertions(+) diff --git a/src/FlagStore.js b/src/FlagStore.js index 3413980..2583399 100644 --- a/src/FlagStore.js +++ b/src/FlagStore.js @@ -1,9 +1,26 @@ const utils = require('./utils'); +/** + * FlagStore - Centralized flag store and access point for all feature flags + * + * This module manages two types of feature flags: + * 1. Regular flags - Retrieved from LaunchDarkly servers or bootstrap data + * 2. Override flags - Local overrides for debugging/testing + * + * When a flag is requested: + * - If an override exists for that flag, the override value is returned + * - Otherwise, the regular flag value is returned + */ function FlagStore() { let flags = {}; + // The flag overrides are set lazily to allow bypassing property checks when no overrides are present. let flagOverrides; + /** + * Gets a single flag by key, with overrides taking precedence over regular flags + * @param {string} key The flag key to retrieve + * @returns {Object|null} The flag object or null if not found + */ function get(key) { // Check overrides first, then real flags if (flagOverrides && utils.objectHasOwnProperty(flagOverrides, key) && flagOverrides[key]) { @@ -17,6 +34,10 @@ function FlagStore() { return null; } + /** + * Gets all flags with overrides applied + * @returns {Object} Object containing all flags with any overrides applied + */ function getFlagsWithOverrides() { const result = {}; @@ -41,10 +62,19 @@ function FlagStore() { return result; } + /** + * Replaces all flags with new flag data + * @param {Object} newFlags - Object containing the new flag data + */ function setFlags(newFlags) { flags = { ...newFlags }; } + /** + * Sets an override value for a specific flag + * @param {string} key The flag key to override + * @param {*} value The override value for the flag + */ function setOverride(key, value) { if (!flagOverrides) { flagOverrides = {}; @@ -52,6 +82,10 @@ function FlagStore() { flagOverrides[key] = { value }; } + /** + * Removes an override for a specific flag + * @param {string} key The flag key to remove the override for + */ function removeOverride(key) { if (!flagOverrides || !flagOverrides[key]) { return; // No override to remove @@ -65,6 +99,10 @@ function FlagStore() { } } + /** + * Clears all flag overrides and returns the cleared overrides + * @returns {Object} The overrides that were cleared, useful for tracking what was removed + */ function clearAllOverrides() { if (!flagOverrides) { return {}; // No overrides to clear, return empty object for consistency @@ -75,10 +113,18 @@ function FlagStore() { return clearedOverrides; } + /** + * Gets the internal flag state without overrides applied + * @returns {Object} The internal flag data structure + */ function getFlags() { return flags; } + /** + * Gets the flag overrides data + * @returns {Object} The flag overrides object, or empty object if no overrides exist + */ function getFlagOverrides() { return flagOverrides || {}; } diff --git a/typings.d.ts b/typings.d.ts index d39d7d9..3aae737 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -368,6 +368,9 @@ export interface LDPlugin { * This method allows plugins to receive a debug override interface for * temporarily overriding flag values during development and testing. * + * @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time. + * The API may change in future versions. + * * @param debugOverride The debug override interface instance */ registerDebug?(debugOverride: LDDebugOverride): void; @@ -378,6 +381,9 @@ export interface LDPlugin { * This interface provides methods to temporarily override flag values that take * precedence over the actual flag values from LaunchDarkly. These overrides are * useful for testing, development, and debugging scenarios. + * + * @experimental This interface is experimental and intended for use by LaunchDarkly tools at this time. + * The API may change in future versions. */ export interface LDDebugOverride { /**