launchdarkly-js-sdk-common 3.4.0 → 4.0.2

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 +24 -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 +5 -33
  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 +683 -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 +37 -0
  54. package/src/index.js +769 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +217 -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 +98 -45
  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
package/src/index.js ADDED
@@ -0,0 +1,769 @@
1
+ const EventProcessor = require('./EventProcessor');
2
+ const EventEmitter = require('./EventEmitter');
3
+ const EventSender = require('./EventSender');
4
+ const InitializationStateTracker = require('./InitializationState');
5
+ const PersistentFlagStore = require('./PersistentFlagStore');
6
+ const PersistentStorage = require('./PersistentStorage');
7
+ const Stream = require('./Stream');
8
+ const Requestor = require('./Requestor');
9
+ const Identity = require('./Identity');
10
+ const UserValidator = require('./UserValidator');
11
+ const configuration = require('./configuration');
12
+ const diagnostics = require('./diagnosticEvents');
13
+ const { commonBasicLogger } = require('./loggers');
14
+ const utils = require('./utils');
15
+ const errors = require('./errors');
16
+ const messages = require('./messages');
17
+
18
+ const changeEvent = 'change';
19
+ const internalChangeEvent = 'internal-change';
20
+
21
+ // This is called by the per-platform initialize functions to create the base client object that we
22
+ // may also extend with additional behavior. It returns an object with these properties:
23
+ // client: the actual client object
24
+ // options: the configuration (after any appropriate defaults have been applied)
25
+ // If we need to give the platform-specific clients access to any internals here, we should add those
26
+ // as properties of the return object, not public properties of the client.
27
+ //
28
+ // For definitions of the API in the platform object, see stubPlatform.js in the test code.
29
+
30
+ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
31
+ const logger = createLogger();
32
+ const emitter = EventEmitter(logger);
33
+ const initializationStateTracker = InitializationStateTracker(emitter);
34
+ const options = configuration.validate(specifiedOptions, emitter, extraOptionDefs, logger);
35
+ const sendEvents = options.sendEvents;
36
+ let environment = env;
37
+ let hash = options.hash;
38
+
39
+ const persistentStorage = PersistentStorage(platform.localStorage, logger);
40
+
41
+ const eventSender = EventSender(platform, environment, options);
42
+
43
+ const diagnosticsEnabled = options.sendEvents && !options.diagnosticOptOut;
44
+ const diagnosticId = diagnosticsEnabled ? diagnostics.DiagnosticId(environment) : null;
45
+ const diagnosticsAccumulator = diagnosticsEnabled ? diagnostics.DiagnosticsAccumulator(new Date().getTime()) : null;
46
+ const diagnosticsManager = diagnosticsEnabled
47
+ ? diagnostics.DiagnosticsManager(
48
+ platform,
49
+ persistentStorage,
50
+ diagnosticsAccumulator,
51
+ eventSender,
52
+ environment,
53
+ options,
54
+ diagnosticId
55
+ )
56
+ : null;
57
+
58
+ const stream = Stream(platform, options, environment, diagnosticsAccumulator);
59
+
60
+ const events =
61
+ options.eventProcessor ||
62
+ EventProcessor(platform, options, environment, diagnosticsAccumulator, emitter, eventSender);
63
+
64
+ const requestor = Requestor(platform, options, environment);
65
+
66
+ const seenRequests = {};
67
+ let flags = {};
68
+ let useLocalStorage;
69
+ let streamActive;
70
+ let streamForcedState = options.streaming;
71
+ let subscribedToChangeEvents;
72
+ let inited = false;
73
+ let closed = false;
74
+ let firstEvent = true;
75
+
76
+ // The "stateProvider" object is used in the Electron SDK, to allow one client instance to take partial
77
+ // control of another. If present, it has the following contract:
78
+ // - getInitialState() returns the initial client state if it is already available. The state is an
79
+ // object whose properties are "environment", "user", and "flags".
80
+ // - on("init", listener) triggers an event when the initial client state becomes available, passing
81
+ // the state object to the listener.
82
+ // - on("update", listener) triggers an event when flag values change and/or the current user changes.
83
+ // The parameter is an object that *may* contain "user" and/or "flags".
84
+ // - enqueueEvent(event) accepts an analytics event object and returns true if the stateProvider will
85
+ // be responsible for delivering it, or false if we still should deliver it ourselves.
86
+ const stateProvider = options.stateProvider;
87
+
88
+ const ident = Identity(null, onIdentifyChange);
89
+ const userValidator = UserValidator(persistentStorage);
90
+ const persistentFlagStore = persistentStorage.isEnabled()
91
+ ? new PersistentFlagStore(persistentStorage, environment, hash, ident, logger)
92
+ : null;
93
+
94
+ function createLogger() {
95
+ if (specifiedOptions && specifiedOptions.logger) {
96
+ return specifiedOptions.logger;
97
+ }
98
+ return (extraOptionDefs && extraOptionDefs.logger && extraOptionDefs.logger.default) || commonBasicLogger('warn');
99
+ }
100
+
101
+ function readFlagsFromBootstrap(data) {
102
+ // If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
103
+ // Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
104
+ // the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
105
+ const keys = Object.keys(data);
106
+ const metadataKey = '$flagsState';
107
+ const validKey = '$valid';
108
+ const metadata = data[metadataKey];
109
+ if (!metadata && keys.length) {
110
+ logger.warn(messages.bootstrapOldFormat());
111
+ }
112
+ if (data[validKey] === false) {
113
+ logger.warn(messages.bootstrapInvalid());
114
+ }
115
+ const ret = {};
116
+ keys.forEach(key => {
117
+ if (key !== metadataKey && key !== validKey) {
118
+ let flag = { value: data[key] };
119
+ if (metadata && metadata[key]) {
120
+ flag = utils.extend(flag, metadata[key]);
121
+ } else {
122
+ flag.version = 0;
123
+ }
124
+ ret[key] = flag;
125
+ }
126
+ });
127
+ return ret;
128
+ }
129
+
130
+ function shouldEnqueueEvent() {
131
+ return sendEvents && !closed && !platform.isDoNotTrack();
132
+ }
133
+
134
+ function enqueueEvent(event) {
135
+ if (!environment) {
136
+ // We're in paired mode and haven't been initialized with an environment or user yet
137
+ return;
138
+ }
139
+ if (stateProvider && stateProvider.enqueueEvent && stateProvider.enqueueEvent(event)) {
140
+ return; // it'll be handled elsewhere
141
+ }
142
+ if (event.kind !== 'alias') {
143
+ if (!event.user) {
144
+ if (firstEvent) {
145
+ logger.warn(messages.eventWithoutUser());
146
+ firstEvent = false;
147
+ }
148
+ return;
149
+ }
150
+ firstEvent = false;
151
+ }
152
+ if (shouldEnqueueEvent()) {
153
+ logger.debug(messages.debugEnqueueingEvent(event.kind));
154
+ events.enqueue(event);
155
+ }
156
+ }
157
+
158
+ function onIdentifyChange(user, previousUser) {
159
+ sendIdentifyEvent(user);
160
+ if (!options.autoAliasingOptOut && previousUser && previousUser.anonymous && user && !user.anonymous) {
161
+ alias(user, previousUser);
162
+ }
163
+ }
164
+
165
+ function sendIdentifyEvent(user) {
166
+ if (stateProvider) {
167
+ // In paired mode, the other client is responsible for sending identify events
168
+ return;
169
+ }
170
+ if (user) {
171
+ enqueueEvent({
172
+ kind: 'identify',
173
+ key: user.key,
174
+ user: user,
175
+ creationDate: new Date().getTime(),
176
+ });
177
+ }
178
+ }
179
+
180
+ function sendFlagEvent(key, detail, defaultValue, includeReason) {
181
+ const user = ident.getUser();
182
+ const now = new Date();
183
+ const value = detail ? detail.value : null;
184
+ if (!options.allowFrequentDuplicateEvents) {
185
+ const cacheKey = JSON.stringify(value) + (user && user.key ? user.key : '') + key; // see below
186
+ const cached = seenRequests[cacheKey];
187
+ // cache TTL is five minutes
188
+ if (cached && now - cached < 300000) {
189
+ return;
190
+ }
191
+ seenRequests[cacheKey] = now;
192
+ }
193
+
194
+ const event = {
195
+ kind: 'feature',
196
+ key: key,
197
+ user: user,
198
+ value: value,
199
+ variation: detail ? detail.variationIndex : null,
200
+ default: defaultValue,
201
+ creationDate: now.getTime(),
202
+ };
203
+ if (user && user.anonymous) {
204
+ event.contextKind = userContextKind(user);
205
+ }
206
+ const flag = flags[key];
207
+ if (flag) {
208
+ event.version = flag.flagVersion ? flag.flagVersion : flag.version;
209
+ event.trackEvents = flag.trackEvents;
210
+ event.debugEventsUntilDate = flag.debugEventsUntilDate;
211
+ }
212
+ if ((includeReason || (flag && flag.trackReason)) && detail) {
213
+ event.reason = detail.reason;
214
+ }
215
+
216
+ enqueueEvent(event);
217
+ }
218
+
219
+ function identify(user, newHash, onDone) {
220
+ if (closed) {
221
+ return utils.wrapPromiseCallback(Promise.resolve({}), onDone);
222
+ }
223
+ if (stateProvider) {
224
+ // We're being controlled by another client instance, so only that instance is allowed to change the user
225
+ logger.warn(messages.identifyDisabled());
226
+ return utils.wrapPromiseCallback(Promise.resolve(utils.transformVersionedValuesToValues(flags)), onDone);
227
+ }
228
+ const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve();
229
+ return utils.wrapPromiseCallback(
230
+ clearFirst
231
+ .then(() => userValidator.validateUser(user))
232
+ .then(realUser =>
233
+ requestor
234
+ .fetchFlagSettings(realUser, newHash)
235
+ // the following then() is nested within this one so we can use realUser from the previous closure
236
+ .then(requestedFlags => {
237
+ const flagValueMap = utils.transformVersionedValuesToValues(requestedFlags);
238
+ ident.setUser(realUser);
239
+ hash = newHash;
240
+ if (requestedFlags) {
241
+ return replaceAllFlags(requestedFlags).then(() => flagValueMap);
242
+ } else {
243
+ return flagValueMap;
244
+ }
245
+ })
246
+ )
247
+ .then(flagValueMap => {
248
+ if (streamActive) {
249
+ connectStream();
250
+ }
251
+ return flagValueMap;
252
+ })
253
+ .catch(err => {
254
+ emitter.maybeReportError(err);
255
+ return Promise.reject(err);
256
+ }),
257
+ onDone
258
+ );
259
+ }
260
+
261
+ function getUser() {
262
+ return ident.getUser();
263
+ }
264
+
265
+ function flush(onDone) {
266
+ return utils.wrapPromiseCallback(sendEvents ? events.flush() : Promise.resolve(), onDone);
267
+ }
268
+
269
+ function variation(key, defaultValue) {
270
+ return variationDetailInternal(key, defaultValue, true, false).value;
271
+ }
272
+
273
+ function variationDetail(key, defaultValue) {
274
+ return variationDetailInternal(key, defaultValue, true, true);
275
+ }
276
+
277
+ function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent) {
278
+ let detail;
279
+
280
+ if (flags && utils.objectHasOwnProperty(flags, key) && flags[key] && !flags[key].deleted) {
281
+ const flag = flags[key];
282
+ detail = getFlagDetail(flag);
283
+ if (flag.value === null || flag.value === undefined) {
284
+ detail.value = defaultValue;
285
+ }
286
+ } else {
287
+ detail = { value: defaultValue, variationIndex: null, reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' } };
288
+ }
289
+
290
+ if (sendEvent) {
291
+ sendFlagEvent(key, detail, defaultValue, includeReasonInEvent);
292
+ }
293
+
294
+ return detail;
295
+ }
296
+
297
+ function getFlagDetail(flag) {
298
+ return {
299
+ value: flag.value,
300
+ variationIndex: flag.variation === undefined ? null : flag.variation,
301
+ reason: flag.reason || null,
302
+ };
303
+ // Note, the logic above ensures that variationIndex and reason will always be null rather than
304
+ // undefined if we don't have values for them. That's just to avoid subtle errors that depend on
305
+ // whether an object was JSON-encoded with null properties omitted or not.
306
+ }
307
+
308
+ function allFlags() {
309
+ const results = {};
310
+
311
+ if (!flags) {
312
+ return results;
313
+ }
314
+
315
+ for (const key in flags) {
316
+ if (utils.objectHasOwnProperty(flags, key)) {
317
+ results[key] = variationDetailInternal(key, null, !options.sendEventsOnlyForVariation).value;
318
+ }
319
+ }
320
+
321
+ return results;
322
+ }
323
+
324
+ function userContextKind(user) {
325
+ return user.anonymous ? 'anonymousUser' : 'user';
326
+ }
327
+
328
+ function alias(user, previousUser) {
329
+ if (stateProvider) {
330
+ // In paired mode, the other client is responsible for sending alias events
331
+ return;
332
+ }
333
+
334
+ if (!user || !previousUser) {
335
+ return;
336
+ }
337
+
338
+ enqueueEvent({
339
+ kind: 'alias',
340
+ key: user.key,
341
+ contextKind: userContextKind(user),
342
+ previousKey: previousUser.key,
343
+ previousContextKind: userContextKind(previousUser),
344
+ creationDate: new Date().getTime(),
345
+ });
346
+ }
347
+
348
+ function track(key, data, metricValue) {
349
+ if (typeof key !== 'string') {
350
+ emitter.maybeReportError(new errors.LDInvalidEventKeyError(messages.unknownCustomEventKey(key)));
351
+ return;
352
+ }
353
+
354
+ // The following logic was used only for the JS browser SDK (js-client-sdk) and
355
+ // is no longer needed as of version 2.9.13 of that SDK. The other client-side
356
+ // JS-based SDKs did not define customEventFilter, and now none of them do. We
357
+ // can remove this in the next major version of the common code, when it's OK to
358
+ // make breaking changes to our internal API contracts.
359
+ if (platform.customEventFilter && !platform.customEventFilter(key)) {
360
+ logger.warn(messages.unknownCustomEventKey(key));
361
+ }
362
+
363
+ const user = ident.getUser();
364
+ const e = {
365
+ kind: 'custom',
366
+ key: key,
367
+ user: user,
368
+ url: platform.getCurrentUrl(),
369
+ creationDate: new Date().getTime(),
370
+ };
371
+ if (user && user.anonymous) {
372
+ e.contextKind = userContextKind(user);
373
+ }
374
+ // Note, check specifically for null/undefined because it is legal to set these fields to a falsey value.
375
+ if (data !== null && data !== undefined) {
376
+ e.data = data;
377
+ }
378
+ if (metricValue !== null && metricValue !== undefined) {
379
+ e.metricValue = metricValue;
380
+ }
381
+ enqueueEvent(e);
382
+ }
383
+
384
+ function connectStream() {
385
+ streamActive = true;
386
+ if (!ident.getUser()) {
387
+ return;
388
+ }
389
+ stream.connect(ident.getUser(), hash, {
390
+ ping: function() {
391
+ logger.debug(messages.debugStreamPing());
392
+ const userAtTimeOfPingEvent = ident.getUser();
393
+ requestor
394
+ .fetchFlagSettings(userAtTimeOfPingEvent, hash)
395
+ .then(requestedFlags => {
396
+ // Check whether the current user is still the same - we don't want to overwrite the flags if
397
+ // the application has called identify() while this request was in progress
398
+ if (utils.deepEquals(userAtTimeOfPingEvent, ident.getUser())) {
399
+ replaceAllFlags(requestedFlags || {});
400
+ }
401
+ })
402
+ .catch(err => {
403
+ emitter.maybeReportError(new errors.LDFlagFetchError(messages.errorFetchingFlags(err)));
404
+ });
405
+ },
406
+ put: function(e) {
407
+ const data = JSON.parse(e.data);
408
+ logger.debug(messages.debugStreamPut());
409
+ replaceAllFlags(data); // don't wait for this Promise to be resolved
410
+ },
411
+ patch: function(e) {
412
+ const data = JSON.parse(e.data);
413
+ // If both the flag and the patch have a version property, then the patch version must be
414
+ // greater than the flag version for us to accept the patch. If either one has no version
415
+ // then the patch always succeeds.
416
+ const oldFlag = flags[data.key];
417
+ if (!oldFlag || !oldFlag.version || !data.version || oldFlag.version < data.version) {
418
+ logger.debug(messages.debugStreamPatch(data.key));
419
+ const mods = {};
420
+ const newFlag = utils.extend({}, data);
421
+ delete newFlag['key'];
422
+ flags[data.key] = newFlag;
423
+ const newDetail = getFlagDetail(newFlag);
424
+ if (oldFlag) {
425
+ mods[data.key] = { previous: oldFlag.value, current: newDetail };
426
+ } else {
427
+ mods[data.key] = { current: newDetail };
428
+ }
429
+ handleFlagChanges(mods); // don't wait for this Promise to be resolved
430
+ } else {
431
+ logger.debug(messages.debugStreamPatchIgnored(data.key));
432
+ }
433
+ },
434
+ delete: function(e) {
435
+ const data = JSON.parse(e.data);
436
+ if (!flags[data.key] || flags[data.key].version < data.version) {
437
+ logger.debug(messages.debugStreamDelete(data.key));
438
+ const mods = {};
439
+ if (flags[data.key] && !flags[data.key].deleted) {
440
+ mods[data.key] = { previous: flags[data.key].value };
441
+ }
442
+ flags[data.key] = { version: data.version, deleted: true };
443
+ handleFlagChanges(mods); // don't wait for this Promise to be resolved
444
+ } else {
445
+ logger.debug(messages.debugStreamDeleteIgnored(data.key));
446
+ }
447
+ },
448
+ });
449
+ }
450
+
451
+ function disconnectStream() {
452
+ if (streamActive) {
453
+ stream.disconnect();
454
+ streamActive = false;
455
+ }
456
+ }
457
+
458
+ // Returns a Promise which will be resolved when we have completely updated the internal flags state,
459
+ // dispatched all change events, and updated local storage if appropriate. This Promise is guaranteed
460
+ // never to have an unhandled rejection.
461
+ function replaceAllFlags(newFlags) {
462
+ const changes = {};
463
+
464
+ if (!newFlags) {
465
+ return Promise.resolve();
466
+ }
467
+
468
+ for (const key in flags) {
469
+ if (utils.objectHasOwnProperty(flags, key) && flags[key]) {
470
+ if (newFlags[key] && !utils.deepEquals(newFlags[key].value, flags[key].value)) {
471
+ changes[key] = { previous: flags[key].value, current: getFlagDetail(newFlags[key]) };
472
+ } else if (!newFlags[key] || newFlags[key].deleted) {
473
+ changes[key] = { previous: flags[key].value };
474
+ }
475
+ }
476
+ }
477
+ for (const key in newFlags) {
478
+ if (utils.objectHasOwnProperty(newFlags, key) && newFlags[key] && (!flags[key] || flags[key].deleted)) {
479
+ changes[key] = { current: getFlagDetail(newFlags[key]) };
480
+ }
481
+ }
482
+
483
+ flags = { ...newFlags };
484
+ return handleFlagChanges(changes).catch(() => {}); // swallow any exceptions from this Promise
485
+ }
486
+
487
+ // Returns a Promise which will be resolved when we have dispatched all change events and updated
488
+ // local storage if appropriate.
489
+ function handleFlagChanges(changes) {
490
+ const keys = Object.keys(changes);
491
+
492
+ if (keys.length > 0) {
493
+ const changeEventParams = {};
494
+ keys.forEach(key => {
495
+ const current = changes[key].current;
496
+ const value = current ? current.value : undefined;
497
+ const previous = changes[key].previous;
498
+ emitter.emit(changeEvent + ':' + key, value, previous);
499
+ changeEventParams[key] = current ? { current: value, previous: previous } : { previous: previous };
500
+ });
501
+
502
+ emitter.emit(changeEvent, changeEventParams);
503
+ emitter.emit(internalChangeEvent, flags);
504
+
505
+ // By default, we send feature evaluation events whenever we have received new flag values -
506
+ // the client has in effect evaluated these flags just by receiving them. This can be suppressed
507
+ // by setting "sendEventsOnlyForVariation". Also, if we have a stateProvider, we don't send these
508
+ // events because we assume they have already been sent by the other client that gave us the flags
509
+ // (when it received them in the first place).
510
+ if (!options.sendEventsOnlyForVariation && !stateProvider) {
511
+ keys.forEach(key => {
512
+ sendFlagEvent(key, changes[key].current);
513
+ });
514
+ }
515
+ }
516
+
517
+ if (useLocalStorage && persistentFlagStore) {
518
+ return persistentFlagStore.saveFlags(flags);
519
+ } else {
520
+ return Promise.resolve();
521
+ }
522
+ }
523
+
524
+ function on(event, handler, context) {
525
+ if (isChangeEventKey(event)) {
526
+ subscribedToChangeEvents = true;
527
+ if (inited) {
528
+ updateStreamingState();
529
+ }
530
+ emitter.on(event, handler, context);
531
+ } else {
532
+ emitter.on(...arguments);
533
+ }
534
+ }
535
+
536
+ function off(event) {
537
+ emitter.off(...arguments);
538
+ if (isChangeEventKey(event)) {
539
+ let haveListeners = false;
540
+ emitter.getEvents().forEach(key => {
541
+ if (isChangeEventKey(key) && emitter.getEventListenerCount(key) > 0) {
542
+ haveListeners = true;
543
+ }
544
+ });
545
+ if (!haveListeners) {
546
+ subscribedToChangeEvents = false;
547
+ if (streamActive && streamForcedState === undefined) {
548
+ disconnectStream();
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ function setStreaming(state) {
555
+ const newState = state === null ? undefined : state;
556
+ if (newState !== streamForcedState) {
557
+ streamForcedState = newState;
558
+ updateStreamingState();
559
+ }
560
+ }
561
+
562
+ function updateStreamingState() {
563
+ const shouldBeStreaming = streamForcedState || (subscribedToChangeEvents && streamForcedState === undefined);
564
+ if (shouldBeStreaming && !streamActive) {
565
+ connectStream();
566
+ } else if (!shouldBeStreaming && streamActive) {
567
+ disconnectStream();
568
+ }
569
+ if (diagnosticsManager) {
570
+ diagnosticsManager.setStreaming(shouldBeStreaming);
571
+ }
572
+ }
573
+
574
+ function isChangeEventKey(event) {
575
+ return event === changeEvent || event.substr(0, changeEvent.length + 1) === changeEvent + ':';
576
+ }
577
+
578
+ if (typeof options.bootstrap === 'string' && options.bootstrap.toUpperCase() === 'LOCALSTORAGE') {
579
+ if (persistentFlagStore) {
580
+ useLocalStorage = true;
581
+ } else {
582
+ logger.warn(messages.localStorageUnavailable());
583
+ }
584
+ }
585
+
586
+ if (typeof options.bootstrap === 'object') {
587
+ // Set the flags as soon as possible before we get into any async code, so application code can read
588
+ // them even if the ready event has not yet fired.
589
+ flags = readFlagsFromBootstrap(options.bootstrap);
590
+ }
591
+
592
+ if (stateProvider) {
593
+ // The stateProvider option is used in the Electron SDK, to allow a client instance in the main process
594
+ // to control another client instance (i.e. this one) in the renderer process. We can't predict which
595
+ // one will start up first, so the initial state may already be available for us or we may have to wait
596
+ // to receive it.
597
+ const state = stateProvider.getInitialState();
598
+ if (state) {
599
+ initFromStateProvider(state);
600
+ } else {
601
+ stateProvider.on('init', initFromStateProvider);
602
+ }
603
+ stateProvider.on('update', updateFromStateProvider);
604
+ } else {
605
+ finishInit().catch(signalFailedInit);
606
+ }
607
+
608
+ function finishInit() {
609
+ if (!env) {
610
+ return Promise.reject(new errors.LDInvalidEnvironmentIdError(messages.environmentNotSpecified()));
611
+ }
612
+ return userValidator.validateUser(user).then(realUser => {
613
+ ident.setUser(realUser);
614
+ if (typeof options.bootstrap === 'object') {
615
+ // flags have already been set earlier
616
+ return signalSuccessfulInit();
617
+ } else if (useLocalStorage) {
618
+ return finishInitWithLocalStorage();
619
+ } else {
620
+ return finishInitWithPolling();
621
+ }
622
+ });
623
+ }
624
+
625
+ function finishInitWithLocalStorage() {
626
+ return persistentFlagStore.loadFlags().then(storedFlags => {
627
+ if (storedFlags === null || storedFlags === undefined) {
628
+ flags = {};
629
+ return requestor
630
+ .fetchFlagSettings(ident.getUser(), hash)
631
+ .then(requestedFlags => replaceAllFlags(requestedFlags || {}))
632
+ .then(signalSuccessfulInit)
633
+ .catch(err => {
634
+ const initErr = new errors.LDFlagFetchError(messages.errorFetchingFlags(err));
635
+ signalFailedInit(initErr);
636
+ });
637
+ } else {
638
+ // We're reading the flags from local storage. Signal that we're ready,
639
+ // then update localStorage for the next page load. We won't signal changes or update
640
+ // the in-memory flags unless you subscribe for changes
641
+ flags = storedFlags;
642
+ utils.onNextTick(signalSuccessfulInit);
643
+
644
+ return requestor
645
+ .fetchFlagSettings(ident.getUser(), hash)
646
+ .then(requestedFlags => replaceAllFlags(requestedFlags))
647
+ .catch(err => emitter.maybeReportError(err));
648
+ }
649
+ });
650
+ }
651
+
652
+ function finishInitWithPolling() {
653
+ return requestor
654
+ .fetchFlagSettings(ident.getUser(), hash)
655
+ .then(requestedFlags => {
656
+ flags = requestedFlags || {};
657
+ // Note, we don't need to call updateSettings here because local storage and change events are not relevant
658
+ signalSuccessfulInit();
659
+ })
660
+ .catch(err => {
661
+ flags = {};
662
+ signalFailedInit(err);
663
+ });
664
+ }
665
+
666
+ function initFromStateProvider(state) {
667
+ environment = state.environment;
668
+ ident.setUser(state.user);
669
+ flags = { ...state.flags };
670
+ utils.onNextTick(signalSuccessfulInit);
671
+ }
672
+
673
+ function updateFromStateProvider(state) {
674
+ if (state.user) {
675
+ ident.setUser(state.user);
676
+ }
677
+ if (state.flags) {
678
+ replaceAllFlags(state.flags); // don't wait for this Promise to be resolved
679
+ }
680
+ }
681
+
682
+ function signalSuccessfulInit() {
683
+ logger.info(messages.clientInitialized());
684
+ inited = true;
685
+ updateStreamingState();
686
+ initializationStateTracker.signalSuccess();
687
+ }
688
+
689
+ function signalFailedInit(err) {
690
+ initializationStateTracker.signalFailure(err);
691
+ }
692
+
693
+ function start() {
694
+ if (sendEvents) {
695
+ if (diagnosticsManager) {
696
+ diagnosticsManager.start();
697
+ }
698
+ events.start();
699
+ }
700
+ }
701
+
702
+ function close(onDone) {
703
+ if (closed) {
704
+ return utils.wrapPromiseCallback(Promise.resolve(), onDone);
705
+ }
706
+ const finishClose = () => {
707
+ closed = true;
708
+ flags = {};
709
+ };
710
+ const p = Promise.resolve()
711
+ .then(() => {
712
+ disconnectStream();
713
+ if (diagnosticsManager) {
714
+ diagnosticsManager.stop();
715
+ }
716
+ if (sendEvents) {
717
+ events.stop();
718
+ return events.flush();
719
+ }
720
+ })
721
+ .then(finishClose)
722
+ .catch(finishClose);
723
+ return utils.wrapPromiseCallback(p, onDone);
724
+ }
725
+
726
+ function getFlagsInternal() {
727
+ // used by Electron integration
728
+ return flags;
729
+ }
730
+
731
+ const client = {
732
+ waitForInitialization: () => initializationStateTracker.getInitializationPromise(),
733
+ waitUntilReady: () => initializationStateTracker.getReadyPromise(),
734
+ identify: identify,
735
+ getUser: getUser,
736
+ variation: variation,
737
+ variationDetail: variationDetail,
738
+ track: track,
739
+ alias: alias,
740
+ on: on,
741
+ off: off,
742
+ setStreaming: setStreaming,
743
+ flush: flush,
744
+ allFlags: allFlags,
745
+ close: close,
746
+ };
747
+
748
+ return {
749
+ client: client, // The client object containing all public methods.
750
+ options: options, // The validated configuration object, including all defaults.
751
+ emitter: emitter, // The event emitter which can be used to log errors or trigger events.
752
+ ident: ident, // The Identity object that manages the current user.
753
+ logger: logger, // The logging abstraction.
754
+ requestor: requestor, // The Requestor object.
755
+ start: start, // Starts the client once the environment is ready.
756
+ enqueueEvent: enqueueEvent, // Puts an analytics event in the queue, if event sending is enabled.
757
+ getFlagsInternal: getFlagsInternal, // Returns flag data structure with all details.
758
+ getEnvironmentId: () => environment, // Gets the environment ID (this may have changed since initialization, if we have a state provider)
759
+ internalChangeEventName: internalChangeEvent, // This event is triggered whenever we have new flag state.
760
+ };
761
+ }
762
+
763
+ module.exports = {
764
+ initialize,
765
+ commonBasicLogger,
766
+ errors,
767
+ messages,
768
+ utils,
769
+ };