Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
46 changes: 20 additions & 26 deletions packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand All @@ -21,6 +21,8 @@ const mockLDClient = {
allFlags: vi.fn().mockReturnValue(rawFlags),
variation: vi.fn((_, defaultValue) => defaultValue),
identify: vi.fn(),
start: vi.fn(),
Comment thread
cursor[bot] marked this conversation as resolved.
waitForInitialization: vi.fn().mockReturnValue(Promise.resolve({ status: 'complete' })),
};

describe('launchDarkly', () => {
Expand All @@ -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');
});
Expand All @@ -40,8 +42,7 @@ describe('launchDarkly', () => {
const ld = LD;

beforeEach(() => {
// mocks the initialize function to return the mockLDClient
(initialize as Mock<typeof initialize>).mockReturnValue(
(createClient as Mock<typeof createClient>).mockReturnValue(
mockLDClient as unknown as LDClient,
);
});
Expand All @@ -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);
Expand All @@ -104,8 +103,7 @@ describe('launchDarkly', () => {
const ld = LD;

beforeEach(() => {
// mocks the initialize function to return the mockLDClient
(initialize as Mock<typeof initialize>).mockReturnValue(
Comment thread
cursor[bot] marked this conversation as resolved.
(createClient as Mock<typeof createClient>).mockReturnValue(
mockLDClient as unknown as LDClient,
);
});
Expand All @@ -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`
Expand Down Expand Up @@ -166,8 +162,7 @@ describe('launchDarkly', () => {
const ld = LD;

beforeEach(() => {
// mocks the initialize function to return the mockLDClient
(initialize as Mock<typeof initialize>).mockReturnValue(
(createClient as Mock<typeof createClient>).mockReturnValue(
mockLDClient as unknown as LDClient,
);
});
Expand All @@ -191,8 +186,7 @@ describe('launchDarkly', () => {
const ld = LD;

beforeEach(() => {
// mocks the initialize function to return the mockLDClient
(initialize as Mock<typeof initialize>).mockReturnValue(
(createClient as Mock<typeof createClient>).mockReturnValue(
mockLDClient as unknown as LDClient,
);
});
Expand Down
61 changes: 39 additions & 22 deletions packages/sdk/svelte/src/lib/client/SvelteLDClient.ts
Original file line number Diff line number Diff line change
@@ -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,
createClient as createClientSdk,
type LDClient,
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 };

Expand Down Expand Up @@ -62,43 +64,58 @@ function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags {
* Creates a LaunchDarkly instance.
* @returns {Object} The LaunchDarkly instance object.
*/
function createLD() {
function init() {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
let coreLdClient: LDClient | undefined;
const loading = writable(true);
const flagsWritable = writable<LDFlags>({});
const initializeResult = writable<string>('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);
function initialize(
clientId: LDClientID,
context: LDContext,
options?: LDOptions,
): Promise<void> {
coreLdClient = createClientSdk(clientId, context, options);

coreLdClient.on('change', () => {
const rawFlags = coreLdClient!.allFlags();
const allFlags = toFlagsProxy(coreLdClient!, rawFlags);
flagsWritable.set(allFlags);
});

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();

Comment thread
cursor[bot] marked this conversation as resolved.
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');
});
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
joker23 marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
}

/**
* Identifies the user context.
* @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<LDIdentifyResult> {
isClientInitialized(coreLdClient);
return coreLdClient.identify(context);
}
Expand All @@ -125,12 +142,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();
6 changes: 4 additions & 2 deletions packages/sdk/svelte/src/lib/provider/LDProvider.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@

export let clientID: LDClientID;
export let context: LDContext;
const { initialize, initializing } = LD;
const { initialize, initalizationState } = LD;

onMount(() => {
initialize(clientID, context);
});
</script>

{#if $$slots.initializing && $initializing}
{#if $$slots.initializing && $initalizationState === 'pending'}
<slot name="initializing">Loading flags (default loading slot value)...</slot>
{:else if $initalizationState === 'failed' || $initalizationState === 'timeout'}
<slot name="failed">Failed to initialize LaunchDarkly client ({$initalizationState})</slot>
{:else}
<slot />
{/if}