Skip to content

Commit f58e746

Browse files
authored
fix: Ensure streaming connection is closed on SDK close. (#774)
1 parent 6058249 commit f58e746

File tree

7 files changed

+117
-5
lines changed

7 files changed

+117
-5
lines changed

packages/sdk/browser/__tests__/BrowserDataManager.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
6363
let diagnosticsManager: jest.Mocked<internal.DiagnosticsManager>;
6464
let dataManager: BrowserDataManager;
6565
let logger: LDLogger;
66+
let eventSourceCloseMethod: jest.Mock;
67+
6668
beforeEach(() => {
6769
logger = {
6870
error: jest.fn(),
6971
warn: jest.fn(),
7072
info: jest.fn(),
7173
debug: jest.fn(),
7274
};
75+
eventSourceCloseMethod = jest.fn();
7376
config = {
7477
logger,
7578
maxCachedContexts: 5,
@@ -106,7 +109,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
106109
options,
107110
onclose: jest.fn(),
108111
addEventListener: jest.fn(),
109-
close: jest.fn(),
112+
close: eventSourceCloseMethod,
110113
})),
111114
fetch: mockedFetch,
112115
getEventSourceCapabilities: jest.fn(),
@@ -495,4 +498,31 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
495498

496499
expect(platform.requests.createEventSource).toHaveBeenCalled();
497500
});
501+
502+
it('closes the event source when the data manager is closed', async () => {
503+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
504+
const identifyOptions: BrowserIdentifyOptions = {};
505+
const identifyResolve = jest.fn();
506+
const identifyReject = jest.fn();
507+
508+
dataManager.setForcedStreaming(undefined);
509+
dataManager.setAutomaticStreamingState(true);
510+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
511+
512+
flagManager.loadCached.mockResolvedValue(false);
513+
514+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
515+
516+
expect(platform.requests.createEventSource).toHaveBeenCalled();
517+
518+
dataManager.close();
519+
expect(eventSourceCloseMethod).toHaveBeenCalled();
520+
// Verify a subsequent identify doesn't create a new event source
521+
await dataManager.identify(identifyResolve, identifyReject, context, {});
522+
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
523+
524+
expect(logger.debug).toHaveBeenCalledWith(
525+
'[BrowserDataManager] Identify called after data manager was closed.',
526+
);
527+
});
498528
});

packages/sdk/browser/src/BrowserDataManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export default class BrowserDataManager extends BaseDataManager {
7474
context: Context,
7575
identifyOptions?: LDIdentifyOptions,
7676
): Promise<void> {
77+
if (this.closed) {
78+
this._debugLog('Identify called after data manager was closed.');
79+
return;
80+
}
81+
7782
this.context = context;
7883
const browserIdentifyOptions = identifyOptions as BrowserIdentifyOptions | undefined;
7984
if (browserIdentifyOptions?.hash) {

packages/sdk/react-native/__tests__/MobileDataManager.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ describe('given a MobileDataManager with mocked dependencies', () => {
5454
let diagnosticsManager: jest.Mocked<internal.DiagnosticsManager>;
5555
let mobileDataManager: MobileDataManager;
5656
let logger: LDLogger;
57+
let eventSourceCloseMethod: jest.Mock;
58+
5759
beforeEach(() => {
5860
logger = {
5961
error: jest.fn(),
6062
warn: jest.fn(),
6163
info: jest.fn(),
6264
debug: jest.fn(),
6365
};
66+
eventSourceCloseMethod = jest.fn();
67+
6468
config = {
6569
logger,
6670
maxCachedContexts: 5,
@@ -94,7 +98,7 @@ describe('given a MobileDataManager with mocked dependencies', () => {
9498
options,
9599
onclose: jest.fn(),
96100
addEventListener: jest.fn(),
97-
close: jest.fn(),
101+
close: eventSourceCloseMethod,
98102
})),
99103
fetch: mockedFetch,
100104
getEventSourceCapabilities: jest.fn(),
@@ -223,6 +227,23 @@ describe('given a MobileDataManager with mocked dependencies', () => {
223227
expect(platform.requests.fetch).not.toHaveBeenCalled();
224228
});
225229

230+
it('makes no connection when closed', async () => {
231+
mobileDataManager.close();
232+
233+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
234+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
235+
const identifyResolve = jest.fn();
236+
const identifyReject = jest.fn();
237+
238+
await mobileDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
239+
240+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
241+
expect(platform.requests.fetch).not.toHaveBeenCalled();
242+
expect(logger.debug).toHaveBeenCalledWith(
243+
'[MobileDataManager] Identify called after data manager was closed.',
244+
);
245+
});
246+
226247
it('should load cached flags and resolve the identify', async () => {
227248
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
228249
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
@@ -299,4 +320,25 @@ describe('given a MobileDataManager with mocked dependencies', () => {
299320
expect(identifyResolve).toHaveBeenCalled();
300321
expect(identifyReject).not.toHaveBeenCalled();
301322
});
323+
324+
it('closes the event source when the data manager is closed', async () => {
325+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
326+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
327+
const identifyResolve = jest.fn();
328+
const identifyReject = jest.fn();
329+
330+
await mobileDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
331+
expect(platform.requests.createEventSource).toHaveBeenCalled();
332+
333+
mobileDataManager.close();
334+
expect(eventSourceCloseMethod).toHaveBeenCalled();
335+
336+
// Verify a subsequent identify doesn't create a new event source
337+
await mobileDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
338+
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
339+
340+
expect(logger.debug).toHaveBeenCalledWith(
341+
'[MobileDataManager] Identify called after data manager was closed.',
342+
);
343+
});
302344
});

packages/sdk/react-native/src/MobileDataManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export default class MobileDataManager extends BaseDataManager {
5858
context: Context,
5959
identifyOptions?: LDIdentifyOptions,
6060
): Promise<void> {
61+
if (this.closed) {
62+
this._debugLog('Identify called after data manager was closed.');
63+
return;
64+
}
6165
this.context = context;
6266
const offline = this.connectionMode === 'offline';
6367
// In offline mode we do not support waiting for results.
@@ -140,6 +144,11 @@ export default class MobileDataManager extends BaseDataManager {
140144
}
141145

142146
async setConnectionMode(mode: ConnectionMode): Promise<void> {
147+
if (this.closed) {
148+
this._debugLog('setting connection mode after data manager was closed');
149+
return;
150+
}
151+
143152
if (this.connectionMode === mode) {
144153
this._debugLog(`setConnectionMode ignored. Mode is already '${mode}'.`);
145154
return;

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,21 @@ describe('sdk-client object', () => {
413413
}),
414414
);
415415
});
416+
417+
test('closes event source when client is closed', async () => {
418+
const carContext: LDContext = { kind: 'car', key: 'test-car' };
419+
420+
const mockCreateEventSource = jest.fn((streamUri: string = '', options: any = {}) => {
421+
mockEventSource = new MockEventSource(streamUri, options);
422+
mockEventSource.simulateEvents('put', [{ data: JSON.stringify(defaultPutResponse) }]);
423+
return mockEventSource;
424+
});
425+
mockPlatform.requests.createEventSource = mockCreateEventSource;
426+
427+
await ldc.identify(carContext);
428+
expect(mockEventSource.closed).toBe(false);
429+
430+
await ldc.close();
431+
expect(mockEventSource.closed).toBe(true);
432+
});
416433
});

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export interface DataManager {
4343
context: Context,
4444
identifyOptions?: LDIdentifyOptions,
4545
): Promise<void>;
46+
47+
/**
48+
* Closes the data manager. Any active connections are closed.
49+
*/
50+
close(): void;
4651
}
4752

4853
/**
@@ -69,6 +74,7 @@ export abstract class BaseDataManager implements DataManager {
6974
private _connectionParams?: ConnectionParams;
7075
protected readonly dataSourceStatusManager: DataSourceStatusManager;
7176
private readonly _dataSourceEventHandler: DataSourceEventHandler;
77+
protected closed = false;
7278

7379
constructor(
7480
protected readonly platform: Platform,
@@ -221,4 +227,9 @@ export abstract class BaseDataManager implements DataManager {
221227
},
222228
};
223229
}
230+
231+
public close() {
232+
this.updateProcessor?.close();
233+
this.closed = true;
234+
}
224235
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
LDHeaders,
1212
LDLogger,
1313
Platform,
14-
subsystem,
1514
timedPromise,
1615
TypeValidators,
1716
} from '@launchdarkly/js-sdk-common';
@@ -48,7 +47,6 @@ export default class LDClientImpl implements LDClient {
4847
private readonly _diagnosticsManager?: internal.DiagnosticsManager;
4948
private _eventProcessor?: internal.EventProcessor;
5049
readonly logger: LDLogger;
51-
private _updateProcessor?: subsystem.LDStreamProcessor;
5250

5351
private readonly _highTimeoutThreshold: number = 15;
5452

@@ -153,7 +151,7 @@ export default class LDClientImpl implements LDClient {
153151
async close(): Promise<void> {
154152
await this.flush();
155153
this._eventProcessor?.close();
156-
this._updateProcessor?.close();
154+
this.dataManager.close();
157155
this.logger.debug('Closed event processor and data source.');
158156
}
159157

0 commit comments

Comments
 (0)