Skip to content

Commit 7df3547

Browse files
committed
Merge branch 'main' into ta/sc-249239/data-source-status-rebase
2 parents 60ce6c5 + 980e4da commit 7df3547

File tree

11 files changed

+232
-48
lines changed

11 files changed

+232
-48
lines changed

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

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
7474
};
7575
config = {
7676
logger,
77-
baseUri: 'string',
78-
eventsUri: 'string',
79-
streamUri: 'string',
8077
maxCachedContexts: 5,
8178
capacity: 100,
8279
diagnosticRecordingInterval: 1000,
@@ -128,7 +125,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
128125
off: jest.fn(),
129126
} as unknown as jest.Mocked<FlagManager>;
130127

131-
browserConfig = validateOptions({ streaming: false }, logger);
128+
browserConfig = validateOptions({}, logger);
132129
baseHeaders = {};
133130
emitter = {
134131
emit: jest.fn(),
@@ -277,7 +274,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
277274
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
278275

279276
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
280-
dataManager.startDataSource();
277+
dataManager.setForcedStreaming(true);
281278
expect(platform.requests.createEventSource).toHaveBeenCalled();
282279
});
283280

@@ -292,8 +289,8 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
292289
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
293290

294291
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
295-
dataManager.startDataSource();
296-
dataManager.startDataSource();
292+
dataManager.setForcedStreaming(true);
293+
dataManager.setForcedStreaming(true);
297294
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
298295
expect(logger.debug).toHaveBeenCalledWith(
299296
'[BrowserDataManager] Update processor already active. Not changing state.',
@@ -302,10 +299,66 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
302299

303300
it('does not start a stream if identify has not been called', async () => {
304301
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
305-
dataManager.startDataSource();
302+
dataManager.setForcedStreaming(true);
306303
expect(platform.requests.createEventSource).not.toHaveBeenCalledTimes(1);
307304
expect(logger.debug).toHaveBeenCalledWith(
308305
'[BrowserDataManager] Context not set, not starting update processor.',
309306
);
310307
});
308+
309+
it('starts a stream on demand when not forced on/off', async () => {
310+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
311+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
312+
const identifyResolve = jest.fn();
313+
const identifyReject = jest.fn();
314+
315+
flagManager.loadCached.mockResolvedValue(false);
316+
317+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
318+
319+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
320+
dataManager.setAutomaticStreamingState(true);
321+
expect(platform.requests.createEventSource).toHaveBeenCalled();
322+
expect(logger.debug).toHaveBeenCalledWith('[BrowserDataManager] Starting update processor.');
323+
expect(logger.debug).toHaveBeenCalledWith(
324+
'[BrowserDataManager] Updating streaming state. forced(undefined) automatic(true)',
325+
);
326+
});
327+
328+
it('does not start a stream when forced off', async () => {
329+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
330+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
331+
const identifyResolve = jest.fn();
332+
const identifyReject = jest.fn();
333+
334+
dataManager.setForcedStreaming(false);
335+
336+
flagManager.loadCached.mockResolvedValue(false);
337+
338+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
339+
340+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
341+
dataManager.setAutomaticStreamingState(true);
342+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
343+
expect(logger.debug).toHaveBeenCalledWith(
344+
'[BrowserDataManager] Updating streaming state. forced(false) automatic(true)',
345+
);
346+
});
347+
348+
it('starts streaming on identify if the automatic state is true', async () => {
349+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
350+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
351+
const identifyResolve = jest.fn();
352+
const identifyReject = jest.fn();
353+
354+
dataManager.setForcedStreaming(undefined);
355+
dataManager.setAutomaticStreamingState(true);
356+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
357+
358+
flagManager.loadCached.mockResolvedValue(false);
359+
360+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
361+
362+
expect(platform.requests.createEventSource).toHaveBeenCalled();
363+
});
311364
});

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
AutoEnvAttributes,
33
base64UrlEncode,
4-
BasicLogger,
54
LDClient as CommonClient,
65
Configuration,
6+
createSafeLogger,
77
Encoding,
88
FlagManager,
99
internal,
@@ -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,24 +30,40 @@ 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,
4051
options: BrowserOptions = {},
4152
overridePlatform?: Platform,
4253
) {
4354
const { logger: customLogger, debug } = options;
55+
// Overrides the default logger from the common implementation.
4456
const logger =
4557
customLogger ??
46-
new BasicLogger({
47-
level: debug ? 'debug' : 'info',
58+
createSafeLogger({
59+
// eslint-disable-next-line no-console
60+
debug: debug ? console.debug : () => {},
61+
// eslint-disable-next-line no-console
62+
info: console.info,
4863
// eslint-disable-next-line no-console
49-
destination: console.log,
64+
warn: console.warn,
65+
// eslint-disable-next-line no-console
66+
error: console.error,
5067
});
5168

5269
// TODO: Use the already-configured baseUri from the SDK config. SDK-560
@@ -59,7 +76,7 @@ export class BrowserClient extends LDClientImpl {
5976
clientSideId,
6077
autoEnvAttributes,
6178
platform,
62-
filterToBaseOptions(options),
79+
filterToBaseOptions({ ...options, logger }),
6380
(
6481
flagManager: FlagManager,
6582
configuration: Configuration,
@@ -164,12 +181,27 @@ export class BrowserClient extends LDClientImpl {
164181
this.goalManager?.startTracking();
165182
}
166183

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

packages/sdk/browser/src/BrowserDataManager.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ 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+
26+
// +-----------+-----------+---------------+
27+
// | forced | automatic | state |
28+
// +-----------+-----------+---------------+
29+
// | true | false | streaming |
30+
// | true | true | streaming |
31+
// | false | true | not streaming |
32+
// | false | false | not streaming |
33+
// | undefined | true | streaming |
34+
// | undefined | false | not streaming |
35+
// +-----------+-----------+---------------+
36+
2137
constructor(
2238
platform: Platform,
2339
flagManager: FlagManager,
@@ -41,6 +57,7 @@ export default class BrowserDataManager extends BaseDataManager {
4157
emitter,
4258
diagnosticsManager,
4359
);
60+
this.forcedStreaming = browserConfig.streaming;
4461
}
4562

4663
private debugLog(message: any, ...args: any[]) {
@@ -71,17 +88,43 @@ export default class BrowserDataManager extends BaseDataManager {
7188
identifyReject(e);
7289
}
7390

74-
if (this.browserConfig.streaming) {
75-
this.setupConnection(context);
91+
this.updateStreamingState();
92+
}
93+
94+
setForcedStreaming(streaming?: boolean) {
95+
this.forcedStreaming = streaming;
96+
this.updateStreamingState();
97+
}
98+
99+
setAutomaticStreamingState(streaming: boolean) {
100+
this.automaticStreamingState = streaming;
101+
this.updateStreamingState();
102+
}
103+
104+
private updateStreamingState() {
105+
const shouldBeStreaming =
106+
this.forcedStreaming || (this.automaticStreamingState && this.forcedStreaming === undefined);
107+
108+
this.debugLog(
109+
`Updating streaming state. forced(${this.forcedStreaming}) automatic(${this.automaticStreamingState})`,
110+
);
111+
112+
if (shouldBeStreaming) {
113+
this.startDataSource();
114+
} else {
115+
this.stopDataSource();
76116
}
77117
}
78118

79-
stopDataSource() {
119+
private stopDataSource() {
120+
if (this.updateProcessor) {
121+
this.debugLog('Stopping update processor.');
122+
}
80123
this.updateProcessor?.close();
81124
this.updateProcessor = undefined;
82125
}
83126

84-
startDataSource() {
127+
private startDataSource() {
85128
if (this.updateProcessor) {
86129
this.debugLog('Update processor already active. Not changing state.');
87130
return;
@@ -132,5 +175,4 @@ export default class BrowserDataManager extends BaseDataManager {
132175
const uri = getPollingUri(this.config.serviceEndpoints, path, parameters);
133176
return new Requestor(this.platform.requests, uri, headers, method, body);
134177
}
135-
// TODO: Automatically start streaming if event handlers are registered.
136178
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,6 @@ describe('given a MobileDataManager with mocked dependencies', () => {
6363
};
6464
config = {
6565
logger,
66-
baseUri: 'string',
67-
eventsUri: 'string',
68-
streamUri: 'string',
6966
maxCachedContexts: 5,
7067
capacity: 100,
7168
diagnosticRecordingInterval: 1000,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ describe('sdk-client object', () => {
282282
const carContext2: LDContext = { kind: 'car', key: 'test-car-2' };
283283
await ldc.identify(carContext2);
284284

285-
expect(emitter.listenerCount('change')).toEqual(1);
285+
// No default listeners. This is important for clients to be able to determine if there are
286+
// any listeners and act on that information.
287+
expect(emitter.listenerCount('change')).toEqual(0);
286288
expect(emitter.listenerCount('error')).toEqual(1);
287289
});
288290

packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-disable no-console */
2+
import { createSafeLogger } from '@launchdarkly/js-sdk-common';
3+
24
import ConfigurationImpl from '../../src/configuration/Configuration';
35

46
describe('Configuration', () => {
@@ -120,3 +122,48 @@ describe('Configuration', () => {
120122
},
121123
);
122124
});
125+
126+
it('makes a safe logger', () => {
127+
const badLogger = {
128+
debug: () => {
129+
throw new Error('bad');
130+
},
131+
info: () => {
132+
throw new Error('bad');
133+
},
134+
warn: () => {
135+
throw new Error('bad');
136+
},
137+
error: () => {
138+
throw new Error('bad');
139+
},
140+
};
141+
const config = new ConfigurationImpl({
142+
logger: badLogger,
143+
});
144+
145+
expect(() => config.logger.debug('debug')).not.toThrow();
146+
expect(() => config.logger.info('info')).not.toThrow();
147+
expect(() => config.logger.warn('warn')).not.toThrow();
148+
expect(() => config.logger.error('error')).not.toThrow();
149+
expect(config.logger).not.toBe(badLogger);
150+
});
151+
152+
it('does not wrap already safe loggers', () => {
153+
const logger = createSafeLogger({
154+
debug: () => {
155+
throw new Error('bad');
156+
},
157+
info: () => {
158+
throw new Error('bad');
159+
},
160+
warn: () => {
161+
throw new Error('bad');
162+
},
163+
error: () => {
164+
throw new Error('bad');
165+
},
166+
});
167+
const config = new ConfigurationImpl({ logger });
168+
expect(config.logger).toBe(logger);
169+
});

packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ describe('automatic environment attributes', () => {
233233

234234
await addAutoEnv(context, mockPlatform, config);
235235

236-
expect(config.logger.warn).toHaveBeenCalledTimes(1);
237-
expect(config.logger.warn).toHaveBeenCalledWith(
236+
expect(logger.warn).toHaveBeenCalledTimes(1);
237+
expect(logger.warn).toHaveBeenCalledWith(
238238
expect.stringMatching(/ld_application.*already exists/),
239239
);
240240
});
@@ -251,10 +251,8 @@ describe('automatic environment attributes', () => {
251251

252252
await addAutoEnv(context, mockPlatform, config);
253253

254-
expect(config.logger.warn).toHaveBeenCalledTimes(1);
255-
expect(config.logger.warn).toHaveBeenCalledWith(
256-
expect.stringMatching(/ld_device.*already exists/),
257-
);
254+
expect(logger.warn).toHaveBeenCalledTimes(1);
255+
expect(logger.warn).toHaveBeenCalledWith(expect.stringMatching(/ld_device.*already exists/));
258256
});
259257

260258
test('single context with an attribute called ld_application should have auto env attributes', async () => {

0 commit comments

Comments
 (0)