Skip to content

Commit 1ef3126

Browse files
committed
feat: Automatically start streaming based on event handlers.
1 parent 0da9d31 commit 1ef3126

File tree

4 files changed

+111
-15
lines changed

4 files changed

+111
-15
lines changed

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
128128
off: jest.fn(),
129129
} as unknown as jest.Mocked<FlagManager>;
130130

131-
browserConfig = validateOptions({ streaming: false }, logger);
131+
browserConfig = validateOptions({}, logger);
132132
baseHeaders = {};
133133
emitter = {
134134
emit: jest.fn(),
@@ -262,7 +262,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
262262
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
263263

264264
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
265-
dataManager.startDataSource();
265+
dataManager.setForcedStreaming(true);
266266
expect(platform.requests.createEventSource).toHaveBeenCalled();
267267
});
268268

@@ -277,8 +277,8 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
277277
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
278278

279279
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
280-
dataManager.startDataSource();
281-
dataManager.startDataSource();
280+
dataManager.setForcedStreaming(true);
281+
dataManager.setForcedStreaming(true);
282282
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
283283
expect(logger.debug).toHaveBeenCalledWith(
284284
'[BrowserDataManager] Update processor already active. Not changing state.',
@@ -287,10 +287,49 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
287287

288288
it('does not start a stream if identify has not been called', async () => {
289289
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
290-
dataManager.startDataSource();
290+
dataManager.setForcedStreaming(true);
291291
expect(platform.requests.createEventSource).not.toHaveBeenCalledTimes(1);
292292
expect(logger.debug).toHaveBeenCalledWith(
293293
'[BrowserDataManager] Context not set, not starting update processor.',
294294
);
295295
});
296+
297+
it('starts a stream on demand when not forced on/off', async () => {
298+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
299+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
300+
const identifyResolve = jest.fn();
301+
const identifyReject = jest.fn();
302+
303+
flagManager.loadCached.mockResolvedValue(false);
304+
305+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
306+
307+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
308+
dataManager.setAutomaticStreamingState(true);
309+
expect(platform.requests.createEventSource).toHaveBeenCalled();
310+
expect(logger.debug).toHaveBeenCalledWith('[BrowserDataManager] Starting update processor.');
311+
expect(logger.debug).toHaveBeenCalledWith(
312+
'[BrowserDataManager] Updating streaming state. forced(undefined) automatic(true)',
313+
);
314+
});
315+
316+
it('does not start a stream when forced off', async () => {
317+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
318+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
319+
const identifyResolve = jest.fn();
320+
const identifyReject = jest.fn();
321+
322+
dataManager.setForcedStreaming(false);
323+
324+
flagManager.loadCached.mockResolvedValue(false);
325+
326+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
327+
328+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
329+
dataManager.setAutomaticStreamingState(true);
330+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
331+
expect(logger.debug).toHaveBeenCalledWith(
332+
'[BrowserDataManager] Updating streaming state. forced(false) automatic(true)',
333+
);
334+
});
296335
});

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Platform,
1515
} from '@launchdarkly/js-client-sdk-common';
1616
import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common/dist/api/LDIdentifyOptions';
17+
import { EventName } from '@launchdarkly/js-client-sdk-common/dist/LDEmitter';
1718

1819
import BrowserDataManager from './BrowserDataManager';
1920
import GoalManager from './goals/GoalManager';
@@ -29,11 +30,21 @@ export type LDClient = Omit<
2930
CommonClient,
3031
'setConnectionMode' | 'getConnectionMode' | 'getOffline'
3132
> & {
32-
setStreaming(streaming: boolean): void;
33+
/**
34+
* Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates.
35+
*
36+
* If this is true, the client will always attempt to maintain a streaming connection; if false,
37+
* it never will. If you leave the value undefined (the default), the client will open a streaming
38+
* connection if you subscribe to `"change"` or `"change:flag-key"` events (see {@link LDClient.on}).
39+
*
40+
* This can also be set as the `streaming` property of {@link LDOptions}.
41+
*/
42+
setStreaming(streaming?: boolean): void;
3343
};
3444

3545
export class BrowserClient extends LDClientImpl {
3646
private readonly goalManager?: GoalManager;
47+
3748
constructor(
3849
private readonly clientSideId: string,
3950
autoEnvAttributes: AutoEnvAttributes,
@@ -164,12 +175,27 @@ export class BrowserClient extends LDClientImpl {
164175
this.goalManager?.startTracking();
165176
}
166177

167-
setStreaming(streaming: boolean): void {
178+
setStreaming(streaming?: boolean): void {
179+
// With FDv2 we may want to consider if we support connection mode directly.
180+
// Maybe with an extension to connection mode for 'automatic'.
168181
const browserDataManager = this.dataManager as BrowserDataManager;
169-
if (streaming) {
170-
browserDataManager.startDataSource();
171-
} else {
172-
browserDataManager.stopDataSource();
173-
}
182+
browserDataManager.setForcedStreaming(streaming);
183+
}
184+
185+
private updateAutomaticStreamingState() {
186+
const browserDataManager = this.dataManager as BrowserDataManager;
187+
// This will need changed if support for listening to individual flag change
188+
// events it added.
189+
browserDataManager.setAutomaticStreamingState(!!this.emitter.listenerCount('change'));
190+
}
191+
192+
override on(eventName: EventName, listener: Function): void {
193+
super.on(eventName, listener);
194+
this.updateAutomaticStreamingState();
195+
}
196+
197+
override off(eventName: EventName, listener: Function): void {
198+
super.off(eventName, listener);
199+
this.updateAutomaticStreamingState();
174200
}
175201
}

packages/sdk/browser/src/BrowserDataManager.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import { ValidatedOptions } from './options';
1818
const logTag = '[BrowserDataManager]';
1919

2020
export default class BrowserDataManager extends BaseDataManager {
21+
// If streaming is forced on or off, then we follow that setting.
22+
// Otherwise we automatically manage streaming state.
23+
private forcedStreaming?: boolean = undefined;
24+
private automaticStreamingState?: boolean = false;
25+
2126
constructor(
2227
platform: Platform,
2328
flagManager: FlagManager,
@@ -41,6 +46,7 @@ export default class BrowserDataManager extends BaseDataManager {
4146
emitter,
4247
diagnosticsManager,
4348
);
49+
this.forcedStreaming = browserConfig.streaming;
4450
}
4551

4652
private debugLog(message: any, ...args: any[]) {
@@ -76,12 +82,37 @@ export default class BrowserDataManager extends BaseDataManager {
7682
}
7783
}
7884

79-
stopDataSource() {
85+
setForcedStreaming(streaming?: boolean) {
86+
this.forcedStreaming = streaming;
87+
this.updateStreamingState();
88+
}
89+
90+
setAutomaticStreamingState(streaming: boolean) {
91+
this.automaticStreamingState = streaming;
92+
this.updateStreamingState();
93+
}
94+
95+
private updateStreamingState() {
96+
const shouldBeStreaming =
97+
this.forcedStreaming || (this.automaticStreamingState && this.forcedStreaming === undefined);
98+
99+
this.debugLog(
100+
`Updating streaming state. forced(${this.forcedStreaming}) automatic(${this.automaticStreamingState})`,
101+
);
102+
103+
if (shouldBeStreaming) {
104+
this.startDataSource();
105+
} else {
106+
this.stopDataSource();
107+
}
108+
}
109+
110+
private stopDataSource() {
80111
this.updateProcessor?.close();
81112
this.updateProcessor = undefined;
82113
}
83114

84-
startDataSource() {
115+
private startDataSource() {
85116
if (this.updateProcessor) {
86117
this.debugLog('Update processor already active. Not changing state.');
87118
return;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class LDClientImpl implements LDClient {
5454

5555
private eventFactoryDefault = new EventFactory(false);
5656
private eventFactoryWithReasons = new EventFactory(true);
57-
private emitter: LDEmitter;
57+
protected emitter: LDEmitter;
5858
private flagManager: DefaultFlagManager;
5959

6060
private eventSendingEnabled: boolean = false;

0 commit comments

Comments
 (0)