Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
e6a4d76
feat: add FDv2 types, refined validators, and DataManager interface e…
kinyoklion Mar 19, 2026
a4d5ad0
feat: add FlagManager.applyChanges for FDv2 full/partial/none semantics
kinyoklion Mar 19, 2026
cb9d6b6
feat: add SourceFactoryProvider for declarative data source creation
kinyoklion Mar 19, 2026
c33790c
Merge branch 'rlamb/fdv2-flag-manager-apply-changes' into rlamb/fdv2-…
kinyoklion Mar 19, 2026
455e057
feat: add FDv2DataManagerBase for mode switching and data source life…
kinyoklion Mar 19, 2026
20f3d28
Merge branch 'main' into rlamb/fdv2-source-factory-provider
kinyoklion Mar 20, 2026
a908f6f
feat: wire FDv2 data manager into BrowserClient
kinyoklion Mar 20, 2026
97568e4
fix: address PR review feedback on SourceFactoryProvider
kinyoklion Mar 20, 2026
25121e3
Merge branch 'rlamb/fdv2-source-factory-provider' into rlamb/fdv2-dat…
kinyoklion Mar 20, 2026
b1ca6c1
fix: update FDv2DataManagerBase for restructured SourceFactoryContext
kinyoklion Mar 20, 2026
de39213
fix: increase bundle size limits for common and sdk-client packages
kinyoklion Mar 20, 2026
d9711c4
Merge branch 'rlamb/fdv2-source-factory-provider' into rlamb/fdv2-dat…
kinyoklion Mar 20, 2026
2acb9ab
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
a343b8b
fix: add applyChanges to FlagUpdater/FlagStore to bypass FDv1 version…
kinyoklion Mar 20, 2026
a076479
Merge branch 'rlamb/fdv2-flag-manager-apply-changes' into rlamb/fdv2-…
kinyoklion Mar 20, 2026
90e6af6
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
95dabe4
Merge remote-tracking branch 'origin/main' into rlamb/fdv2-flag-manag…
kinyoklion Mar 20, 2026
83cb188
Merge remote-tracking branch 'origin/main' into rlamb/fdv2-data-manag…
kinyoklion Mar 20, 2026
9887de7
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
80852ff
fix: increase browser SDK bundle size limit
kinyoklion Mar 20, 2026
01bb153
Merge branch 'rlamb/fdv2-flag-manager-apply-changes' into rlamb/fdv2-…
kinyoklion Mar 20, 2026
b434d8f
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
eeea559
fix: delegate full applyChanges to init to avoid duplication
kinyoklion Mar 20, 2026
806b12a
Merge branch 'rlamb/fdv2-flag-manager-apply-changes' into rlamb/fdv2-…
kinyoklion Mar 20, 2026
4c4b9d0
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
b5e5dc3
fix: unify applyChanges at FlagStore layer for future FDv1 removal
kinyoklion Mar 20, 2026
fece197
Merge branch 'rlamb/fdv2-flag-manager-apply-changes' into rlamb/fdv2-…
kinyoklion Mar 20, 2026
87465f8
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 20, 2026
a78c26e
Merge remote-tracking branch 'origin/main' into rlamb/fdv2-data-manag…
kinyoklion Mar 20, 2026
3fa35fd
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 21, 2026
ecc301d
feat: refactor mode switching to discriminated union with foregroundC…
kinyoklion Mar 23, 2026
de58921
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 23, 2026
8ac0234
fix: derive initialForegroundMode from ManualModeSwitching in Browser…
kinyoklion Mar 23, 2026
76f795c
fix: add ManualModeSwitching to LDClientDataSystemOptions union type
kinyoklion Mar 23, 2026
b360da4
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 23, 2026
a39bdb4
fix: rename initialForegroundMode to foregroundMode
kinyoklion Mar 24, 2026
a96d111
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 24, 2026
2fecbc5
fix: rename initialForegroundMode to foregroundMode in BrowserClient
kinyoklion Mar 24, 2026
a9ac466
fix: simplify dataCallback by using ?? [] for transfer-none updates
kinyoklion Mar 24, 2026
09f0a3b
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 24, 2026
659ff40
fix: unconditionally assign selector from payload state
kinyoklion Mar 24, 2026
77f5077
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 24, 2026
9ce4858
fix: revert combined-browser size limit decrease
kinyoklion Mar 24, 2026
5f4f9f9
fix: extract resolveForegroundMode helper with type guard
kinyoklion Mar 24, 2026
63a0ac9
fix: remove unnecessary as any cast for bootstrap check
kinyoklion Mar 25, 2026
86f3f1d
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
ad9a304
fix: re-check closed state after await in identify to prevent resourc…
kinyoklion Mar 25, 2026
bcd670a
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
68b4baf
fix: update doc comments to match discriminated union API
kinyoklion Mar 25, 2026
f536b54
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
5233983
feat: add setConnectionMode as top-level override bypassing transitio…
kinyoklion Mar 25, 2026
1702f8d
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
f91a28f
fix: add .catch to flagManager.applyChanges to prevent unhandled reje…
kinyoklion Mar 25, 2026
2e9bc71
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
0786b64
feat: add custom is predicate to validatorOf for discriminated unions
kinyoklion Mar 25, 2026
273af24
Merge branch 'rlamb/fdv2-data-manager-base' into rlamb/fdv2-browser-c…
kinyoklion Mar 25, 2026
0ac3c0d
fix: forward browser streaming option to FDv2 data manager
kinyoklion Mar 25, 2026
04afb4e
Merge branch 'main' into rlamb/fdv2-browser-client-integration
kinyoklion Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions packages/sdk/browser/__tests__/BrowserClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,4 +872,72 @@ describe('given a mock platform for a BrowserClient', () => {
// Verify that no fetch calls were made
expect(platform.requests.fetch.mock.calls.length).toBe(0);
});

it('uses FDv1 endpoints when dataSystem is not set', async () => {
const client = makeClient(
'client-side-id',
{ key: 'user-key', kind: 'user' },
AutoEnvAttributes.Disabled,
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
platform,
);

await client.start();

const fetchUrl = platform.requests.fetch.mock.calls[0][0];
expect(fetchUrl).toContain('/sdk/evalx/');
expect(fetchUrl).not.toContain('/sdk/poll/eval');
});

it('uses FDv2 endpoints when dataSystem is set', async () => {
const client = makeClient(
'client-side-id',
{ key: 'user-key', kind: 'user' },
AutoEnvAttributes.Disabled,
{
streaming: false,
logger,
diagnosticOptOut: true,
sendEvents: false,
fetchGoals: false,
// @ts-ignore dataSystem is @internal
dataSystem: {},
},
platform,
);

await client.start();

const fetchUrl = platform.requests.fetch.mock.calls[0][0];
expect(fetchUrl).toContain('/sdk/poll/eval/');
});

it('validates dataSystem options and applies browser defaults', async () => {
const client = makeClient(
'client-side-id',
{ key: 'user-key', kind: 'user' },
AutoEnvAttributes.Disabled,
{
streaming: false,
logger,
diagnosticOptOut: true,
sendEvents: false,
fetchGoals: false,
// @ts-ignore dataSystem is @internal
dataSystem: { backgroundConnectionMode: 'invalid-mode' },
},
platform,
);

// Invalid mode should produce a warning
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('dataSystem.backgroundConnectionMode'),
);

await client.start();

// Should still use FDv2 — invalid sub-fields fall back to defaults, not disable FDv2
const fetchUrl = platform.requests.fetch.mock.calls[0][0];
expect(fetchUrl).toContain('/sdk/poll/eval/');
});
});
132 changes: 83 additions & 49 deletions packages/sdk/browser/src/BrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import {
AutoEnvAttributes,
BasicLogger,
BROWSER_DATA_SYSTEM_DEFAULTS,
BROWSER_TRANSITION_TABLE,
browserFdv1Endpoints,
Configuration,
createDefaultSourceFactoryProvider,
createFDv2DataManagerBase,
FlagManager,
Hook,
internal,
LDIdentifyOptions as LDBaseIdentifyOptions,
LDClientImpl,
LDContext,
LDEmitter,
Expand All @@ -17,8 +21,10 @@ import {
LDPluginEnvironmentMetadata,
LDWaitForInitializationOptions,
LDWaitForInitializationResult,
MODE_TABLE,
Platform,
readFlagsFromBootstrap,
resolveForegroundMode,
safeRegisterDebugOverridePlugins,
} from '@launchdarkly/js-client-sdk-common';

Expand Down Expand Up @@ -78,57 +84,89 @@ class BrowserClientImpl extends LDClientImpl {
const { eventUrlTransformer } = validatedBrowserOptions;
const endpoints = browserFdv1Endpoints(clientSideId);

super(
clientSideId,
autoEnvAttributes,
platform,
baseOptionsWithDefaults,
(
flagManager: FlagManager,
configuration: Configuration,
baseHeaders: LDHeaders,
emitter: LDEmitter,
diagnosticsManager?: internal.DiagnosticsManager,
) =>
new BrowserDataManager(
const dataManagerFactory = (
flagManager: FlagManager,
configuration: Configuration,
baseHeaders: LDHeaders,
emitter: LDEmitter,
diagnosticsManager?: internal.DiagnosticsManager,
) => {
if (configuration.dataSystem) {
return createFDv2DataManagerBase({
platform,
flagManager,
clientSideId,
configuration,
validatedBrowserOptions,
endpoints.polling,
endpoints.streaming,
credential: clientSideId,
config: configuration,
baseHeaders,
emitter,
diagnosticsManager,
),
{
// This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js
getLegacyStorageKeys: () =>
getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)),
analyticsEventPath: `/events/bulk/${clientSideId}`,
diagnosticEventPath: `/events/diagnostic/${clientSideId}`,
includeAuthorizationHeader: false,
highTimeoutThreshold: 5,
userAgentHeaderName: 'x-launchdarkly-user-agent',
dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS,
trackEventModifier: (event: internal.InputCustomEvent) =>
new internal.InputCustomEvent(
event.context,
event.key,
event.data,
event.metricValue,
event.samplingRatio,
eventUrlTransformer(getHref()),
transitionTable: BROWSER_TRANSITION_TABLE,
foregroundMode: resolveForegroundMode(
configuration.dataSystem,
BROWSER_DATA_SYSTEM_DEFAULTS,
),
getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) =>
internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins),
credentialType: 'clientSideId',
},
);
backgroundMode: undefined,
modeTable: MODE_TABLE,
sourceFactoryProvider: createDefaultSourceFactoryProvider(),
fdv1Endpoints: browserFdv1Endpoints(clientSideId),
buildQueryParams: (identifyOptions?: LDBaseIdentifyOptions) => {
const params: { key: string; value: string }[] = [{ key: 'auth', value: clientSideId }];
const browserOpts = identifyOptions as LDIdentifyOptions | undefined;
if (browserOpts?.hash) {
params.push({ key: 'h', value: browserOpts.hash });
}
return params;
},
});
}

return new BrowserDataManager(
platform,
flagManager,
clientSideId,
configuration,
validatedBrowserOptions,
endpoints.polling,
endpoints.streaming,
baseHeaders,
emitter,
diagnosticsManager,
);
};

super(clientSideId, autoEnvAttributes, platform, baseOptionsWithDefaults, dataManagerFactory, {
// This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js
getLegacyStorageKeys: () =>
getAllStorageKeys().filter((key) => key.startsWith(`ld:${clientSideId}:`)),
analyticsEventPath: `/events/bulk/${clientSideId}`,
diagnosticEventPath: `/events/diagnostic/${clientSideId}`,
includeAuthorizationHeader: false,
highTimeoutThreshold: 5,
userAgentHeaderName: 'x-launchdarkly-user-agent',
dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS,
trackEventModifier: (event: internal.InputCustomEvent) =>
new internal.InputCustomEvent(
event.context,
event.key,
event.data,
event.metricValue,
event.samplingRatio,
eventUrlTransformer(getHref()),
),
getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) =>
internal.safeGetHooks(logger, environmentMetadata, validatedBrowserOptions.plugins),
credentialType: 'clientSideId',
});

this.setEventSendingEnabled(true, false);

// Forward the browser streaming option to the FDv2 data manager so that
// an explicit streaming: false prevents auto-promotion to streaming.
if (validatedBrowserOptions.streaming !== undefined) {
this.dataManager.setForcedStreaming?.(validatedBrowserOptions.streaming);
}

this.dataManager.setFlushCallback?.(() => this.flush());
Comment thread
cursor[bot] marked this conversation as resolved.

this._plugins = validatedBrowserOptions.plugins;

if (validatedBrowserOptions.fetchGoals) {
Expand Down Expand Up @@ -281,18 +319,14 @@ class BrowserClientImpl extends LDClientImpl {
}

setStreaming(streaming?: boolean): void {
// With FDv2 we may want to consider if we support connection mode directly.
// Maybe with an extension to connection mode for 'automatic'.
const browserDataManager = this.dataManager as BrowserDataManager;
browserDataManager.setForcedStreaming(streaming);
this.dataManager.setForcedStreaming?.(streaming);
}

private _updateAutomaticStreamingState() {
const browserDataManager = this.dataManager as BrowserDataManager;
const hasListeners = this.emitter
.eventNames()
.some((name) => name.startsWith('change:') || name === 'change');
browserDataManager.setAutomaticStreamingState(hasListeners);
this.dataManager.setAutomaticStreamingState?.(hasListeners);
}

override on(eventName: LDEmitterEventName, listener: Function): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { TypeValidators } from '@launchdarkly/js-sdk-common';

import type { PlatformDataSystemDefaults } from '../api/datasource';
import type FDv2ConnectionMode from '../api/datasource/FDv2ConnectionMode';
import type {
LDClientDataSystemOptions,
ManualModeSwitching,
PlatformDataSystemDefaults,
} from '../api/datasource/LDClientDataSystemOptions';
import { anyOf, validatorOf } from '../configuration/validateOptions';
import { connectionModesValidator, connectionModeValidator } from './ConnectionModeConfig';

Expand Down Expand Up @@ -56,8 +61,30 @@ const DESKTOP_DATA_SYSTEM_DEFAULTS: PlatformDataSystemDefaults = {
automaticModeSwitching: false,
};

function isManualModeSwitching(
value: LDClientDataSystemOptions['automaticModeSwitching'],
): value is ManualModeSwitching {
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual';
}
Comment thread
cursor[bot] marked this conversation as resolved.

/**
* Resolve the foreground connection mode from a validated data system config
* and platform defaults. Uses the mode from `ManualModeSwitching` when present,
* otherwise falls back to the platform default.
*/
function resolveForegroundMode(
dataSystem: LDClientDataSystemOptions,
defaults: PlatformDataSystemDefaults,
): FDv2ConnectionMode {
if (isManualModeSwitching(dataSystem.automaticModeSwitching)) {
return dataSystem.automaticModeSwitching.initialConnectionMode;
}
return defaults.foregroundConnectionMode;
}

export {
dataSystemValidators,
resolveForegroundMode,
BROWSER_DATA_SYSTEM_DEFAULTS,
MOBILE_DATA_SYSTEM_DEFAULTS,
DESKTOP_DATA_SYSTEM_DEFAULTS,
Expand Down
1 change: 1 addition & 0 deletions packages/shared/sdk-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type { DataSourceStatusManager } from './datasource/DataSourceStatusManag
// FDv2 data system validators and platform defaults.
export {
dataSystemValidators,
resolveForegroundMode,
BROWSER_DATA_SYSTEM_DEFAULTS,
MOBILE_DATA_SYSTEM_DEFAULTS,
DESKTOP_DATA_SYSTEM_DEFAULTS,
Expand Down
Loading