diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index ea689d6eda..760ee6cc13 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,8 @@ const mockLDClient = { allFlags: vi.fn().mockReturnValue(rawFlags), variation: vi.fn((_, defaultValue) => defaultValue), identify: vi.fn(), + start: vi.fn(), + waitForInitialization: vi.fn().mockReturnValue(Promise.resolve({ status: 'complete' })), }; describe('launchDarkly', () => { @@ -31,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'); }); @@ -40,8 +42,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, ); }); @@ -62,27 +63,25 @@ 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('ready'); + expect(get(initalizationState)).toBe('pending'); - expect(get(initializing)).toBe(false); + await promise; + expect(get(initalizationState)).toBe('complete'); }); 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', () => { + 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('ready'); + await ld.initialize(clientSideID, mockContext); expect(allFlagsSpy).toHaveBeenCalledOnce(); expect(allFlagsSpy).toHaveReturnedWith(newFlags); @@ -104,8 +103,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, ); }); @@ -124,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('ready'); - + 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` @@ -166,8 +162,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 +186,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/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 21fc32ffb5..0c67f5cdd4 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,12 +1,14 @@ import { derived, type Readable, readonly, writable, type Writable } from 'svelte/store'; -import type { LDFlagSet } from '@launchdarkly/js-client-sdk'; import { - initialize, - type LDClient, + createClient, + type LDClient as LDClientBase, type LDContext, + type LDFlagSet, type LDFlagValue, -} from '@launchdarkly/js-client-sdk/compat'; + type LDIdentifyResult, + type LDOptions, +} from '@launchdarkly/js-client-sdk'; export type { LDContext, LDFlagValue }; @@ -16,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'); } @@ -37,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); @@ -62,35 +110,50 @@ function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { * Creates a LaunchDarkly instance. * @returns {Object} The LaunchDarkly instance object. */ -function createLD() { - let coreLdClient: LDClient | undefined; - const loading = writable(true); +function init(): LDClient { + let coreLdClient: LDClientBase | undefined; 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 LDInitialize(clientId: LDClientID, context: LDContext) { - coreLdClient = initialize(clientId, context); - coreLdClient!.on('ready', () => { - loading.set(false); - const rawFlags = coreLdClient!.allFlags(); - const allFlags = toFlagsProxy(coreLdClient!, rawFlags); - flagsWritable.set(allFlags); - }); - - coreLdClient!.on('change', () => { + function initialize( + clientId: LDClientID, + context: LDContext, + options?: LDOptions, + ): Promise { + coreLdClient = createClient(clientId, context, options); + + coreLdClient.on('change', () => { const rawFlags = coreLdClient!.allFlags(); const allFlags = toFlagsProxy(coreLdClient!, rawFlags); flagsWritable.set(allFlags); }); - 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. + coreLdClient.start(); + + return coreLdClient + .waitForInitialization() + .then((result) => { + const rawFlags = coreLdClient!.allFlags(); + const allFlags = toFlagsProxy(coreLdClient!, rawFlags); + flagsWritable.set(allFlags); + + 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'); + }); } /** @@ -98,7 +161,7 @@ function createLD() { * @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); } @@ -125,12 +188,12 @@ function createLD() { return { identify, flags: readonly(flagsWritable), - initialize: LDInitialize, - initializing: readonly(loading), + initialize, + initalizationState: readonly(initializeResult), watch, useFlag, }; } /** The LaunchDarkly instance */ -export const LD = createLD(); +export const LD = init(); diff --git a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte index d5a53be7fe..92c62de6d2 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 if $initalizationState === 'failed' || $initalizationState === 'timeout'} + Failed to initialize LaunchDarkly client ({$initalizationState}) {:else} {/if}