From e053681aa86a5de5aef25cba2eafa4e1d2b858c7 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Fri, 19 Dec 2025 10:21:13 -0600 Subject: [PATCH 1/4] chore: update svelte sdk to use browser 4.x - removes the browser compat dependency on svelte sdk --- .../lib/client/SvelteLDClient.test.ts | 25 ++++++++----------- .../svelte/src/lib/client/SvelteLDClient.ts | 23 +++++++++-------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index ea689d6eda..607fb54fd1 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -2,11 +2,11 @@ import { EventEmitter } from 'node:events'; import { get } from 'svelte/store'; import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; -import { initialize, LDClient } from '@launchdarkly/js-client-sdk/compat'; +import { createClient, LDClient } from '@launchdarkly/js-client-sdk'; import { LD } from '../../../src/lib/client/SvelteLDClient'; -vi.mock('@launchdarkly/js-client-sdk/compat', { spy: true }); +vi.mock('@launchdarkly/js-client-sdk', { spy: true }); const clientSideID = 'test-client-side-id'; const rawFlags = { 'test-flag': true, 'another-test-flag': 'flag-value' }; @@ -21,6 +21,7 @@ const mockLDClient = { allFlags: vi.fn().mockReturnValue(rawFlags), variation: vi.fn((_, defaultValue) => defaultValue), identify: vi.fn(), + start: vi.fn(), }; describe('launchDarkly', () => { @@ -40,8 +41,7 @@ describe('launchDarkly', () => { const ld = LD; beforeEach(() => { - // mocks the initialize function to return the mockLDClient - (initialize as Mock).mockReturnValue( + (createClient as Mock).mockReturnValue( mockLDClient as unknown as LDClient, ); }); @@ -66,7 +66,7 @@ describe('launchDarkly', () => { ld.initialize(clientSideID, mockContext); expect(get(initializing)).toBe(true); // should be true before the ready event is emitted - mockLDEventEmitter.emit('ready'); + mockLDEventEmitter.emit('initialized'); expect(get(initializing)).toBe(false); }); @@ -74,7 +74,7 @@ describe('launchDarkly', () => { it('should initialize the LaunchDarkly SDK instance', () => { ld.initialize(clientSideID, mockContext); - expect(initialize).toHaveBeenCalledWith('test-client-side-id', mockContext); + expect(createClient).toHaveBeenCalledWith('test-client-side-id', mockContext, undefined); }); it('should register function that gets flag values when client is ready', () => { @@ -82,7 +82,7 @@ describe('launchDarkly', () => { const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags); ld.initialize(clientSideID, mockContext); - mockLDEventEmitter.emit('ready'); + mockLDEventEmitter.emit('initialized'); expect(allFlagsSpy).toHaveBeenCalledOnce(); expect(allFlagsSpy).toHaveReturnedWith(newFlags); @@ -104,8 +104,7 @@ describe('launchDarkly', () => { const ld = LD; beforeEach(() => { - // mocks the initialize function to return the mockLDClient - (initialize as Mock).mockReturnValue( + (createClient as Mock).mockReturnValue( mockLDClient as unknown as LDClient, ); }); @@ -132,7 +131,7 @@ describe('launchDarkly', () => { const flagStore2 = ld.watch(stringFlagKey); // emit ready event to set initial flag values - mockLDEventEmitter.emit('ready'); + mockLDEventEmitter.emit('initialized'); // 'test-flag' initial value is true according to `rawFlags` expect(get(flagStore)).toBe(true); @@ -166,8 +165,7 @@ describe('launchDarkly', () => { const ld = LD; beforeEach(() => { - // mocks the initialize function to return the mockLDClient - (initialize as Mock).mockReturnValue( + (createClient as Mock).mockReturnValue( mockLDClient as unknown as LDClient, ); }); @@ -191,8 +189,7 @@ describe('launchDarkly', () => { const ld = LD; beforeEach(() => { - // mocks the initialize function to return the mockLDClient - (initialize as Mock).mockReturnValue( + (createClient as Mock).mockReturnValue( mockLDClient as unknown as LDClient, ); }); diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index 21fc32ffb5..83a4e849a9 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,12 +1,13 @@ import { derived, type Readable, readonly, writable, type Writable } from 'svelte/store'; -import type { LDFlagSet } from '@launchdarkly/js-client-sdk'; import { - initialize, + createClient as createClientSdk, type LDClient, type LDContext, + type LDFlagSet, type LDFlagValue, -} from '@launchdarkly/js-client-sdk/compat'; + type LDOptions, +} from '@launchdarkly/js-client-sdk'; export type { LDContext, LDFlagValue }; @@ -62,7 +63,7 @@ function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { * Creates a LaunchDarkly instance. * @returns {Object} The LaunchDarkly instance object. */ -function createLD() { +function init() { let coreLdClient: LDClient | undefined; const loading = writable(true); const flagsWritable = writable({}); @@ -73,21 +74,23 @@ function createLD() { * @param {LDContext} context - The user context. * @returns {Object} An object with the initialization status store. */ - function LDInitialize(clientId: LDClientID, context: LDContext) { - coreLdClient = initialize(clientId, context); - coreLdClient!.on('ready', () => { + function initialize(clientId: LDClientID, context: LDContext, options?: LDOptions) { + coreLdClient = createClientSdk(clientId, context, options); + coreLdClient.on('initialized', () => { loading.set(false); const rawFlags = coreLdClient!.allFlags(); const allFlags = toFlagsProxy(coreLdClient!, rawFlags); flagsWritable.set(allFlags); }); - coreLdClient!.on('change', () => { + coreLdClient.on('change', () => { const rawFlags = coreLdClient!.allFlags(); const allFlags = toFlagsProxy(coreLdClient!, rawFlags); flagsWritable.set(allFlags); }); + coreLdClient.start(); + return { initializing: loading, }; @@ -125,7 +128,7 @@ function createLD() { return { identify, flags: readonly(flagsWritable), - initialize: LDInitialize, + initialize, initializing: readonly(loading), watch, useFlag, @@ -133,4 +136,4 @@ function createLD() { } /** The LaunchDarkly instance */ -export const LD = createLD(); +export const LD = init(); From ada845db915bc02ee24ccd4b21a7d9fcbcef9872 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Tue, 6 Jan 2026 12:05:32 -0600 Subject: [PATCH 2/4] chore: addressing PR comments --- .../sdk/svelte/example/{.env => .env.example} | 0 .../svelte/src/lib/client/SvelteLDClient.ts | 27 ++++++++++--------- .../svelte/src/lib/provider/LDProvider.svelte | 8 +++--- 3 files changed, 19 insertions(+), 16 deletions(-) rename packages/sdk/svelte/example/{.env => .env.example} (100%) diff --git a/packages/sdk/svelte/example/.env b/packages/sdk/svelte/example/.env.example similarity index 100% rename from packages/sdk/svelte/example/.env rename to packages/sdk/svelte/example/.env.example diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index 83a4e849a9..c1851b0a4a 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -65,8 +65,8 @@ function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { */ function init() { let coreLdClient: LDClient | undefined; - const loading = writable(true); const flagsWritable = writable({}); + const initializeResult = writable('pending'); /** * Initializes the LaunchDarkly client. @@ -76,12 +76,6 @@ function init() { */ function initialize(clientId: LDClientID, context: LDContext, options?: LDOptions) { coreLdClient = createClientSdk(clientId, context, options); - coreLdClient.on('initialized', () => { - loading.set(false); - const rawFlags = coreLdClient!.allFlags(); - const allFlags = toFlagsProxy(coreLdClient!, rawFlags); - flagsWritable.set(allFlags); - }); coreLdClient.on('change', () => { const rawFlags = coreLdClient!.allFlags(); @@ -89,11 +83,18 @@ function init() { flagsWritable.set(allFlags); }); - coreLdClient.start(); - - return { - initializing: loading, - }; + // TODO: currently all options are defaulted which means that the client initailization will timeout in 5 seconds. + // we will need to address this before this SDK is marked as stable. + void coreLdClient.start(); + void coreLdClient.waitForInitialization() + .then((result) => { + initializeResult.set(result.status); + }) + .catch(() => { + // NOTE: this should never happen as we don't throw errors from initialization. + options?.logger?.error('Failed to initialize LaunchDarkly client'); + initializeResult.set('failed'); + }); } /** @@ -129,7 +130,7 @@ function init() { identify, flags: readonly(flagsWritable), initialize, - initializing: readonly(loading), + initalizationState: readonly(initializeResult), watch, useFlag, }; diff --git a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte index d5a53be7fe..d4392d6f8a 100644 --- a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte +++ b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte @@ -5,15 +5,17 @@ export let clientID: LDClientID; export let context: LDContext; - const { initialize, initializing } = LD; + const { initialize, initalizationState } = LD; onMount(() => { initialize(clientID, context); }); -{#if $$slots.initializing && $initializing} +{#if $$slots.initializing && $initalizationState === 'pending'} Loading flags (default loading slot value)... -{:else} +{:else if $initalizationState === 'complete'} +{:else} + Failed to initialize LaunchDarkly client ({$initalizationState}) {/if} From 34672de80128fa9197fc7f93d7344590ea9d1b5a Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Tue, 6 Jan 2026 12:41:49 -0600 Subject: [PATCH 3/4] chore: addressing additional comments --- .../lib/client/SvelteLDClient.test.ts | 27 +++++++++---------- .../svelte/src/lib/client/SvelteLDClient.ts | 21 ++++++++++++--- .../svelte/src/lib/provider/LDProvider.svelte | 6 ++--- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index 607fb54fd1..760ee6cc13 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -22,6 +22,7 @@ const mockLDClient = { variation: vi.fn((_, defaultValue) => defaultValue), identify: vi.fn(), start: vi.fn(), + waitForInitialization: vi.fn().mockReturnValue(Promise.resolve({ status: 'complete' })), }; describe('launchDarkly', () => { @@ -32,7 +33,7 @@ describe('launchDarkly', () => { expect(ld).toHaveProperty('identify'); expect(ld).toHaveProperty('flags'); expect(ld).toHaveProperty('initialize'); - expect(ld).toHaveProperty('initializing'); + expect(ld).toHaveProperty('initalizationState'); expect(ld).toHaveProperty('watch'); expect(ld).toHaveProperty('useFlag'); }); @@ -62,13 +63,13 @@ describe('launchDarkly', () => { }); it('should set the loading status to false when the client is ready', async () => { - const { initializing } = ld; - ld.initialize(clientSideID, mockContext); + const { initalizationState } = ld; + const promise = ld.initialize(clientSideID, mockContext); - expect(get(initializing)).toBe(true); // should be true before the ready event is emitted - mockLDEventEmitter.emit('initialized'); + expect(get(initalizationState)).toBe('pending'); - expect(get(initializing)).toBe(false); + await promise; + expect(get(initalizationState)).toBe('complete'); }); it('should initialize the LaunchDarkly SDK instance', () => { @@ -77,12 +78,10 @@ describe('launchDarkly', () => { expect(createClient).toHaveBeenCalledWith('test-client-side-id', mockContext, undefined); }); - it('should register function that gets flag values when client is ready', () => { + it('should register function that gets flag values when client is ready', async () => { const newFlags = { ...rawFlags, 'new-flag': true }; const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags); - - ld.initialize(clientSideID, mockContext); - mockLDEventEmitter.emit('initialized'); + await ld.initialize(clientSideID, mockContext); expect(allFlagsSpy).toHaveBeenCalledOnce(); expect(allFlagsSpy).toHaveReturnedWith(newFlags); @@ -123,16 +122,14 @@ describe('launchDarkly', () => { expect(get(flagStore)).toBe(true); }); - it('should update the flag store when the flag value changes', () => { + it('should update the flag store when the flag value changes', async () => { const booleanFlagKey = 'test-flag'; const stringFlagKey = 'another-test-flag'; - ld.initialize(clientSideID, mockContext); + const initializationPromise = ld.initialize(clientSideID, mockContext); const flagStore = ld.watch(booleanFlagKey); const flagStore2 = ld.watch(stringFlagKey); - // emit ready event to set initial flag values - mockLDEventEmitter.emit('initialized'); - + await initializationPromise; // 'test-flag' initial value is true according to `rawFlags` expect(get(flagStore)).toBe(true); // 'another-test-flag' intial value is 'flag-value' according to `rawFlags` diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index c1851b0a4a..d7ddeba7f3 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -6,6 +6,7 @@ import { type LDContext, type LDFlagSet, type LDFlagValue, + type LDIdentifyResult, type LDOptions, } from '@launchdarkly/js-client-sdk'; @@ -68,13 +69,19 @@ function init() { const flagsWritable = writable({}); const initializeResult = writable('pending'); + // NOTE: we will returns an empty promise for now as the promise states and handling is being wrappered + // we can evaluate this decision in the future before this SDK is marked as stable. /** * Initializes the LaunchDarkly client. * @param {LDClientID} clientId - The client ID. * @param {LDContext} context - The user context. * @returns {Object} An object with the initialization status store. */ - function initialize(clientId: LDClientID, context: LDContext, options?: LDOptions) { + function initialize( + clientId: LDClientID, + context: LDContext, + options?: LDOptions, + ): Promise { coreLdClient = createClientSdk(clientId, context, options); coreLdClient.on('change', () => { @@ -85,9 +92,15 @@ function init() { // TODO: currently all options are defaulted which means that the client initailization will timeout in 5 seconds. // we will need to address this before this SDK is marked as stable. - void coreLdClient.start(); - void coreLdClient.waitForInitialization() + coreLdClient.start(); + + return coreLdClient + .waitForInitialization() .then((result) => { + const rawFlags = coreLdClient!.allFlags(); + const allFlags = toFlagsProxy(coreLdClient!, rawFlags); + flagsWritable.set(allFlags); + initializeResult.set(result.status); }) .catch(() => { @@ -102,7 +115,7 @@ function init() { * @param {LDContext} context - The user context. * @returns {Promise} A promise that resolves when the user is identified. */ - async function identify(context: LDContext) { + async function identify(context: LDContext): Promise { isClientInitialized(coreLdClient); return coreLdClient.identify(context); } diff --git a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte index d4392d6f8a..92c62de6d2 100644 --- a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte +++ b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte @@ -14,8 +14,8 @@ {#if $$slots.initializing && $initalizationState === 'pending'} Loading flags (default loading slot value)... -{:else if $initalizationState === 'complete'} - +{:else if $initalizationState === 'failed' || $initalizationState === 'timeout'} + Failed to initialize LaunchDarkly client ({$initalizationState}) {:else} - Failed to initialize LaunchDarkly client ({$initalizationState}) + {/if} From 02ceefe712cd58278f2104e9c4195ca47e068898 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Thu, 8 Jan 2026 10:04:11 -0600 Subject: [PATCH 4/4] chore: addressing PR comments --- .../svelte/src/lib/client/SvelteLDClient.ts | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index d7ddeba7f3..0c67f5cdd4 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,8 +1,8 @@ import { derived, type Readable, readonly, writable, type Writable } from 'svelte/store'; import { - createClient as createClientSdk, - type LDClient, + createClient, + type LDClient as LDClientBase, type LDContext, type LDFlagSet, type LDFlagValue, @@ -18,12 +18,58 @@ export type LDClientID = string; /** Flags for LaunchDarkly */ export type LDFlags = LDFlagSet; +/** + * The LaunchDarkly client interface for Svelte which is a restrictive proxy of the {@link LDClientBase}. + */ +export interface LDClient { + /** + * Initializes the LaunchDarkly client. + * @param {LDClientID} clientId - The LD client-side ID. + * @param {LDContext} context - The user context. + * @param {LDOptions} options - The options. + * @returns {Promise} A promise that resolves when the client is initialized. + */ + initialize(clientId: LDClientID, context: LDContext, options?: LDOptions): Promise; + + /** + * Identifies the user context. + * @param {LDContext} context - The user context. + * @returns {Promise} A promise that resolves when the user is identified. + */ + identify(context: LDContext): Promise; + + /** + * The flags store. + */ + flags: Readable; + + /** + * The initialization state store. + */ + initalizationState: Readable; + + /** + * Watches a flag for changes. + * @param {string} flagKey - The key of the flag to watch. + * @returns {Readable} A readable store of the flag value. + */ + watch(flagKey: string): Readable; + + /** + * Gets the current value of a flag. + * @param {string} flagKey - The key of the flag to get. + * @param {TFlag} defaultValue - The default value of the flag. + * @returns {TFlag} The current value of the flag. + */ + useFlag(flagKey: string, defaultValue: TFlag): TFlag; +} + /** * Checks if the LaunchDarkly client is initialized. * @param {LDClient | undefined} client - The LaunchDarkly client. * @throws {Error} If the client is not initialized. */ -function isClientInitialized(client: LDClient | undefined): asserts client is LDClient { +function isClientInitialized(client: LDClientBase | undefined): asserts client is LDClientBase { if (!client) { throw new Error('LaunchDarkly client not initialized'); } @@ -39,7 +85,7 @@ function isClientInitialized(client: LDClient | undefined): asserts client is LD * @param flags - The initial flags object to be proxied. * @returns A proxy object that intercepts access to flag values and returns the appropriate variation. */ -function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { +function toFlagsProxy(client: LDClientBase, flags: LDFlags): LDFlags { return new Proxy(flags, { get(target, prop, receiver) { const currentValue = Reflect.get(target, prop, receiver); @@ -64,8 +110,8 @@ function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { * Creates a LaunchDarkly instance. * @returns {Object} The LaunchDarkly instance object. */ -function init() { - let coreLdClient: LDClient | undefined; +function init(): LDClient { + let coreLdClient: LDClientBase | undefined; const flagsWritable = writable({}); const initializeResult = writable('pending'); @@ -82,7 +128,7 @@ function init() { context: LDContext, options?: LDOptions, ): Promise { - coreLdClient = createClientSdk(clientId, context, options); + coreLdClient = createClient(clientId, context, options); coreLdClient.on('change', () => { const rawFlags = coreLdClient!.allFlags();