Skip to content

Commit c4879f5

Browse files
committed
updated datasource states. shutdown to be closed and now closed and initializing can occur more than once
1 parent ff6dda5 commit c4879f5

File tree

7 files changed

+80
-62
lines changed

7 files changed

+80
-62
lines changed

packages/shared/sdk-client/__tests__/LDClientImpl.test.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ describe('sdk-client object', () => {
3333
let mockPlatform: ReturnType<typeof createBasicPlatform>;
3434
let logger: ReturnType<typeof createLogger>;
3535

36-
const onDataSourceChangePromise = () =>
37-
new Promise<string[]>((res) => {
38-
ldc.on('dataSourceStatus', (_context: LDContext, changes: string[]) => {
39-
res(changes);
36+
function onDataSourceChangePromise(numToAwait: number) {
37+
let countdown = numToAwait;
38+
// eslint-disable-next-line no-new
39+
return new Promise<void>((res) => {
40+
ldc.on('dataSourceStatus', () => {
41+
countdown -= 1;
42+
if (countdown === 0) {
43+
res();
44+
}
4045
});
4146
});
47+
}
4248

4349
beforeEach(() => {
4450
mockPlatform = createBasicPlatform();
@@ -321,12 +327,21 @@ describe('sdk-client object', () => {
321327

322328
const spyListener = jest.fn();
323329
ldc.on('dataSourceStatus', spyListener);
324-
const changePromise = onDataSourceChangePromise();
330+
const changePromise = onDataSourceChangePromise(2);
325331
await ldc.identify(carContext);
326332
await changePromise;
327333

334+
expect(spyListener).toHaveBeenCalledTimes(2);
328335
expect(spyListener).toHaveBeenNthCalledWith(
329336
1,
337+
expect.objectContaining({
338+
state: DataSourceState.Initializing,
339+
stateSince: expect.any(Number),
340+
lastError: undefined,
341+
}),
342+
);
343+
expect(spyListener).toHaveBeenNthCalledWith(
344+
2,
330345
expect.objectContaining({
331346
state: DataSourceState.Valid,
332347
stateSince: expect.any(Number),
@@ -335,7 +350,7 @@ describe('sdk-client object', () => {
335350
);
336351
});
337352

338-
test('data source status emits shutdown when initialization encounters unrecoverable error', async () => {
353+
test('data source status emits closed when initialization encounters unrecoverable error', async () => {
339354
const carContext: LDContext = { kind: 'car', key: 'test-car' };
340355

341356
mockPlatform.crypto.randomUUID.mockReturnValue('random1');
@@ -350,16 +365,23 @@ describe('sdk-client object', () => {
350365

351366
const spyListener = jest.fn();
352367
ldc.on('dataSourceStatus', spyListener);
353-
const changePromise = onDataSourceChangePromise();
354-
368+
const changePromise = onDataSourceChangePromise(2);
355369
await expect(ldc.identify(carContext)).rejects.toThrow('test-error');
356370
await changePromise;
357371

358-
expect(spyListener).toHaveBeenCalledTimes(1);
372+
expect(spyListener).toHaveBeenCalledTimes(2);
359373
expect(spyListener).toHaveBeenNthCalledWith(
360374
1,
361375
expect.objectContaining({
362-
state: DataSourceState.Shutdown,
376+
state: DataSourceState.Initializing,
377+
stateSince: expect.any(Number),
378+
lastError: undefined,
379+
}),
380+
);
381+
expect(spyListener).toHaveBeenNthCalledWith(
382+
2,
383+
expect.objectContaining({
384+
state: DataSourceState.Closed,
363385
stateSince: expect.any(Number),
364386
lastError: expect.anything(),
365387
}),

packages/shared/sdk-client/__tests__/datasource/DataSourceStatusManager.test.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,40 @@ import { DataSourceState } from '../../src/datasource/DataSourceStatus';
44
import DataSourceStatusManager from '../../src/datasource/DataSourceStatusManager';
55

66
describe('DataSourceStatusManager', () => {
7-
test('its first state is initializing', async () => {
7+
test('its first state is closed', async () => {
88
const underTest = new DataSourceStatusManager();
9-
expect(underTest.status.state).toEqual(DataSourceState.Initializing);
9+
expect(underTest.status.state).toEqual(DataSourceState.Closed);
1010
});
1111

12-
test('it stays at initializing if receives non shutdown error', async () => {
12+
test('it stays at initializing if receives recoverable error', async () => {
1313
const underTest = new DataSourceStatusManager();
14-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
14+
underTest.requestStateUpdate(DataSourceState.Initializing);
15+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
1516
expect(underTest.status.state).toEqual(DataSourceState.Initializing);
1617
});
1718

18-
test('it moves to shutdown if receives shutdown error', async () => {
19+
test('it moves to closed if receives unrecoverable error', async () => {
1920
const underTest = new DataSourceStatusManager();
20-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 404, false);
21-
expect(underTest.status.state).toEqual(DataSourceState.Shutdown);
21+
underTest.requestStateUpdate(DataSourceState.Initializing);
22+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 404, false);
23+
expect(underTest.status.state).toEqual(DataSourceState.Closed);
2224
});
2325

2426
test('it updates last error time with each error, but not stateSince', async () => {
2527
let time = 0;
2628
const stamper: () => number = () => time;
2729
const underTest = new DataSourceStatusManager(stamper);
28-
29-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
30+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
3031
expect(underTest.status.stateSince).toEqual(0);
3132
expect(underTest.status.lastError?.time).toEqual(0);
3233

3334
time += 1;
34-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
35+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
3536
expect(underTest.status.stateSince).toEqual(0);
3637
expect(underTest.status.lastError?.time).toEqual(1);
3738

3839
time += 1;
39-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
40+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 404, true);
4041
expect(underTest.status.stateSince).toEqual(0);
4142
expect(underTest.status.lastError?.time).toEqual(2);
4243
});
@@ -46,15 +47,15 @@ describe('DataSourceStatusManager', () => {
4647
const stamper: () => number = () => time;
4748

4849
const underTest = new DataSourceStatusManager(stamper);
49-
expect(underTest.status.state).toEqual(DataSourceState.Initializing);
50+
expect(underTest.status.state).toEqual(DataSourceState.Closed);
5051
expect(underTest.status.stateSince).toEqual(0);
5152

5253
time += 1;
53-
underTest.setValid();
54+
underTest.requestStateUpdate(DataSourceState.Valid);
5455
expect(underTest.status.stateSince).toEqual(1);
5556

5657
time += 1;
57-
underTest.setShutdown();
58+
underTest.requestStateUpdate(DataSourceState.Closed);
5859
expect(underTest.status.stateSince).toEqual(2);
5960
});
6061

@@ -65,13 +66,13 @@ describe('DataSourceStatusManager', () => {
6566
const spyListener = jest.fn();
6667
underTest.on(spyListener);
6768

68-
underTest.setOffline();
69+
underTest.requestStateUpdate(DataSourceState.SetOffline);
6970
time += 1;
70-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 400, true);
71+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 400, true);
7172
time += 1;
72-
underTest.setError(DataSourceErrorKind.ErrorResponse, 'womp', 400, true);
73+
underTest.reportError(DataSourceErrorKind.ErrorResponse, 'womp', 400, true);
7374
time += 1;
74-
underTest.setShutdown();
75+
underTest.requestStateUpdate(DataSourceState.Closed);
7576
expect(spyListener).toHaveBeenCalledTimes(4);
7677
expect(spyListener).toHaveBeenNthCalledWith(
7778
1,
@@ -100,7 +101,7 @@ describe('DataSourceStatusManager', () => {
100101
expect(spyListener).toHaveBeenNthCalledWith(
101102
4,
102103
expect.objectContaining({
103-
state: DataSourceState.Shutdown,
104+
state: DataSourceState.Closed,
104105
stateSince: 3,
105106
lastError: expect.anything(),
106107
}),

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { LDClientInternalOptions } from './configuration/Configuration';
2727
import { addAutoEnv } from './context/addAutoEnv';
2828
import { ensureKey } from './context/ensureKey';
2929
import DataSourceEventHandler from './datasource/DataSourceEventHandler';
30-
import DataSourceStatus from './datasource/DataSourceStatus';
30+
import DataSourceStatus, { DataSourceState } from './datasource/DataSourceStatus';
3131
import DataSourceStatusManager from './datasource/DataSourceStatusManager';
3232
import createDiagnosticsManager from './diagnostics/createDiagnosticsManager';
3333
import {
@@ -155,7 +155,7 @@ export default class LDClientImpl implements LDClient {
155155
switch (mode) {
156156
case 'offline':
157157
this.updateProcessor?.close();
158-
this.dataSourceStatusManager.setOffline();
158+
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.SetOffline);
159159
break;
160160
case 'polling':
161161
case 'streaming':
@@ -204,7 +204,7 @@ export default class LDClientImpl implements LDClient {
204204
await this.flush();
205205
this.eventProcessor?.close();
206206
this.updateProcessor?.close();
207-
this.dataSourceStatusManager.setShutdown();
207+
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Closed);
208208
this.logger.debug('Closed event processor and data source.');
209209
}
210210

@@ -387,7 +387,10 @@ export default class LDClientImpl implements LDClient {
387387
}
388388
} else {
389389
// we don't update data source status in this path because we are about to start again
390-
this.updateProcessor?.close();
390+
if (this.updateProcessor) {
391+
this.updateProcessor.close();
392+
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Closed);
393+
}
391394

392395
switch (this.getConnectionMode()) {
393396
case 'streaming':
@@ -399,6 +402,8 @@ export default class LDClientImpl implements LDClient {
399402
default:
400403
break;
401404
}
405+
// update status before starting processor to ensure potential errors are reported after initializing
406+
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Initializing);
402407
this.updateProcessor!.start();
403408
}
404409

packages/shared/sdk-client/src/datasource/DataSourceEventHandler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Context, internal, LDLogger } from '@launchdarkly/js-sdk-common';
33
import FlagManager from '../flag-manager/FlagManager';
44
import { ItemDescriptor } from '../flag-manager/ItemDescriptor';
55
import { DeleteFlag, Flags, PatchFlag } from '../types';
6+
import { DataSourceState } from './DataSourceStatus';
67
import DataSourceStatusManager from './DataSourceStatusManager';
78

89
type LDStreamingError = internal.LDStreamingError;
@@ -27,7 +28,7 @@ export default class DataSourceEventHandler {
2728
{},
2829
);
2930
await this.flagManager.init(context, descriptors);
30-
this.statusManager.setValid();
31+
this.statusManager.requestStateUpdate(DataSourceState.Valid);
3132
}
3233

3334
async handlePatch(context: Context, patchFlag: PatchFlag) {
@@ -57,10 +58,10 @@ export default class DataSourceEventHandler {
5758
}
5859

5960
handleStreamingError(error: LDStreamingError) {
60-
this.statusManager.setError(error.kind, error.message, error.code, error.recoverable);
61+
this.statusManager.reportError(error.kind, error.message, error.code, error.recoverable);
6162
}
6263

6364
handlePollingError(error: LDPollingError) {
64-
this.statusManager.setError(error.kind, error.message, error.status, error.recoverable);
65+
this.statusManager.reportError(error.kind, error.message, error.status, error.recoverable);
6566
}
6667
}

packages/shared/sdk-client/src/datasource/DataSourceStatus.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export enum DataSourceState {
55
Valid,
66
Interrupted,
77
SetOffline,
8-
Shutdown,
8+
Closed,
99
// TODO: SDK-702 - Implement network availability behaviors
1010
// NetworkUnavailable,
1111
}
@@ -20,8 +20,8 @@ export default interface DataSourceStatus {
2020
* The UNIX epoch timestamp in milliseconds that the value of State most recently changed.
2121
*
2222
* The meaning of this depends on the current state:
23-
* For {@link DataSourceState.Initializing}, it is the time that the SDK started
24-
* initializing.
23+
* For {@link DataSourceState.Initializing}, it is the time that the datasource started
24+
* attempting to retrieve data.
2525
*
2626
* For {@link DataSourceState.Valid}, it is the time that the data source most
2727
* recently entered a valid state, after previously having been
@@ -32,8 +32,8 @@ export default interface DataSourceStatus {
3232
* most recently entered an error state, after previously having been
3333
* {@link DataSourceState.valid}.
3434
*
35-
* For {@link DataSourceState.Shutdown}, it is the time that the data source
36-
* encountered an unrecoverable error or that the datasource was explicitly shut down.
35+
* For {@link DataSourceState.Closed}, it is the time that the data source
36+
* encountered an unrecoverable error or that the datasource was explicitly closed.
3737
*/
3838
readonly stateSince: number;
3939

packages/shared/sdk-client/src/datasource/DataSourceStatusManager.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default class DataSourceStatusManager {
2222
private emitter: LDEmitter;
2323

2424
constructor(timeStamper: () => number = () => Date.now()) {
25-
this.state = DataSourceState.Initializing;
25+
this.state = DataSourceState.Closed;
2626
this.stateSinceMillis = timeStamper();
2727
this.emitter = new LDEmitter();
2828
this.timeStamper = timeStamper;
@@ -74,24 +74,12 @@ export default class DataSourceStatusManager {
7474
}
7575

7676
/**
77-
* Sets the state to {@link DataSourceState.Valid}
77+
* Requests the manager move to the provided state. This request may be ignored
78+
* if the current state cannot transition to the requested state.
79+
* @param state that is requested
7880
*/
79-
setValid() {
80-
this.updateState(DataSourceState.Valid);
81-
}
82-
83-
/**
84-
* Sets the state to {@link DataSourceState.SetOffline}
85-
*/
86-
setOffline() {
87-
this.updateState(DataSourceState.SetOffline);
88-
}
89-
90-
/**
91-
* Sets the state to {@link DataSourceState.Shutdown}
92-
*/
93-
setShutdown() {
94-
this.updateState(DataSourceState.Shutdown);
81+
requestStateUpdate(state: DataSourceState) {
82+
this.updateState(state);
9583
}
9684

9785
/**
@@ -104,7 +92,7 @@ export default class DataSourceStatusManager {
10492
* @param statusCode of the error if there was one
10593
* @param recoverable to indicate that the error is anticipated to be recoverable
10694
*/
107-
setError(
95+
reportError(
10896
kind: DataSourceErrorKind,
10997
message: string,
11098
statusCode?: number,
@@ -117,7 +105,7 @@ export default class DataSourceStatusManager {
117105
time: this.timeStamper(),
118106
};
119107
this.errorInfo = errorInfo;
120-
this.updateState(recoverable ? DataSourceState.Interrupted : DataSourceState.Shutdown, true);
108+
this.updateState(recoverable ? DataSourceState.Interrupted : DataSourceState.Closed, true);
121109
}
122110

123111
// TODO: SDK-702 - Implement network availability behaviors

packages/shared/sdk-client/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LDClientInternalOptions } from './configuration/Configuration';
2+
import DataSourceStatus from './datasource/DataSourceStatus';
23
import LDClientImpl from './LDClientImpl';
34

45
export * from '@launchdarkly/js-sdk-common';
@@ -19,4 +20,4 @@ export type {
1920

2021
export { DataSourcePaths } from './streaming';
2122

22-
export { LDClientImpl, LDClientInternalOptions };
23+
export { DataSourceStatus, LDClientImpl, LDClientInternalOptions };

0 commit comments

Comments
 (0)