From 47b3f51301d1aa97a990b0113c677a343f80ae25 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:51:53 -0700 Subject: [PATCH 1/9] feat: expose setConnectionMode on browser SDK and restrict automatic mode switching Remove the automatic connection mode option from the browser SDK's public interface since it has no impact. Add setConnectionMode as an EAP method on the browser LDClient, giving users direct control over the connection mode with higher priority than setStreaming. --- packages/sdk/browser/src/BrowserClient.ts | 6 ++++ packages/sdk/browser/src/LDClient.ts | 24 ++++++++++++-- packages/sdk/browser/src/index.ts | 1 + packages/sdk/browser/src/options.ts | 31 +++++++++++++++++++ packages/shared/sdk-client/src/DataManager.ts | 10 ++++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 5c4b3c55fc..44b7c04967 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -7,6 +7,7 @@ import { Configuration, createDefaultSourceFactoryProvider, createFDv2DataManagerBase, + FDv2ConnectionMode, FlagManager, Hook, internal, @@ -318,6 +319,10 @@ class BrowserClientImpl extends LDClientImpl { return this._startPromise; } + setConnectionMode(mode?: FDv2ConnectionMode): void { + this.dataManager.setConnectionMode?.(mode); + } + setStreaming(streaming?: boolean): void { this.dataManager.setForcedStreaming?.(streaming); } @@ -376,6 +381,7 @@ export function makeClient( on: (key: LDEmitterEventName, callback: (...args: any[]) => void) => impl.on(key, callback), off: (key: LDEmitterEventName, callback: (...args: any[]) => void) => impl.off(key, callback), flush: () => impl.flush(), + setConnectionMode: (mode?: FDv2ConnectionMode) => impl.setConnectionMode(mode), setStreaming: (streaming?: boolean) => impl.setStreaming(streaming), identify: (pristineContext: LDContext, identifyOptions?: LDIdentifyOptions) => impl.identifyResult(pristineContext, identifyOptions), diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index 6eadedee28..83fc70f3da 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -1,4 +1,5 @@ import { + FDv2ConnectionMode, LDClient as CommonClient, LDContext, LDIdentifyResult, @@ -34,15 +35,34 @@ export interface LDStartOptions extends LDWaitForInitializationOptions { export type LDClient = Omit< CommonClient, - 'setConnectionMode' | 'getConnectionMode' | 'getOffline' | 'identify' + 'getConnectionMode' | 'getOffline' | 'identify' > & { /** * @ignore - * Implementation Note: We are not supporting dynamically setting the connection mode on the LDClient. * Implementation Note: The SDK does not support offline mode. Instead bootstrap data can be used. * Implementation Note: The browser SDK has different identify options, so omits the base implementation * from the interface. */ + /** + * Sets the connection mode for the SDK's data system. + * + * When set, this mode is used exclusively, overriding all automatic mode + * selection (including {@link setStreaming}). The {@link setStreaming} state + * is not modified -- {@link setConnectionMode} acts as a higher-priority + * override. + * + * Pass `undefined` (or call with no arguments) to clear the override and + * return to automatic mode selection. + * + * This method is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode + * + * @param mode The connection mode to use, or `undefined` to clear the override. + */ + setConnectionMode(mode?: FDv2ConnectionMode): void; + /** * Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates. * diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index ebae17c8ac..8889f1a9ec 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -18,6 +18,7 @@ import { BrowserOptions as LDOptions } from './options'; export * from './common'; export type { LDClient, LDOptions, LDStartOptions }; +export type { BrowserDataSystemOptions } from './options'; export type { LDPlugin } from './LDPlugin'; /** diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index e4e15d50a6..198599c275 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -1,6 +1,8 @@ import { + LDClientDataSystemOptions, LDLogger, LDOptions as LDOptionsBase, + ManualModeSwitching, OptionMessages, TypeValidator, TypeValidators, @@ -10,10 +12,39 @@ import { LDPlugin } from './LDPlugin'; const DEFAULT_FLUSH_INTERVAL_SECONDS = 2; +/** + * Data system options for the browser SDK. + * + * The browser SDK does not support automatic mode switching (lifecycle or + * network-based). Use `false` (the default) to disable automatic switching, + * or {@link ManualModeSwitching} to specify an explicit initial connection mode. + * + * This interface is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode + */ +export interface BrowserDataSystemOptions + extends Omit { + automaticModeSwitching?: false | ManualModeSwitching; +} + /** * Initialization options for the LaunchDarkly browser SDK. */ export interface BrowserOptions extends Omit { + /** + * @internal + * + * Configuration for the FDv2 data system. When present, the SDK uses + * the FDv2 protocol for flag delivery instead of the default FDv1 + * protocol. + * + * The browser SDK restricts `automaticModeSwitching` to `false` or + * {@link ManualModeSwitching} only — automatic switching has no effect + * in browser environments. + */ + dataSystem?: BrowserDataSystemOptions; /** * Whether the client should make a request to LaunchDarkly for Experimentation metrics (goals). * diff --git a/packages/shared/sdk-client/src/DataManager.ts b/packages/shared/sdk-client/src/DataManager.ts index 644b06d399..ef07f6e37f 100644 --- a/packages/shared/sdk-client/src/DataManager.ts +++ b/packages/shared/sdk-client/src/DataManager.ts @@ -10,6 +10,7 @@ import { subsystem, } from '@launchdarkly/js-sdk-common'; +import type FDv2ConnectionMode from './api/datasource/FDv2ConnectionMode'; import { LDIdentifyOptions } from './api/LDIdentifyOptions'; import { Configuration } from './configuration/Configuration'; import { @@ -73,6 +74,15 @@ export interface DataManager { */ setAutomaticStreamingState?(streaming: boolean): void; + /** + * Set an explicit connection mode override. When set, only this mode is + * used, bypassing all automatic behavior. Pass undefined to clear the + * override. + * + * Optional — only FDv2 data managers implement this. + */ + setConnectionMode?(mode?: FDv2ConnectionMode): void; + /** * Set a callback to flush pending analytics events. Called immediately * (not debounced) when the lifecycle transitions to background. From 3ee2efa65277cb9e01706783c662a4fc3ff3eb64 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:58:06 -0700 Subject: [PATCH 2/9] feat: add connection mode buttons to FDv2 example app Add a Connection Mode control section with buttons for each FDv2ConnectionMode (streaming, polling, offline, one-shot, background) and a Clear button to reset to automatic mode selection. --- packages/sdk/browser/example-fdv2/src/app.ts | 57 +++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/sdk/browser/example-fdv2/src/app.ts b/packages/sdk/browser/example-fdv2/src/app.ts index 3e5a928c15..412b585a7c 100644 --- a/packages/sdk/browser/example-fdv2/src/app.ts +++ b/packages/sdk/browser/example-fdv2/src/app.ts @@ -1,5 +1,10 @@ // Temporary app for testing FDv2 functionality. -import { basicLogger, createClient, type LDClient } from '@launchdarkly/js-client-sdk'; +import { + basicLogger, + createClient, + type FDv2ConnectionMode, + type LDClient, +} from '@launchdarkly/js-client-sdk'; // Set clientSideID to your LaunchDarkly client-side ID const clientSideID = 'LD_CLIENT_SIDE_ID'; @@ -98,6 +103,26 @@ function buildUI() { streamSection.appendChild(btnUndef); controls.appendChild(streamSection); + // Connection mode control + const modeSection = el('div'); + modeSection.appendChild(el('h3')); + modeSection.querySelector('h3')!.textContent = 'Connection Mode'; + const modeStatus = el('span', { id: 'mode-status' }); + modeStatus.textContent = 'undefined (automatic)'; + modeSection.appendChild(modeStatus); + modeSection.appendChild(el('br')); + const modes: FDv2ConnectionMode[] = ['streaming', 'polling', 'offline', 'one-shot', 'background']; + for (const mode of modes) { + const btn = el('button', { id: `btn-mode-${mode}` }); + btn.textContent = mode; + modeSection.appendChild(btn); + modeSection.appendChild(text(' ')); + } + const btnModeClear = el('button', { id: 'btn-mode-clear' }); + btnModeClear.textContent = 'Clear'; + modeSection.appendChild(btnModeClear); + controls.appendChild(modeSection); + // Log const logSection = el('div'); logSection.appendChild(el('h3')); @@ -149,6 +174,15 @@ function updateEvtStatus() { } } +function updateModeStatus(mode: FDv2ConnectionMode | undefined) { + const label = document.getElementById('mode-status')!; + if (mode !== undefined) { + label.textContent = `${mode} (override active)`; + } else { + label.textContent = 'undefined (automatic)'; + } +} + function updateStreamStatus(value: boolean | undefined) { const label = document.getElementById('stream-status')!; if (value === true) { @@ -240,6 +274,27 @@ const main = async () => { log('setStreaming(undefined)'); }); + // Connection mode controls + const connectionModes: FDv2ConnectionMode[] = [ + 'streaming', + 'polling', + 'offline', + 'one-shot', + 'background', + ]; + for (const mode of connectionModes) { + document.getElementById(`btn-mode-${mode}`)!.addEventListener('click', () => { + client.setConnectionMode(mode); + updateModeStatus(mode); + log(`setConnectionMode('${mode}')`); + }); + } + document.getElementById('btn-mode-clear')!.addEventListener('click', () => { + client.setConnectionMode(undefined); + updateModeStatus(undefined); + log('setConnectionMode(undefined)'); + }); + // Start client.start(); const { status } = await client.waitForInitialization(); From bcd488836152c41980ab98ab2aa058d4ad0bb557 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:39:53 -0700 Subject: [PATCH 3/9] fix: lint issues in browser SDK and example app --- packages/sdk/browser/example-fdv2/src/app.ts | 8 ++++---- packages/sdk/browser/src/LDClient.ts | 7 ++----- packages/sdk/browser/src/options.ts | 6 ++++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/sdk/browser/example-fdv2/src/app.ts b/packages/sdk/browser/example-fdv2/src/app.ts index 412b585a7c..2162b257d1 100644 --- a/packages/sdk/browser/example-fdv2/src/app.ts +++ b/packages/sdk/browser/example-fdv2/src/app.ts @@ -112,12 +112,12 @@ function buildUI() { modeSection.appendChild(modeStatus); modeSection.appendChild(el('br')); const modes: FDv2ConnectionMode[] = ['streaming', 'polling', 'offline', 'one-shot', 'background']; - for (const mode of modes) { + modes.forEach((mode) => { const btn = el('button', { id: `btn-mode-${mode}` }); btn.textContent = mode; modeSection.appendChild(btn); modeSection.appendChild(text(' ')); - } + }); const btnModeClear = el('button', { id: 'btn-mode-clear' }); btnModeClear.textContent = 'Clear'; modeSection.appendChild(btnModeClear); @@ -282,13 +282,13 @@ const main = async () => { 'one-shot', 'background', ]; - for (const mode of connectionModes) { + connectionModes.forEach((mode) => { document.getElementById(`btn-mode-${mode}`)!.addEventListener('click', () => { client.setConnectionMode(mode); updateModeStatus(mode); log(`setConnectionMode('${mode}')`); }); - } + }); document.getElementById('btn-mode-clear')!.addEventListener('click', () => { client.setConnectionMode(undefined); updateModeStatus(undefined); diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index 83fc70f3da..c48c9caf94 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -1,6 +1,6 @@ import { - FDv2ConnectionMode, LDClient as CommonClient, + FDv2ConnectionMode, LDContext, LDIdentifyResult, LDWaitForInitializationOptions, @@ -33,10 +33,7 @@ export interface LDStartOptions extends LDWaitForInitializationOptions { * For more information, see the [SDK Reference Guide](https://docs.launchdarkly.com/sdk/client-side/javascript). */ -export type LDClient = Omit< - CommonClient, - 'getConnectionMode' | 'getOffline' | 'identify' -> & { +export type LDClient = Omit & { /** * @ignore * Implementation Note: The SDK does not support offline mode. Instead bootstrap data can be used. diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index 198599c275..8500b0a01c 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -24,8 +24,10 @@ const DEFAULT_FLUSH_INTERVAL_SECONDS = 2; * to this feature please join the EAP. * https://launchdarkly.com/docs/sdk/features/data-saving-mode */ -export interface BrowserDataSystemOptions - extends Omit { +export interface BrowserDataSystemOptions extends Omit< + LDClientDataSystemOptions, + 'automaticModeSwitching' +> { automaticModeSwitching?: false | ManualModeSwitching; } From 843688dadde6f3fe96a403b3dc5b5eab823088fd Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:16:36 -0700 Subject: [PATCH 4/9] fix: mark setConnectionMode as @internal with not-ready warning --- packages/sdk/browser/example-fdv2/src/app.ts | 2 ++ packages/sdk/browser/src/LDClient.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/packages/sdk/browser/example-fdv2/src/app.ts b/packages/sdk/browser/example-fdv2/src/app.ts index 2162b257d1..ffaa39e23d 100644 --- a/packages/sdk/browser/example-fdv2/src/app.ts +++ b/packages/sdk/browser/example-fdv2/src/app.ts @@ -284,12 +284,14 @@ const main = async () => { ]; connectionModes.forEach((mode) => { document.getElementById(`btn-mode-${mode}`)!.addEventListener('click', () => { + // @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in client.setConnectionMode(mode); updateModeStatus(mode); log(`setConnectionMode('${mode}')`); }); }); document.getElementById('btn-mode-clear')!.addEventListener('click', () => { + // @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in client.setConnectionMode(undefined); updateModeStatus(undefined); log('setConnectionMode(undefined)'); diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index c48c9caf94..11e6f2c088 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -41,6 +41,11 @@ export type LDClient = Omit Date: Mon, 30 Mar 2026 11:19:33 -0700 Subject: [PATCH 5/9] fix: add not-ready warning to browser dataSystem config --- packages/sdk/browser/src/options.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index 8500b0a01c..fa719818a9 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -38,6 +38,9 @@ export interface BrowserOptions extends Omit Date: Mon, 30 Mar 2026 11:36:07 -0700 Subject: [PATCH 6/9] fix: use standard experimental warning on setConnectionMode and dataSystem --- packages/sdk/browser/src/LDClient.ts | 10 +++------- packages/sdk/browser/src/options.ts | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index 11e6f2c088..ff002a28ba 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -43,8 +43,9 @@ export type LDClient = Omit Date: Tue, 31 Mar 2026 14:37:52 -0700 Subject: [PATCH 7/9] fix: warn when setConnectionMode is called without FDv2 data system --- packages/sdk/browser/src/BrowserClient.ts | 9 ++++++++- packages/sdk/browser/src/LDClient.ts | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 44b7c04967..1936600558 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -320,7 +320,14 @@ class BrowserClientImpl extends LDClientImpl { } setConnectionMode(mode?: FDv2ConnectionMode): void { - this.dataManager.setConnectionMode?.(mode); + if (!this.dataManager.setConnectionMode) { + this.logger.warn( + 'setConnectionMode requires the FDv2 data system (dataSystem option). ' + + 'The call has no effect without it.', + ); + return; + } + this.dataManager.setConnectionMode(mode); } setStreaming(streaming?: boolean): void { diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index ff002a28ba..7de1ea0d2d 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -57,6 +57,9 @@ export type LDClient = Omit Date: Tue, 31 Mar 2026 14:49:23 -0700 Subject: [PATCH 8/9] fix: validate connection mode before forwarding to data manager --- packages/sdk/browser/src/BrowserClient.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 1936600558..5e5e086e7b 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -41,6 +41,14 @@ import validateBrowserOptions, { BrowserOptions, filterToBaseOptionsWithDefaults import BrowserPlatform from './platform/BrowserPlatform'; import { getAllStorageKeys } from './platform/LocalStorage'; +const VALID_CONNECTION_MODES: Set = new Set([ + 'streaming', + 'polling', + 'offline', + 'one-shot', + 'background', +]); + class BrowserClientImpl extends LDClientImpl { private readonly _goalManager?: GoalManager; private readonly _plugins?: LDPlugin[]; @@ -327,6 +335,13 @@ class BrowserClientImpl extends LDClientImpl { ); return; } + if (mode !== undefined && !VALID_CONNECTION_MODES.has(mode)) { + this.logger.warn( + `setConnectionMode called with invalid mode '${mode}'. ` + + `Valid modes: ${[...VALID_CONNECTION_MODES].join(', ')}.`, + ); + return; + } this.dataManager.setConnectionMode(mode); } From 52eafe25ef529b1d6882e7126f43ad24f4a48cdd Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:59:43 -0700 Subject: [PATCH 9/9] fix: derive valid connection modes from MODE_TABLE instead of hardcoding --- packages/sdk/browser/src/BrowserClient.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 5e5e086e7b..6d6660aad4 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -41,14 +41,6 @@ import validateBrowserOptions, { BrowserOptions, filterToBaseOptionsWithDefaults import BrowserPlatform from './platform/BrowserPlatform'; import { getAllStorageKeys } from './platform/LocalStorage'; -const VALID_CONNECTION_MODES: Set = new Set([ - 'streaming', - 'polling', - 'offline', - 'one-shot', - 'background', -]); - class BrowserClientImpl extends LDClientImpl { private readonly _goalManager?: GoalManager; private readonly _plugins?: LDPlugin[]; @@ -335,10 +327,10 @@ class BrowserClientImpl extends LDClientImpl { ); return; } - if (mode !== undefined && !VALID_CONNECTION_MODES.has(mode)) { + if (mode !== undefined && !(mode in MODE_TABLE)) { this.logger.warn( `setConnectionMode called with invalid mode '${mode}'. ` + - `Valid modes: ${[...VALID_CONNECTION_MODES].join(', ')}.`, + `Valid modes: ${Object.keys(MODE_TABLE).join(', ')}.`, ); return; }