launchdarkly-js-sdk-common 3.5.0 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +103 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. package/.github/pull_request_template.md +21 -0
  7. package/.ldrelease/config.yml +24 -0
  8. package/.prettierignore +1 -0
  9. package/.prettierrc +5 -0
  10. package/CHANGELOG.md +17 -0
  11. package/CONTRIBUTING.md +45 -0
  12. package/babel.config.js +18 -0
  13. package/docs/typedoc.js +11 -0
  14. package/jest.config.js +12 -0
  15. package/package.json +4 -32
  16. package/scripts/better-audit.sh +76 -0
  17. package/src/EventEmitter.js +60 -0
  18. package/src/EventProcessor.js +175 -0
  19. package/src/EventSender.js +87 -0
  20. package/src/EventSummarizer.js +84 -0
  21. package/src/Identity.js +26 -0
  22. package/src/InitializationState.js +83 -0
  23. package/src/PersistentFlagStore.js +50 -0
  24. package/src/PersistentStorage.js +81 -0
  25. package/src/Requestor.js +111 -0
  26. package/src/Stream.js +154 -0
  27. package/src/UserFilter.js +75 -0
  28. package/src/UserValidator.js +56 -0
  29. package/src/__tests__/.eslintrc.yaml +6 -0
  30. package/src/__tests__/EventProcessor-test.js +559 -0
  31. package/src/__tests__/EventSender-test.js +252 -0
  32. package/src/__tests__/EventSource-mock.js +61 -0
  33. package/src/__tests__/EventSummarizer-test.js +103 -0
  34. package/src/__tests__/LDClient-events-test.js +757 -0
  35. package/src/__tests__/LDClient-localstorage-test.js +179 -0
  36. package/src/__tests__/LDClient-streaming-test.js +716 -0
  37. package/src/__tests__/LDClient-test.js +753 -0
  38. package/src/__tests__/PersistentFlagStore-test.js +111 -0
  39. package/src/__tests__/Requestor-test.js +362 -0
  40. package/src/__tests__/Stream-test.js +299 -0
  41. package/src/__tests__/UserFilter-test.js +93 -0
  42. package/src/__tests__/UserValidator-test.js +57 -0
  43. package/src/__tests__/configuration-test.js +217 -0
  44. package/src/__tests__/diagnosticEvents-test.js +449 -0
  45. package/src/__tests__/loggers-test.js +149 -0
  46. package/src/__tests__/mockHttp.js +122 -0
  47. package/src/__tests__/promiseCoalescer-test.js +128 -0
  48. package/src/__tests__/stubPlatform.js +148 -0
  49. package/src/__tests__/testUtils.js +77 -0
  50. package/src/__tests__/utils-test.js +148 -0
  51. package/src/configuration.js +151 -0
  52. package/src/diagnosticEvents.js +269 -0
  53. package/src/errors.js +39 -0
  54. package/src/index.js +788 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +222 -0
  58. package/src/promiseCoalescer.js +52 -0
  59. package/src/utils.js +214 -0
  60. package/test-types.ts +94 -0
  61. package/tsconfig.json +13 -0
  62. package/typings.d.ts +4 -43
  63. package/dist/ldclient-common.cjs.js +0 -2
  64. package/dist/ldclient-common.cjs.js.map +0 -1
  65. package/dist/ldclient-common.es.js +0 -2
  66. package/dist/ldclient-common.es.js.map +0 -1
  67. package/dist/ldclient-common.min.js +0 -2
  68. package/dist/ldclient-common.min.js.map +0 -1
@@ -0,0 +1,716 @@
1
+ import * as messages from '../messages';
2
+ import * as utils from '../utils';
3
+
4
+ import { AsyncQueue, eventSink, sleepAsync, withCloseable } from 'launchdarkly-js-test-helpers';
5
+
6
+ import EventSource from './EventSource-mock';
7
+ import { respondJson } from './mockHttp';
8
+ import * as stubPlatform from './stubPlatform';
9
+ import { makeBootstrap } from './testUtils';
10
+
11
+ // These tests verify the client's optional streaming behavior. The actual implementation of
12
+ // the SSE client is provided by the platform-specific SDKs (e.g. the browser SDK uses
13
+ // EventSource, other SDKs use the js-eventsource polyfill) so these tests use only a mock
14
+ // implementation, verifying that the SDK interacts properly with the stream abstraction.
15
+
16
+ describe('LDClient streaming', () => {
17
+ const defaultStreamBaseUrl = 'https://clientstream.launchdarkly.com';
18
+ const envName = 'UNKNOWN_ENVIRONMENT_ID';
19
+ const lsKey = 'ld:UNKNOWN_ENVIRONMENT_ID:' + utils.btoa('{"key":"user"}');
20
+ const user = { key: 'user' };
21
+ const encodedUser = 'eyJrZXkiOiJ1c2VyIn0';
22
+ const hash = '012345789abcde';
23
+ let platform;
24
+
25
+ beforeEach(() => {
26
+ platform = stubPlatform.defaults();
27
+ });
28
+
29
+ async function withClientAndServer(extraConfig, asyncCallback) {
30
+ const server = platform.testing.http.newServer();
31
+ server.byDefault(respondJson({}));
32
+ const config = { ...extraConfig, baseUrl: server.url };
33
+ const client = platform.testing.makeClient(envName, user, config);
34
+ return await withCloseable(client, async () => await asyncCallback(client, server));
35
+ }
36
+
37
+ function makeExpectedStreamUrl(base64User, userHash, withReasons) {
38
+ const baseUrl = defaultStreamBaseUrl + '/eval/' + envName + '/' + base64User;
39
+ const queryParams = [];
40
+ if (userHash) {
41
+ queryParams.push('h=' + userHash);
42
+ }
43
+ if (withReasons) {
44
+ queryParams.push('?withReasons=true');
45
+ }
46
+ return baseUrl + (queryParams.length ? '?' + queryParams.join('&') : '');
47
+ }
48
+
49
+ describe('streaming/event listening', () => {
50
+ const fullStreamUrlWithUser = makeExpectedStreamUrl(encodedUser);
51
+
52
+ async function expectStreamConnecting(url) {
53
+ const stream = await platform.testing.expectStream(url);
54
+ expect(stream.eventSource.readyState === EventSource.CONNECTING);
55
+ return stream;
56
+ }
57
+
58
+ function expectNoStreamIsOpen() {
59
+ expect(platform.testing.eventSourcesCreated.length()).toEqual(0);
60
+ }
61
+
62
+ it('does not connect to the stream by default', async () => {
63
+ await withClientAndServer({}, async client => {
64
+ await client.waitForInitialization();
65
+
66
+ expectNoStreamIsOpen();
67
+ });
68
+ });
69
+
70
+ it('connects to the stream if options.streaming is true', async () => {
71
+ await withClientAndServer({ streaming: true }, async client => {
72
+ await client.waitForInitialization();
73
+
74
+ await platform.testing.expectStream(fullStreamUrlWithUser);
75
+ });
76
+ });
77
+
78
+ describe('setStreaming()', () => {
79
+ it('can connect to the stream', async () => {
80
+ await withClientAndServer({}, async client => {
81
+ await client.waitForInitialization();
82
+
83
+ client.setStreaming(true);
84
+ await expectStreamConnecting(fullStreamUrlWithUser);
85
+ });
86
+ });
87
+
88
+ it('can disconnect from the stream', async () => {
89
+ await withClientAndServer({}, async client => {
90
+ await client.waitForInitialization();
91
+
92
+ client.setStreaming(true);
93
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
94
+ client.setStreaming(false);
95
+ expect(stream.eventSource.readyState === EventSource.CLOSED);
96
+ });
97
+ });
98
+ });
99
+
100
+ describe('on("change")', () => {
101
+ it('connects to the stream if not otherwise overridden', async () => {
102
+ await withClientAndServer({}, async client => {
103
+ await client.waitForInitialization();
104
+ client.on('change', () => {});
105
+
106
+ await expectStreamConnecting(fullStreamUrlWithUser);
107
+ });
108
+ });
109
+
110
+ it('also connects if listening for a specific flag', async () => {
111
+ await withClientAndServer({}, async client => {
112
+ await client.waitForInitialization();
113
+ client.on('change:flagkey', () => {});
114
+
115
+ await expectStreamConnecting(fullStreamUrlWithUser);
116
+ });
117
+ });
118
+
119
+ it('does not connect if some other kind of event was specified', async () => {
120
+ await withClientAndServer({}, async client => {
121
+ await client.waitForInitialization();
122
+ client.on('error', () => {});
123
+
124
+ expectNoStreamIsOpen();
125
+ });
126
+ });
127
+
128
+ it('does not connect if options.streaming is explicitly set to false', async () => {
129
+ await withClientAndServer({ streaming: false }, async client => {
130
+ await client.waitForInitialization();
131
+ client.on('change', () => {});
132
+
133
+ expectNoStreamIsOpen();
134
+ });
135
+ });
136
+
137
+ it('does not connect if setStreaming(false) was called', async () => {
138
+ await withClientAndServer({}, async client => {
139
+ await client.waitForInitialization();
140
+ client.setStreaming(false);
141
+ client.on('change', () => {});
142
+
143
+ expectNoStreamIsOpen();
144
+ });
145
+ });
146
+ });
147
+
148
+ describe('off("change")', () => {
149
+ it('disconnects from the stream if all event listeners are removed', async () => {
150
+ await withClientAndServer({}, async client => {
151
+ const listener1 = () => {};
152
+ const listener2 = () => {};
153
+ await client.waitForInitialization();
154
+
155
+ client.on('change', listener1);
156
+ client.on('change:flagKey', listener2);
157
+ client.on('error', () => {});
158
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
159
+
160
+ client.off('change', listener1);
161
+ expect(stream.eventSource.readyState).toEqual(EventSource.CONNECTING);
162
+
163
+ client.off('change:flagKey', listener2);
164
+ expect(stream.eventSource.readyState).toEqual(EventSource.CLOSED);
165
+ });
166
+ });
167
+
168
+ it('does not disconnect if setStreaming(true) was called, but still removes event listener', async () => {
169
+ const changes1 = [];
170
+ const changes2 = [];
171
+
172
+ await withClientAndServer({}, async client => {
173
+ const listener1 = allValues => changes1.push(allValues);
174
+ const listener2 = newValue => changes2.push(newValue);
175
+ await client.waitForInitialization();
176
+
177
+ client.setStreaming(true);
178
+
179
+ client.on('change', listener1);
180
+ client.on('change:flagKey', listener2);
181
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
182
+
183
+ stream.eventSource.mockEmit('put', {
184
+ data: '{"flagKey":{"value":"a","version":1}}',
185
+ });
186
+
187
+ expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]);
188
+ expect(changes2).toEqual(['a']);
189
+
190
+ client.off('change', listener1);
191
+ expect(stream.eventSource.readyState).toEqual(EventSource.CONNECTING);
192
+
193
+ stream.eventSource.mockEmit('put', {
194
+ data: '{"flagKey":{"value":"b","version":1}}',
195
+ });
196
+
197
+ expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]);
198
+ expect(changes2).toEqual(['a', 'b']);
199
+
200
+ client.off('change:flagKey', listener2);
201
+ expect(stream.eventSource.readyState).toEqual(EventSource.CONNECTING);
202
+
203
+ stream.eventSource.mockEmit('put', {
204
+ data: '{"flagKey":{"value":"c","version":1}}',
205
+ });
206
+
207
+ expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]);
208
+ expect(changes2).toEqual(['a', 'b']);
209
+ });
210
+ });
211
+ });
212
+
213
+ it('passes the secure mode hash in the stream URL if provided', async () => {
214
+ await withClientAndServer({ hash }, async client => {
215
+ await client.waitForInitialization();
216
+ client.on('change:flagKey', () => {});
217
+
218
+ await expectStreamConnecting(fullStreamUrlWithUser + '?h=' + hash);
219
+ });
220
+ });
221
+
222
+ it('passes withReasons parameter if provided', async () => {
223
+ await withClientAndServer({ evaluationReasons: true }, async client => {
224
+ await client.waitForInitialization();
225
+ client.setStreaming(true);
226
+
227
+ await expectStreamConnecting(fullStreamUrlWithUser + '?withReasons=true');
228
+ });
229
+ });
230
+
231
+ it('passes secure mode hash and withReasons if provided', async () => {
232
+ await withClientAndServer({ hash, evaluationReasons: true }, async client => {
233
+ await client.waitForInitialization();
234
+ client.setStreaming(true);
235
+
236
+ await expectStreamConnecting(fullStreamUrlWithUser + '?h=' + hash + '&withReasons=true');
237
+ });
238
+ });
239
+
240
+ it('handles stream ping message by getting flags', async () => {
241
+ await withClientAndServer({}, async (client, server) => {
242
+ server.byDefault(respondJson({ flagKey: { value: true, version: 1 } }));
243
+ await client.waitForInitialization();
244
+ client.setStreaming(true);
245
+
246
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
247
+ stream.eventSource.mockEmit('ping');
248
+ await sleepAsync(20); // give response handler a chance to execute
249
+
250
+ expect(client.variation('flagKey')).toEqual(true);
251
+ });
252
+ });
253
+
254
+ it("poll request triggered by stream ping can't overwrite another user's flags", async () => {
255
+ const otherUser = { key: 'otherUser' };
256
+ const initUserBase64 = utils.base64URLEncode(JSON.stringify(user));
257
+ const otherUserBase64 = utils.base64URLEncode(JSON.stringify(otherUser));
258
+
259
+ await withClientAndServer({}, async (client, server) => {
260
+ const reqRespQueue = new AsyncQueue();
261
+ server.byDefault((req, resp) => {
262
+ reqRespQueue.add({ req: req, resp: resp });
263
+ });
264
+
265
+ const initPromise = client.waitForInitialization();
266
+ const poll1 = await reqRespQueue.take();
267
+ expect(poll1.req.path).toContain(initUserBase64);
268
+ respondJson({ flagKey: { value: 1 } })(poll1.req, poll1.resp);
269
+ await initPromise;
270
+
271
+ // The flag value is now 1, from the initial poll
272
+ expect(client.variation('flagKey')).toEqual(1);
273
+
274
+ client.setStreaming(true);
275
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
276
+
277
+ stream.eventSource.mockEmit('ping');
278
+ const poll2 = await reqRespQueue.take();
279
+ // poll2 is the poll request that was triggered by the ping; don't respond to it yet
280
+ expect(poll2.req.path).toContain(initUserBase64);
281
+
282
+ const identifyPromise = client.identify(otherUser);
283
+ const poll3 = await reqRespQueue.take();
284
+ // poll3 is the poll request for the identify
285
+ expect(poll3.req.path).toContain(otherUserBase64);
286
+
287
+ // Now let's say poll3 completes first, setting the flag value to 3 for the new user
288
+ respondJson({ flagKey: { value: 3 } })(poll3.req, poll3.resp);
289
+
290
+ // And then poll2, which was for the previous user, completes with a flag value of 2
291
+ respondJson({ flagKey: { value: 2 } })(poll2.req, poll2.resp);
292
+
293
+ await identifyPromise;
294
+
295
+ // The flag value should now be 3, not 2
296
+ expect(client.variation('flagKey')).toEqual(3);
297
+ });
298
+ });
299
+
300
+ it('handles stream put message by updating flags', async () => {
301
+ await withClientAndServer({}, async client => {
302
+ await client.waitForInitialization();
303
+ client.setStreaming(true);
304
+
305
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
306
+ stream.eventSource.mockEmit('put', {
307
+ data: '{"flagKey":{"value":true,"version":1}}',
308
+ });
309
+
310
+ expect(client.variation('flagKey')).toEqual(true);
311
+ });
312
+ });
313
+
314
+ it('updates local storage for put message if using local storage', async () => {
315
+ platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}');
316
+
317
+ await withClientAndServer({ bootstrap: 'localstorage' }, async client => {
318
+ await client.waitForInitialization();
319
+ client.setStreaming(true);
320
+
321
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
322
+ stream.eventSource.mockEmit('put', {
323
+ data: '{"flagKey":{"value":true,"version":1}}',
324
+ });
325
+
326
+ expect(client.variation('flagKey')).toEqual(true);
327
+ const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey));
328
+ expect(storageData).toMatchObject({ flagKey: { value: true, version: 1 } });
329
+ });
330
+ });
331
+
332
+ it('fires global change event when flags are updated from put event', async () => {
333
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
334
+ await client.waitForInitialization();
335
+
336
+ const receivedChange = eventSink(client, 'change');
337
+
338
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
339
+ stream.eventSource.mockEmit('put', {
340
+ data: '{"flagKey":{"value":true,"version":1}}',
341
+ });
342
+
343
+ const changes = await receivedChange.take();
344
+ expect(changes).toEqual({
345
+ flagKey: { current: true, previous: false },
346
+ });
347
+ });
348
+ });
349
+
350
+ it('does not fire change event if new and old values are equivalent JSON objects', async () => {
351
+ const config = {
352
+ bootstrap: {
353
+ 'will-change': 3,
354
+ 'wont-change': { a: 1, b: 2 },
355
+ },
356
+ };
357
+ await withClientAndServer(config, async client => {
358
+ await client.waitForInitialization();
359
+
360
+ const receivedChange = eventSink(client, 'change');
361
+
362
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
363
+ const putData = {
364
+ 'will-change': { value: 4, version: 2 },
365
+ 'wont-change': { value: { b: 2, a: 1 }, version: 2 },
366
+ };
367
+ stream.eventSource.mockEmit('put', { data: JSON.stringify(putData) });
368
+
369
+ const changes = await receivedChange.take();
370
+ expect(changes).toEqual({
371
+ 'will-change': { current: 4, previous: 3 },
372
+ });
373
+ });
374
+ });
375
+
376
+ it('fires individual change event when flags are updated from put event', async () => {
377
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
378
+ await client.waitForInitialization();
379
+
380
+ const receivedChange = eventSink(client, 'change:flagKey');
381
+
382
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
383
+ stream.eventSource.mockEmit('put', {
384
+ data: '{"flagKey":{"value":true,"version":1}}',
385
+ });
386
+
387
+ const args = await receivedChange.take();
388
+ expect(args).toEqual([true, false]);
389
+ });
390
+ });
391
+
392
+ it('handles patch message by updating flag', async () => {
393
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
394
+ await client.waitForInitialization();
395
+ client.setStreaming(true);
396
+
397
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
398
+ stream.eventSource.mockEmit('patch', { data: '{"key":"flagKey","value":true,"version":1}' });
399
+
400
+ expect(client.variation('flagKey')).toEqual(true);
401
+ });
402
+ });
403
+
404
+ it('does not update flag if patch version < flag version', async () => {
405
+ const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } });
406
+ await withClientAndServer({ bootstrap: initData }, async client => {
407
+ await client.waitForInitialization();
408
+
409
+ expect(client.variation('flagKey')).toEqual('a');
410
+
411
+ client.setStreaming(true);
412
+
413
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
414
+ stream.eventSource.mockEmit('patch', { data: '{"key":"flagKey","value":"b","version":1}' });
415
+
416
+ expect(client.variation('flagKey')).toEqual('a');
417
+ });
418
+ });
419
+
420
+ it('does not update flag if patch version == flag version', async () => {
421
+ const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } });
422
+ await withClientAndServer({ bootstrap: initData }, async client => {
423
+ await client.waitForInitialization();
424
+
425
+ expect(client.variation('flagKey')).toEqual('a');
426
+
427
+ client.setStreaming(true);
428
+
429
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
430
+ stream.eventSource.mockEmit('patch', { data: '{"key":"flagKey","value":"b","version":2}' });
431
+
432
+ expect(client.variation('flagKey')).toEqual('a');
433
+ });
434
+ });
435
+
436
+ it('updates flag if patch has a version and flag has no version', async () => {
437
+ const initData = makeBootstrap({ flagKey: { value: 'a' } });
438
+ await withClientAndServer({ bootstrap: initData }, async client => {
439
+ await client.waitForInitialization();
440
+
441
+ expect(client.variation('flagKey')).toEqual('a');
442
+
443
+ client.setStreaming(true);
444
+
445
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
446
+ stream.eventSource.mockEmit('patch', { data: '{"key":"flagKey","value":"b","version":1}' });
447
+
448
+ expect(client.variation('flagKey')).toEqual('b');
449
+ });
450
+ });
451
+
452
+ it('updates flag if flag has a version and patch has no version', async () => {
453
+ const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } });
454
+ await withClientAndServer({ bootstrap: initData }, async client => {
455
+ await client.waitForInitialization();
456
+
457
+ expect(client.variation('flagKey')).toEqual('a');
458
+
459
+ client.setStreaming(true);
460
+
461
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
462
+ stream.eventSource.mockEmit('patch', { data: '{"key":"flagKey","value":"b"}' });
463
+
464
+ expect(client.variation('flagKey')).toEqual('b');
465
+ });
466
+ });
467
+
468
+ it('updates local storage for patch message if using local storage', async () => {
469
+ platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}');
470
+
471
+ await withClientAndServer({ bootstrap: 'localstorage' }, async client => {
472
+ await client.waitForInitialization();
473
+ client.setStreaming(true);
474
+
475
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
476
+ stream.eventSource.mockEmit('put', {
477
+ data: '{"flagKey":{"value":true,"version":1}}',
478
+ });
479
+
480
+ expect(client.variation('flagKey')).toEqual(true);
481
+ const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey));
482
+ expect(storageData).toMatchObject({ flagKey: { value: true, version: 1 } });
483
+ });
484
+ });
485
+
486
+ it('fires global change event when flag is updated from patch event', async () => {
487
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
488
+ await client.waitForInitialization();
489
+
490
+ const receivedChange = eventSink(client, 'change');
491
+
492
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
493
+ stream.eventSource.mockEmit('patch', {
494
+ data: '{"key":"flagKey","value":true,"version":1}',
495
+ });
496
+
497
+ const changes = await receivedChange.take();
498
+ expect(changes).toEqual({
499
+ flagKey: { current: true, previous: false },
500
+ });
501
+ });
502
+ });
503
+
504
+ it('fires individual change event when flag is updated from patch event', async () => {
505
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
506
+ await client.waitForInitialization();
507
+
508
+ const receivedChange = eventSink(client, 'change:flagKey');
509
+
510
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
511
+ stream.eventSource.mockEmit('patch', {
512
+ data: '{"key":"flagKey","value":true,"version":1}',
513
+ });
514
+
515
+ const args = await receivedChange.take();
516
+ expect(args).toEqual([true, false]);
517
+ });
518
+ });
519
+
520
+ it('fires global change event when flag is newly created from patch event', async () => {
521
+ await withClientAndServer({}, async client => {
522
+ await client.waitForInitialization();
523
+
524
+ const receivedChange = eventSink(client, 'change');
525
+
526
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
527
+ stream.eventSource.mockEmit('patch', {
528
+ data: '{"key":"flagKey","value":true,"version":1}',
529
+ });
530
+
531
+ const changes = await receivedChange.take();
532
+ expect(changes).toEqual({
533
+ flagKey: { current: true },
534
+ });
535
+ });
536
+ });
537
+
538
+ it('fires individual change event when flag is newly created from patch event', async () => {
539
+ await withClientAndServer({}, async client => {
540
+ await client.waitForInitialization();
541
+
542
+ const receivedChange = eventSink(client, 'change:flagKey');
543
+
544
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
545
+ stream.eventSource.mockEmit('patch', {
546
+ data: '{"key":"flagKey","value":true,"version":1}',
547
+ });
548
+
549
+ const args = await receivedChange.take();
550
+ expect(args).toEqual([true, undefined]);
551
+ });
552
+ });
553
+
554
+ it('handles delete message by deleting flag', async () => {
555
+ await withClientAndServer({ bootstrap: { flagKey: false } }, async client => {
556
+ await client.waitForInitialization();
557
+ client.setStreaming(true);
558
+
559
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
560
+ stream.eventSource.mockEmit('delete', {
561
+ data: '{"key":"flagKey","version":1}',
562
+ });
563
+
564
+ expect(client.variation('flagKey')).toBeUndefined();
565
+ });
566
+ });
567
+
568
+ it('handles delete message for unknown flag by storing placeholder', async () => {
569
+ await withClientAndServer({}, async client => {
570
+ await client.waitForInitialization();
571
+ client.setStreaming(true);
572
+
573
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
574
+ stream.eventSource.mockEmit('delete', {
575
+ data: '{"key":"mystery","version":3}',
576
+ });
577
+
578
+ // The following patch message should be ignored because it has a lower version than the deleted placeholder
579
+ stream.eventSource.mockEmit('patch', {
580
+ data: '{"key":"mystery","value":"yes","version":2}',
581
+ });
582
+
583
+ expect(client.variation('mystery')).toBeUndefined();
584
+ });
585
+ });
586
+
587
+ it('ignores delete message with lower version', async () => {
588
+ const initData = makeBootstrap({ flagKey: { value: 'yes', version: 3 } });
589
+ await withClientAndServer({ bootstrap: initData }, async client => {
590
+ await client.waitForInitialization();
591
+ client.setStreaming(true);
592
+
593
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
594
+ stream.eventSource.mockEmit('delete', {
595
+ data: '{"key":"flagKey","version":2}',
596
+ });
597
+
598
+ expect(client.variation('flagKey')).toEqual('yes');
599
+ });
600
+ });
601
+
602
+ it('fires global change event when flag is deleted', async () => {
603
+ await withClientAndServer({ bootstrap: { flagKey: true } }, async client => {
604
+ await client.waitForInitialization();
605
+
606
+ const receivedChange = eventSink(client, 'change');
607
+
608
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
609
+ stream.eventSource.mockEmit('delete', {
610
+ data: '{"key":"flagKey","version":1}',
611
+ });
612
+
613
+ const changes = await receivedChange.take();
614
+ expect(changes).toEqual({
615
+ flagKey: { previous: true },
616
+ });
617
+ });
618
+ });
619
+
620
+ it('fires individual change event when flag is deleted', async () => {
621
+ await withClientAndServer({ bootstrap: { flagKey: true } }, async client => {
622
+ await client.waitForInitialization();
623
+
624
+ const receivedChange = eventSink(client, 'change:flagKey');
625
+
626
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
627
+ stream.eventSource.mockEmit('delete', {
628
+ data: '{"key":"flagKey","version":1}',
629
+ });
630
+
631
+ const args = await receivedChange.take();
632
+ expect(args).toEqual([undefined, true]);
633
+ });
634
+ });
635
+
636
+ it('updates local storage for delete message if using local storage', async () => {
637
+ platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}');
638
+
639
+ await withClientAndServer({ bootstrap: 'localstorage' }, async client => {
640
+ await client.waitForInitialization();
641
+ client.setStreaming(true);
642
+
643
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
644
+ stream.eventSource.mockEmit('delete', {
645
+ data: '{"key":"flagKey","version":1}',
646
+ });
647
+
648
+ expect(client.variation('flagKey')).toEqual(undefined);
649
+ const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey));
650
+ expect(storageData).toMatchObject({ flagKey: { version: 1, deleted: true } });
651
+ });
652
+ });
653
+
654
+ describe('emits error if malformed JSON is received', () => {
655
+ const doMalformedJsonEventTest = async (eventName, eventData) => {
656
+ // First, verify that there isn't an unhandled rejection if we're not listening for an error
657
+ await withClientAndServer({}, async client => {
658
+ await client.waitForInitialization();
659
+ client.setStreaming(true);
660
+
661
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
662
+ stream.eventSource.mockEmit(eventName, { data: eventData });
663
+ });
664
+
665
+ // Then, repeat the test using a listener to observe the error event
666
+ await withClientAndServer({}, async client => {
667
+ const errorEvents = new AsyncQueue();
668
+ client.on('error', e => errorEvents.add(e));
669
+
670
+ await client.waitForInitialization();
671
+ client.setStreaming(true);
672
+
673
+ const stream = await expectStreamConnecting(fullStreamUrlWithUser);
674
+ stream.eventSource.mockEmit(eventName, { data: eventData });
675
+
676
+ const e = await errorEvents.take();
677
+ expect(e.message).toEqual(messages.invalidData());
678
+ });
679
+ };
680
+
681
+ it('in put event', async () => doMalformedJsonEventTest('put', '{no'));
682
+ it('in patch event', async () => doMalformedJsonEventTest('patch', '{no'));
683
+ it('in delete event', async () => doMalformedJsonEventTest('delete', '{no'));
684
+ });
685
+
686
+ it('reconnects to stream if the user changes', async () => {
687
+ const user2 = { key: 'user2' };
688
+ const encodedUser2 = 'eyJrZXkiOiJ1c2VyMiJ9';
689
+ await withClientAndServer({}, async client => {
690
+ await client.waitForInitialization();
691
+ client.setStreaming(true);
692
+
693
+ await expectStreamConnecting(makeExpectedStreamUrl(encodedUser));
694
+
695
+ await client.identify(user2);
696
+ await expectStreamConnecting(makeExpectedStreamUrl(encodedUser2));
697
+ });
698
+ });
699
+
700
+ it('reconnects to stream with new hash value in secure mode if the user changes', async () => {
701
+ const newUser = { key: 'user2' };
702
+ const newEncodedUser = 'eyJrZXkiOiJ1c2VyMiJ9';
703
+ const newHash = hash + 'xxx';
704
+
705
+ await withClientAndServer({ hash }, async client => {
706
+ await client.waitForInitialization();
707
+ client.setStreaming(true);
708
+
709
+ await expectStreamConnecting(makeExpectedStreamUrl(encodedUser, hash));
710
+
711
+ await client.identify(newUser, newHash);
712
+ await expectStreamConnecting(makeExpectedStreamUrl(newEncodedUser, newHash));
713
+ });
714
+ });
715
+ });
716
+ });