Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 0 additions & 4 deletions packages/sdk/react-native/src/provider/setupListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ const setupListeners = (
client: ReactNativeLDClient,
setState: Dispatch<SetStateAction<ReactContext>>,
) => {
client.on('ready', () => {
setState({ client });
});
Comment on lines -10 to -12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ready has been deleted in favor of a simpler design relying only on change.


client.on('change', () => {
setState({ client });
});
Expand Down
8 changes: 0 additions & 8 deletions packages/shared/common/src/api/data/LDFlagChangeset.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/shared/common/src/api/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export * from './LDEvaluationDetail';
export * from './LDEvaluationReason';
export * from './LDFlagSet';
export * from './LDFlagValue';
export * from './LDFlagChangeset';
2 changes: 1 addition & 1 deletion packages/shared/mocks/src/streamingProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const setupMockStreamingProcessor = (
errorHandler(unauthorized);
});
} else {
// execute put which will resolve the init promise
// execute put which will resolve the identify promise
process.nextTick(() => listeners.get('put')?.processJson(putResponseJson));

if (patchResponseJson) {
Expand Down
150 changes: 80 additions & 70 deletions packages/shared/sdk-client/src/LDClientImpl.storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { clone, type LDContext, LDFlagChangeset } from '@launchdarkly/js-sdk-common';
import { clone, type LDContext } from '@launchdarkly/js-sdk-common';
import { basicPlatform, logger, setupMockStreamingProcessor } from '@launchdarkly/private-js-mocks';

import LDEmitter from './api/LDEmitter';
import type { DeleteFlag, Flag, Flags, PatchFlag } from './evaluation/fetchFlags';
import * as mockResponseJson from './evaluation/mockResponse.json';
import LDClientImpl from './LDClientImpl';
import { DeleteFlag, Flag, Flags, PatchFlag } from './types';

jest.mock('@launchdarkly/js-sdk-common', () => {
const actual = jest.requireActual('@launchdarkly/js-sdk-common');
Expand All @@ -22,17 +22,18 @@ jest.mock('@launchdarkly/js-sdk-common', () => {
};
});

const defaultPutResponse = mockResponseJson as Flags;
const testSdkKey = 'test-sdk-key';
const context: LDContext = { kind: 'org', key: 'Testy Pizza' };
let ldc: LDClientImpl;
let emitter: LDEmitter;
let defaultPutResponse: Flags;
let defaultFlagKeys: string[];

// Promisify on.change listener so we can await it in tests.
const onChangePromise = () =>
new Promise<LDFlagChangeset>((res) => {
ldc.on('change', (_context: LDContext, changeset: LDFlagChangeset) => {
res(changeset);
new Promise<string[]>((res) => {
ldc.on('change', (_context: LDContext, changes: string[]) => {
res(changes);
});
});

Expand Down Expand Up @@ -69,6 +70,9 @@ const identifyGetAllFlags = async (
describe('sdk-client storage', () => {
beforeEach(() => {
jest.useFakeTimers();
defaultPutResponse = clone<Flags>(mockResponseJson);
defaultFlagKeys = Object.keys(defaultPutResponse);

basicPlatform.storage.get.mockImplementation(() => JSON.stringify(defaultPutResponse));
jest
.spyOn(LDClientImpl.prototype as any, 'createStreamUriPath')
Expand All @@ -93,8 +97,8 @@ describe('sdk-client storage', () => {

// 'change' should not have been emitted
expect(emitter.emit).toHaveBeenCalledTimes(3);
expect(emitter.emit).toHaveBeenNthCalledWith(1, 'initializing', context);
expect(emitter.emit).toHaveBeenNthCalledWith(2, 'ready', context);
expect(emitter.emit).toHaveBeenNthCalledWith(1, 'identifying', context);
expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, defaultFlagKeys);
expect(emitter.emit).toHaveBeenNthCalledWith(
3,
'error',
Expand All @@ -113,6 +117,43 @@ describe('sdk-client storage', () => {
});
});

test('no storage, cold start from streamer', async () => {
// fake previously cached flags even though there's no storage for this context
// @ts-ignore
ldc.flags = defaultPutResponse;
basicPlatform.storage.get.mockImplementation(() => undefined);
setupMockStreamingProcessor(false, defaultPutResponse);

const p = ldc.identify(context);

// I'm not sure why but both runAllTimersAsync and runAllTicks are required
// here for the identify promise be resolved
await jest.runAllTimersAsync();
jest.runAllTicks();
Comment on lines +129 to +132
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a bug in jest fake timers.

await p;

expect(emitter.emit).toHaveBeenCalledTimes(1);
expect(emitter.emit).toHaveBeenNthCalledWith(1, 'identifying', context);
expect(basicPlatform.storage.set).toHaveBeenNthCalledWith(
1,
'org:Testy Pizza',
JSON.stringify(defaultPutResponse),
);
expect(ldc.logger.debug).toHaveBeenCalledWith('Not emitting changes from PUT');

// this is defaultPutResponse
expect(ldc.allFlags()).toEqual({
'dev-test-flag': true,
'easter-i-tunes-special': false,
'easter-specials': 'no specials',
fdsafdsafdsafdsa: true,
'log-level': 'warn',
'moonshot-demo': true,
test1: 's1',
'this-is-a-test': true,
});
});

test('syncing storage when a flag is deleted', async () => {
const putResponse = clone<Flags>(defaultPutResponse);
delete putResponse['dev-test-flag'];
Expand All @@ -123,11 +164,9 @@ describe('sdk-client storage', () => {
'org:Testy Pizza',
JSON.stringify(putResponse),
);
expect(emitter.emit).toHaveBeenNthCalledWith(1, 'initializing', context);
expect(emitter.emit).toHaveBeenNthCalledWith(2, 'ready', context);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': { previous: true },
});
expect(emitter.emit).toHaveBeenNthCalledWith(1, 'identifying', context);
expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, defaultFlagKeys);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['dev-test-flag']);
});

test('syncing storage when a flag is added', async () => {
Expand All @@ -147,9 +186,7 @@ describe('sdk-client storage', () => {
'org:Testy Pizza',
JSON.stringify(putResponse),
);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'another-dev-test-flag': { current: newFlag.value },
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['another-dev-test-flag']);
});

test('syncing storage when a flag is updated', async () => {
Expand All @@ -159,12 +196,7 @@ describe('sdk-client storage', () => {
const allFlags = await identifyGetAllFlags(false, putResponse);

expect(allFlags).toMatchObject({ 'dev-test-flag': false });
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': {
previous: true,
current: putResponse['dev-test-flag'].value,
},
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['dev-test-flag']);
});

test('syncing storage on multiple flag operations', async () => {
Expand All @@ -179,17 +211,14 @@ describe('sdk-client storage', () => {

expect(allFlags).toMatchObject({ 'dev-test-flag': false, 'another-dev-test-flag': true });
expect(allFlags).not.toHaveProperty('moonshot-demo');
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': {
previous: true,
current: putResponse['dev-test-flag'].value,
},
'another-dev-test-flag': { current: newFlag.value },
'moonshot-demo': { previous: true },
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, [
'moonshot-demo',
'dev-test-flag',
'another-dev-test-flag',
]);
});

test('syncing storage when PUT is consistent so no updates needed', async () => {
test('syncing storage when PUT is consistent so no change', async () => {
const allFlags = await identifyGetAllFlags(
false,
defaultPutResponse,
Expand All @@ -198,8 +227,13 @@ describe('sdk-client storage', () => {
false,
);

expect(basicPlatform.storage.set).not.toHaveBeenCalled();
expect(emitter.emit).not.toHaveBeenCalledWith('change');
expect(basicPlatform.storage.set).toHaveBeenNthCalledWith(
1,
'org:Testy Pizza',
JSON.stringify(defaultPutResponse),
);
expect(emitter.emit).toHaveBeenCalledTimes(2);
expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, defaultFlagKeys);

// this is defaultPutResponse
expect(allFlags).toEqual({
Expand Down Expand Up @@ -229,12 +263,7 @@ describe('sdk-client storage', () => {

// both previous and current are true but inExperiment has changed
// so a change event should be emitted
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': {
previous: true,
current: true,
},
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['dev-test-flag']);
});

test('patch should emit change event', async () => {
Expand All @@ -244,46 +273,33 @@ describe('sdk-client storage', () => {
patchResponse.version += 1;

const allFlags = await identifyGetAllFlags(false, defaultPutResponse, patchResponse);
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.calls[0][1]) as Flags;
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.lastCall[1]) as Flags;

expect(allFlags).toMatchObject({ 'dev-test-flag': false });
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(basicPlatform.storage.set).toHaveBeenNthCalledWith(
1,
expect(basicPlatform.storage.set).toHaveBeenCalledWith(
'org:Testy Pizza',
expect.stringContaining(JSON.stringify(patchResponse)),
);
expect(flagsInStorage['dev-test-flag'].version).toEqual(patchResponse.version);
expect(emitter.emit).toHaveBeenCalledTimes(3);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': {
previous: true,
current: false,
},
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['dev-test-flag']);
});

test('patch should add new flags', async () => {
const patchResponse = clone<PatchFlag>(defaultPutResponse['dev-test-flag']);
patchResponse.key = 'another-dev-test-flag';

const allFlags = await identifyGetAllFlags(false, defaultPutResponse, patchResponse);
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.calls[0][1]) as Flags;
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.lastCall[1]) as Flags;

expect(allFlags).toHaveProperty('another-dev-test-flag');
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(basicPlatform.storage.set).toHaveBeenNthCalledWith(
1,
expect(basicPlatform.storage.set).toHaveBeenCalledWith(
'org:Testy Pizza',
expect.stringContaining(JSON.stringify(patchResponse)),
);
expect(flagsInStorage).toHaveProperty('another-dev-test-flag');
expect(emitter.emit).toHaveBeenCalledTimes(3);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'another-dev-test-flag': {
current: true,
},
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['another-dev-test-flag']);
});

test('patch should ignore older version', async () => {
Expand All @@ -300,7 +316,7 @@ describe('sdk-client storage', () => {
false,
);

expect(basicPlatform.storage.set).not.toHaveBeenCalled();
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(emitter.emit).not.toHaveBeenCalledWith('change');

// this is defaultPutResponse
Expand Down Expand Up @@ -328,22 +344,16 @@ describe('sdk-client storage', () => {
undefined,
deleteResponse,
);
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.calls[0][1]) as Flags;
const flagsInStorage = JSON.parse(basicPlatform.storage.set.mock.lastCall[1]) as Flags;

expect(allFlags).not.toHaveProperty('dev-test-flag');
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(basicPlatform.storage.set).toHaveBeenNthCalledWith(
1,
expect(basicPlatform.storage.set).toHaveBeenCalledWith(
'org:Testy Pizza',
expect.not.stringContaining('dev-test-flag'),
);
expect(flagsInStorage['dev-test-flag']).toBeUndefined();
expect(emitter.emit).toHaveBeenCalledTimes(3);
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, {
'dev-test-flag': {
previous: true,
},
});
expect(emitter.emit).toHaveBeenNthCalledWith(3, 'change', context, ['dev-test-flag']);
});

test('delete should not delete newer version', async () => {
Expand All @@ -361,7 +371,7 @@ describe('sdk-client storage', () => {
);

expect(allFlags).toHaveProperty('dev-test-flag');
expect(basicPlatform.storage.set).not.toHaveBeenCalled();
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(emitter.emit).not.toHaveBeenCalledWith('change');
});

Expand All @@ -373,7 +383,7 @@ describe('sdk-client storage', () => {

await identifyGetAllFlags(false, defaultPutResponse, undefined, deleteResponse, false);

expect(basicPlatform.storage.set).not.toHaveBeenCalled();
expect(basicPlatform.storage.set).toHaveBeenCalledTimes(1);
expect(emitter.emit).not.toHaveBeenCalledWith('change');
});
});
4 changes: 2 additions & 2 deletions packages/shared/sdk-client/src/LDClientImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('sdk-client object', () => {
expect(ldc.getContext()).toBeUndefined();
});

test('identify ready and error listeners', async () => {
test('identify change and error listeners', async () => {
// @ts-ignore
const { emitter } = ldc;

Expand All @@ -142,7 +142,7 @@ describe('sdk-client object', () => {
const carContext2: LDContext = { kind: 'car', key: 'subaru-forrester' };
await ldc.identify(carContext2);

expect(emitter.listenerCount('ready')).toEqual(1);
expect(emitter.listenerCount('change')).toEqual(1);
expect(emitter.listenerCount('error')).toEqual(1);
});
});
Loading