@launchdarkly/js-client-sdk-common 1.9.0 → 1.9.1-beta.1
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.
- package/dist/DataManager.d.ts +6 -5
- package/dist/DataManager.d.ts.map +1 -1
- package/dist/HookRunner.d.ts +3 -3
- package/dist/HookRunner.d.ts.map +1 -1
- package/dist/LDClientImpl.d.ts +19 -17
- package/dist/LDClientImpl.d.ts.map +1 -1
- package/dist/LDEmitter.d.ts +4 -4
- package/dist/LDEmitter.d.ts.map +1 -1
- package/dist/api/LDClient.d.ts +1 -1
- package/dist/api/LDClient.d.ts.map +1 -1
- package/dist/api/LDInspection.d.ts +105 -0
- package/dist/api/LDInspection.d.ts.map +1 -0
- package/dist/api/LDOptions.d.ts +12 -2
- package/dist/api/LDOptions.d.ts.map +1 -1
- package/dist/configuration/Configuration.d.ts +4 -1
- package/dist/configuration/Configuration.d.ts.map +1 -1
- package/dist/configuration/validators.d.ts.map +1 -1
- package/dist/context/addAutoEnv.d.ts +2 -2
- package/dist/context/addAutoEnv.d.ts.map +1 -1
- package/dist/context/ensureKey.d.ts +1 -1
- package/dist/context/ensureKey.d.ts.map +1 -1
- package/dist/{streaming → datasource}/DataSourceConfig.d.ts +1 -0
- package/dist/datasource/DataSourceConfig.d.ts.map +1 -0
- package/dist/datasource/DataSourceEventHandler.d.ts +4 -4
- package/dist/datasource/DataSourceEventHandler.d.ts.map +1 -1
- package/dist/datasource/DataSourceStatusManager.d.ts +7 -7
- package/dist/datasource/DataSourceStatusManager.d.ts.map +1 -1
- package/dist/datasource/Requestor.d.ts +26 -0
- package/dist/datasource/Requestor.d.ts.map +1 -0
- package/dist/evaluation/evaluationDetail.d.ts.map +1 -1
- package/dist/flag-manager/FlagManager.d.ts +4 -5
- package/dist/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/flag-manager/FlagPersistence.d.ts +13 -13
- package/dist/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/flag-manager/FlagStore.d.ts +1 -1
- package/dist/flag-manager/FlagStore.d.ts.map +1 -1
- package/dist/flag-manager/FlagUpdater.d.ts +6 -5
- package/dist/flag-manager/FlagUpdater.d.ts.map +1 -1
- package/dist/index.cjs +581 -374
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +582 -376
- package/dist/index.mjs.map +1 -1
- package/dist/inspection/InspectorManager.d.ts +41 -0
- package/dist/inspection/InspectorManager.d.ts.map +1 -0
- package/dist/inspection/createSafeInspector.d.ts +8 -0
- package/dist/inspection/createSafeInspector.d.ts.map +1 -0
- package/dist/inspection/getInspectorHook.d.ts +4 -0
- package/dist/inspection/getInspectorHook.d.ts.map +1 -0
- package/dist/inspection/messages.d.ts +3 -0
- package/dist/inspection/messages.d.ts.map +1 -0
- package/dist/polling/PollingProcessor.d.ts.map +1 -1
- package/dist/streaming/StreamingProcessor.d.ts +18 -16
- package/dist/streaming/StreamingProcessor.d.ts.map +1 -1
- package/dist/streaming/index.d.ts +1 -1
- package/dist/streaming/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/polling/Requestor.d.ts +0 -21
- package/dist/polling/Requestor.d.ts.map +0 -1
- package/dist/streaming/DataSourceConfig.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TypeValidators, createSafeLogger, ServiceEndpoints, ApplicationTags, OptionMessages, NumberWithMinimum, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, timedPromise, AutoEnvAttributes, LDClientError,
|
|
1
|
+
import { getPollingUri, TypeValidators, createSafeLogger, ServiceEndpoints, ApplicationTags, OptionMessages, NumberWithMinimum, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, timedPromise, AutoEnvAttributes, LDClientError, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
|
|
2
2
|
export * from '@launchdarkly/js-sdk-common';
|
|
3
3
|
import * as jsSdkCommon from '@launchdarkly/js-sdk-common';
|
|
4
4
|
export { jsSdkCommon as platform };
|
|
@@ -14,6 +14,72 @@ var DataSourceState;
|
|
|
14
14
|
// NetworkUnavailable,
|
|
15
15
|
})(DataSourceState || (DataSourceState = {}));
|
|
16
16
|
|
|
17
|
+
// eslint-disable-next-line max-classes-per-file
|
|
18
|
+
function isOk(status) {
|
|
19
|
+
return status >= 200 && status <= 299;
|
|
20
|
+
}
|
|
21
|
+
class LDRequestError extends Error {
|
|
22
|
+
constructor(message, status) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.name = 'LaunchDarklyRequestError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Note: The requestor is implemented independently from polling such that it can be used to
|
|
30
|
+
* make a one-off request.
|
|
31
|
+
*/
|
|
32
|
+
class Requestor {
|
|
33
|
+
constructor(_requests, _uri, _headers, _method, _body) {
|
|
34
|
+
this._requests = _requests;
|
|
35
|
+
this._uri = _uri;
|
|
36
|
+
this._headers = _headers;
|
|
37
|
+
this._method = _method;
|
|
38
|
+
this._body = _body;
|
|
39
|
+
}
|
|
40
|
+
async requestPayload() {
|
|
41
|
+
let status;
|
|
42
|
+
try {
|
|
43
|
+
const res = await this._requests.fetch(this._uri, {
|
|
44
|
+
method: this._method,
|
|
45
|
+
headers: this._headers,
|
|
46
|
+
body: this._body,
|
|
47
|
+
});
|
|
48
|
+
if (isOk(res.status)) {
|
|
49
|
+
return await res.text();
|
|
50
|
+
}
|
|
51
|
+
// Assigning so it can be thrown after the try/catch.
|
|
52
|
+
status = res.status;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw new LDRequestError(err?.message);
|
|
56
|
+
}
|
|
57
|
+
throw new LDRequestError(`Unexpected status code: ${status}`, status);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function makeRequestor(plainContextString, serviceEndpoints, paths, requests, encoding, baseHeaders, baseQueryParams, withReasons, useReport, secureModeHash) {
|
|
61
|
+
let body;
|
|
62
|
+
let method = 'GET';
|
|
63
|
+
const headers = { ...baseHeaders };
|
|
64
|
+
if (useReport) {
|
|
65
|
+
method = 'REPORT';
|
|
66
|
+
headers['content-type'] = 'application/json';
|
|
67
|
+
body = plainContextString; // context is in body for REPORT
|
|
68
|
+
}
|
|
69
|
+
const path = useReport
|
|
70
|
+
? paths.pathReport(encoding, plainContextString)
|
|
71
|
+
: paths.pathGet(encoding, plainContextString);
|
|
72
|
+
const parameters = [...(baseQueryParams ?? [])];
|
|
73
|
+
if (withReasons) {
|
|
74
|
+
parameters.push({ key: 'withReasons', value: 'true' });
|
|
75
|
+
}
|
|
76
|
+
if (secureModeHash) {
|
|
77
|
+
parameters.push({ key: 'h', value: secureModeHash });
|
|
78
|
+
}
|
|
79
|
+
const uri = getPollingUri(serviceEndpoints, path, parameters);
|
|
80
|
+
return new Requestor(requests, uri, headers, method, body);
|
|
81
|
+
}
|
|
82
|
+
|
|
17
83
|
// eslint-disable-next-line max-classes-per-file
|
|
18
84
|
const validators = {
|
|
19
85
|
logger: TypeValidators.Object,
|
|
@@ -38,6 +104,7 @@ const validators = {
|
|
|
38
104
|
wrapperVersion: TypeValidators.String,
|
|
39
105
|
payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
|
|
40
106
|
hooks: TypeValidators.createTypeArray('Hook[]', {}),
|
|
107
|
+
inspectors: TypeValidators.createTypeArray('LDInspection', {}),
|
|
41
108
|
};
|
|
42
109
|
|
|
43
110
|
const DEFAULT_POLLING_INTERVAL = 60 * 5;
|
|
@@ -53,8 +120,13 @@ function ensureSafeLogger(logger) {
|
|
|
53
120
|
class ConfigurationImpl {
|
|
54
121
|
constructor(pristineOptions = {}, internalOptions = {}) {
|
|
55
122
|
this.logger = createSafeLogger();
|
|
123
|
+
// Naming conventions is not followed for these lines because the config validation
|
|
124
|
+
// accesses members based on the keys of the options. (sdk-763)
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
56
126
|
this.baseUri = DEFAULT_POLLING;
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
57
128
|
this.eventsUri = ServiceEndpoints.DEFAULT_EVENTS;
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
58
130
|
this.streamUri = DEFAULT_STREAM;
|
|
59
131
|
this.maxCachedContexts = 5;
|
|
60
132
|
this.capacity = 100;
|
|
@@ -71,8 +143,9 @@ class ConfigurationImpl {
|
|
|
71
143
|
this.privateAttributes = [];
|
|
72
144
|
this.pollInterval = DEFAULT_POLLING_INTERVAL;
|
|
73
145
|
this.hooks = [];
|
|
146
|
+
this.inspectors = [];
|
|
74
147
|
this.logger = ensureSafeLogger(pristineOptions.logger);
|
|
75
|
-
const errors = this.
|
|
148
|
+
const errors = this._validateTypesAndNames(pristineOptions);
|
|
76
149
|
errors.forEach((e) => this.logger.warn(e));
|
|
77
150
|
this.serviceEndpoints = new ServiceEndpoints(this.streamUri, this.baseUri, this.eventsUri, internalOptions.analyticsEventPath, internalOptions.diagnosticEventPath, internalOptions.includeAuthorizationHeader, pristineOptions.payloadFilterKey);
|
|
78
151
|
this.useReport = pristineOptions.useReport ?? false;
|
|
@@ -80,7 +153,7 @@ class ConfigurationImpl {
|
|
|
80
153
|
this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent';
|
|
81
154
|
this.trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event);
|
|
82
155
|
}
|
|
83
|
-
|
|
156
|
+
_validateTypesAndNames(pristineOptions) {
|
|
84
157
|
const errors = [];
|
|
85
158
|
Object.entries(pristineOptions).forEach(([k, v]) => {
|
|
86
159
|
const validator = validators[k];
|
|
@@ -388,11 +461,12 @@ function createErrorEvaluationDetail(errorKind, def) {
|
|
|
388
461
|
};
|
|
389
462
|
}
|
|
390
463
|
function createSuccessEvaluationDetail(value, variationIndex, reason) {
|
|
391
|
-
|
|
464
|
+
const res = {
|
|
392
465
|
value,
|
|
393
466
|
variationIndex: variationIndex ?? null,
|
|
394
467
|
reason: reason ?? null,
|
|
395
468
|
};
|
|
469
|
+
return res;
|
|
396
470
|
}
|
|
397
471
|
|
|
398
472
|
const createEventProcessor = (clientSideID, config, platform, baseHeaders, diagnosticsManager) => {
|
|
@@ -490,23 +564,23 @@ class ContextIndex {
|
|
|
490
564
|
* then persists changes after the updater has completed.
|
|
491
565
|
*/
|
|
492
566
|
class FlagPersistence {
|
|
493
|
-
constructor(
|
|
494
|
-
this.
|
|
495
|
-
this.
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
498
|
-
this.
|
|
499
|
-
this.
|
|
500
|
-
this.
|
|
501
|
-
this.
|
|
567
|
+
constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
568
|
+
this._platform = _platform;
|
|
569
|
+
this._environmentNamespace = _environmentNamespace;
|
|
570
|
+
this._maxCachedContexts = _maxCachedContexts;
|
|
571
|
+
this._flagStore = _flagStore;
|
|
572
|
+
this._flagUpdater = _flagUpdater;
|
|
573
|
+
this._logger = _logger;
|
|
574
|
+
this._timeStamper = _timeStamper;
|
|
575
|
+
this._indexKeyPromise = namespaceForContextIndex(this._environmentNamespace);
|
|
502
576
|
}
|
|
503
577
|
/**
|
|
504
578
|
* Inits flag persistence for the provided context with the provided flags. This will result
|
|
505
579
|
* in the underlying {@link FlagUpdater} switching its active context.
|
|
506
580
|
*/
|
|
507
581
|
async init(context, newFlags) {
|
|
508
|
-
this.
|
|
509
|
-
await this.
|
|
582
|
+
this._flagUpdater.init(context, newFlags);
|
|
583
|
+
await this._storeCache(context);
|
|
510
584
|
}
|
|
511
585
|
/**
|
|
512
586
|
* Upserts a flag into the {@link FlagUpdater} and stores that to persistence if the upsert
|
|
@@ -514,8 +588,8 @@ class FlagPersistence {
|
|
|
514
588
|
* the active context.
|
|
515
589
|
*/
|
|
516
590
|
async upsert(context, key, item) {
|
|
517
|
-
if (this.
|
|
518
|
-
await this.
|
|
591
|
+
if (this._flagUpdater.upsert(context, key, item)) {
|
|
592
|
+
await this._storeCache(context);
|
|
519
593
|
return true;
|
|
520
594
|
}
|
|
521
595
|
return false;
|
|
@@ -525,19 +599,19 @@ class FlagPersistence {
|
|
|
525
599
|
* {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
|
|
526
600
|
*/
|
|
527
601
|
async loadCached(context) {
|
|
528
|
-
const storageKey = await namespaceForContextData(this.
|
|
529
|
-
let flagsJson = await this.
|
|
602
|
+
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
603
|
+
let flagsJson = await this._platform.storage?.get(storageKey);
|
|
530
604
|
if (flagsJson === null || flagsJson === undefined) {
|
|
531
605
|
// Fallback: in version <10.3.1 flag data was stored under the canonical key, check
|
|
532
606
|
// to see if data is present and migrate the data if present.
|
|
533
|
-
flagsJson = await this.
|
|
607
|
+
flagsJson = await this._platform.storage?.get(context.canonicalKey);
|
|
534
608
|
if (flagsJson === null || flagsJson === undefined) {
|
|
535
609
|
// return false indicating cache did not load if flag json is still absent
|
|
536
610
|
return false;
|
|
537
611
|
}
|
|
538
612
|
// migrate data from version <10.3.1 and cleanup data that was under canonical key
|
|
539
|
-
await this.
|
|
540
|
-
await this.
|
|
613
|
+
await this._platform.storage?.set(storageKey, flagsJson);
|
|
614
|
+
await this._platform.storage?.clear(context.canonicalKey);
|
|
541
615
|
}
|
|
542
616
|
try {
|
|
543
617
|
const flags = JSON.parse(flagsJson);
|
|
@@ -546,43 +620,43 @@ class FlagPersistence {
|
|
|
546
620
|
acc[key] = { version: flag.version, flag };
|
|
547
621
|
return acc;
|
|
548
622
|
}, {});
|
|
549
|
-
this.
|
|
550
|
-
this.
|
|
623
|
+
this._flagUpdater.initCached(context, descriptors);
|
|
624
|
+
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
551
625
|
return true;
|
|
552
626
|
}
|
|
553
627
|
catch (e) {
|
|
554
|
-
this.
|
|
628
|
+
this._logger.warn(`Could not load cached flag evaluations from persistent storage: ${e.message}`);
|
|
555
629
|
return false;
|
|
556
630
|
}
|
|
557
631
|
}
|
|
558
|
-
async
|
|
559
|
-
if (this.
|
|
560
|
-
return this.
|
|
632
|
+
async _loadIndex() {
|
|
633
|
+
if (this._contextIndex !== undefined) {
|
|
634
|
+
return this._contextIndex;
|
|
561
635
|
}
|
|
562
|
-
const json = await this.
|
|
636
|
+
const json = await this._platform.storage?.get(await this._indexKeyPromise);
|
|
563
637
|
if (!json) {
|
|
564
|
-
this.
|
|
565
|
-
return this.
|
|
638
|
+
this._contextIndex = new ContextIndex();
|
|
639
|
+
return this._contextIndex;
|
|
566
640
|
}
|
|
567
641
|
try {
|
|
568
|
-
this.
|
|
569
|
-
this.
|
|
642
|
+
this._contextIndex = ContextIndex.fromJson(json);
|
|
643
|
+
this._logger.debug('Loaded context index from persistent storage');
|
|
570
644
|
}
|
|
571
645
|
catch (e) {
|
|
572
|
-
this.
|
|
573
|
-
this.
|
|
646
|
+
this._logger.warn(`Could not load index from persistent storage: ${e.message}`);
|
|
647
|
+
this._contextIndex = new ContextIndex();
|
|
574
648
|
}
|
|
575
|
-
return this.
|
|
576
|
-
}
|
|
577
|
-
async
|
|
578
|
-
const index = await this.
|
|
579
|
-
const storageKey = await namespaceForContextData(this.
|
|
580
|
-
index.notice(storageKey, this.
|
|
581
|
-
const pruned = index.prune(this.
|
|
582
|
-
await Promise.all(pruned.map(async (it) => this.
|
|
649
|
+
return this._contextIndex;
|
|
650
|
+
}
|
|
651
|
+
async _storeCache(context) {
|
|
652
|
+
const index = await this._loadIndex();
|
|
653
|
+
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
654
|
+
index.notice(storageKey, this._timeStamper());
|
|
655
|
+
const pruned = index.prune(this._maxCachedContexts);
|
|
656
|
+
await Promise.all(pruned.map(async (it) => this._platform.storage?.clear(it.id)));
|
|
583
657
|
// store index
|
|
584
|
-
await this.
|
|
585
|
-
const allFlags = this.
|
|
658
|
+
await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
|
|
659
|
+
const allFlags = this._flagStore.getAll();
|
|
586
660
|
// mapping item descriptors to flags
|
|
587
661
|
const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
|
|
588
662
|
if (descriptor.flag !== null && descriptor.flag !== undefined) {
|
|
@@ -592,7 +666,7 @@ class FlagPersistence {
|
|
|
592
666
|
}, {});
|
|
593
667
|
const jsonAll = JSON.stringify(flags);
|
|
594
668
|
// store flag data
|
|
595
|
-
await this.
|
|
669
|
+
await this._platform.storage?.set(storageKey, jsonAll);
|
|
596
670
|
}
|
|
597
671
|
}
|
|
598
672
|
|
|
@@ -601,25 +675,25 @@ class FlagPersistence {
|
|
|
601
675
|
*/
|
|
602
676
|
class DefaultFlagStore {
|
|
603
677
|
constructor() {
|
|
604
|
-
this.
|
|
678
|
+
this._flags = {};
|
|
605
679
|
}
|
|
606
680
|
init(newFlags) {
|
|
607
|
-
this.
|
|
681
|
+
this._flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
|
|
608
682
|
acc[key] = flag;
|
|
609
683
|
return acc;
|
|
610
684
|
}, {});
|
|
611
685
|
}
|
|
612
686
|
insertOrUpdate(key, update) {
|
|
613
|
-
this.
|
|
687
|
+
this._flags[key] = update;
|
|
614
688
|
}
|
|
615
689
|
get(key) {
|
|
616
|
-
if (Object.prototype.hasOwnProperty.call(this.
|
|
617
|
-
return this.
|
|
690
|
+
if (Object.prototype.hasOwnProperty.call(this._flags, key)) {
|
|
691
|
+
return this._flags[key];
|
|
618
692
|
}
|
|
619
693
|
return undefined;
|
|
620
694
|
}
|
|
621
695
|
getAll() {
|
|
622
|
-
return this.
|
|
696
|
+
return this._flags;
|
|
623
697
|
}
|
|
624
698
|
}
|
|
625
699
|
|
|
@@ -648,19 +722,19 @@ function calculateChangedKeys(existingObject, newObject) {
|
|
|
648
722
|
*/
|
|
649
723
|
class FlagUpdater {
|
|
650
724
|
constructor(flagStore, logger) {
|
|
651
|
-
this.
|
|
652
|
-
this.
|
|
653
|
-
this.
|
|
725
|
+
this._changeCallbacks = new Array();
|
|
726
|
+
this._flagStore = flagStore;
|
|
727
|
+
this._logger = logger;
|
|
654
728
|
}
|
|
655
729
|
init(context, newFlags) {
|
|
656
|
-
this.
|
|
657
|
-
const oldFlags = this.
|
|
658
|
-
this.
|
|
730
|
+
this._activeContextKey = context.canonicalKey;
|
|
731
|
+
const oldFlags = this._flagStore.getAll();
|
|
732
|
+
this._flagStore.init(newFlags);
|
|
659
733
|
const changed = calculateChangedKeys(oldFlags, newFlags);
|
|
660
734
|
if (changed.length > 0) {
|
|
661
|
-
this.
|
|
735
|
+
this._changeCallbacks.forEach((callback) => {
|
|
662
736
|
try {
|
|
663
|
-
callback(context, changed);
|
|
737
|
+
callback(context, changed, 'init');
|
|
664
738
|
}
|
|
665
739
|
catch (err) {
|
|
666
740
|
/* intentionally empty */
|
|
@@ -669,25 +743,25 @@ class FlagUpdater {
|
|
|
669
743
|
}
|
|
670
744
|
}
|
|
671
745
|
initCached(context, newFlags) {
|
|
672
|
-
if (this.
|
|
746
|
+
if (this._activeContextKey === context.canonicalKey) {
|
|
673
747
|
return;
|
|
674
748
|
}
|
|
675
749
|
this.init(context, newFlags);
|
|
676
750
|
}
|
|
677
751
|
upsert(context, key, item) {
|
|
678
|
-
if (this.
|
|
679
|
-
this.
|
|
752
|
+
if (this._activeContextKey !== context.canonicalKey) {
|
|
753
|
+
this._logger.warn('Received an update for an inactive context.');
|
|
680
754
|
return false;
|
|
681
755
|
}
|
|
682
|
-
const currentValue = this.
|
|
756
|
+
const currentValue = this._flagStore.get(key);
|
|
683
757
|
if (currentValue !== undefined && currentValue.version >= item.version) {
|
|
684
758
|
// this is an out of order update that can be ignored
|
|
685
759
|
return false;
|
|
686
760
|
}
|
|
687
|
-
this.
|
|
688
|
-
this.
|
|
761
|
+
this._flagStore.insertOrUpdate(key, item);
|
|
762
|
+
this._changeCallbacks.forEach((callback) => {
|
|
689
763
|
try {
|
|
690
|
-
callback(context, [key]);
|
|
764
|
+
callback(context, [key], 'patch');
|
|
691
765
|
}
|
|
692
766
|
catch (err) {
|
|
693
767
|
/* intentionally empty */
|
|
@@ -696,12 +770,12 @@ class FlagUpdater {
|
|
|
696
770
|
return true;
|
|
697
771
|
}
|
|
698
772
|
on(callback) {
|
|
699
|
-
this.
|
|
773
|
+
this._changeCallbacks.push(callback);
|
|
700
774
|
}
|
|
701
775
|
off(callback) {
|
|
702
|
-
const index = this.
|
|
776
|
+
const index = this._changeCallbacks.indexOf(callback);
|
|
703
777
|
if (index > -1) {
|
|
704
|
-
this.
|
|
778
|
+
this._changeCallbacks.splice(index, 1);
|
|
705
779
|
}
|
|
706
780
|
}
|
|
707
781
|
}
|
|
@@ -715,40 +789,39 @@ class DefaultFlagManager {
|
|
|
715
789
|
* @param timeStamper exists for testing purposes
|
|
716
790
|
*/
|
|
717
791
|
constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
718
|
-
this.
|
|
719
|
-
this.
|
|
720
|
-
this.
|
|
721
|
-
this.flagPersistencePromise = this.initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
792
|
+
this._flagStore = new DefaultFlagStore();
|
|
793
|
+
this._flagUpdater = new FlagUpdater(this._flagStore, logger);
|
|
794
|
+
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
722
795
|
}
|
|
723
|
-
async
|
|
796
|
+
async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
724
797
|
const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
|
|
725
|
-
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this.
|
|
798
|
+
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
726
799
|
}
|
|
727
800
|
get(key) {
|
|
728
|
-
return this.
|
|
801
|
+
return this._flagStore.get(key);
|
|
729
802
|
}
|
|
730
803
|
getAll() {
|
|
731
|
-
return this.
|
|
804
|
+
return this._flagStore.getAll();
|
|
732
805
|
}
|
|
733
806
|
setBootstrap(context, newFlags) {
|
|
734
807
|
// Bypasses the persistence as we do not want to put these flags into any cache.
|
|
735
808
|
// Generally speaking persistence likely *SHOULD* be disabled when using bootstrap.
|
|
736
|
-
this.
|
|
809
|
+
this._flagUpdater.init(context, newFlags);
|
|
737
810
|
}
|
|
738
811
|
async init(context, newFlags) {
|
|
739
|
-
return (await this.
|
|
812
|
+
return (await this._flagPersistencePromise).init(context, newFlags);
|
|
740
813
|
}
|
|
741
814
|
async upsert(context, key, item) {
|
|
742
|
-
return (await this.
|
|
815
|
+
return (await this._flagPersistencePromise).upsert(context, key, item);
|
|
743
816
|
}
|
|
744
817
|
async loadCached(context) {
|
|
745
|
-
return (await this.
|
|
818
|
+
return (await this._flagPersistencePromise).loadCached(context);
|
|
746
819
|
}
|
|
747
820
|
on(callback) {
|
|
748
|
-
this.
|
|
821
|
+
this._flagUpdater.on(callback);
|
|
749
822
|
}
|
|
750
823
|
off(callback) {
|
|
751
|
-
this.
|
|
824
|
+
this._flagUpdater.off(callback);
|
|
752
825
|
}
|
|
753
826
|
}
|
|
754
827
|
|
|
@@ -798,39 +871,186 @@ function executeAfterIdentify(logger, hooks, hookContext, updatedData, result) {
|
|
|
798
871
|
}
|
|
799
872
|
}
|
|
800
873
|
class HookRunner {
|
|
801
|
-
constructor(
|
|
802
|
-
this.
|
|
803
|
-
this.
|
|
804
|
-
this.
|
|
874
|
+
constructor(_logger, initialHooks) {
|
|
875
|
+
this._logger = _logger;
|
|
876
|
+
this._hooks = [];
|
|
877
|
+
this._hooks.push(...initialHooks);
|
|
805
878
|
}
|
|
806
879
|
withEvaluation(key, context, defaultValue, method) {
|
|
807
|
-
if (this.
|
|
880
|
+
if (this._hooks.length === 0) {
|
|
808
881
|
return method();
|
|
809
882
|
}
|
|
810
|
-
const hooks = [...this.
|
|
883
|
+
const hooks = [...this._hooks];
|
|
811
884
|
const hookContext = {
|
|
812
885
|
flagKey: key,
|
|
813
886
|
context,
|
|
814
887
|
defaultValue,
|
|
815
888
|
};
|
|
816
|
-
const hookData = executeBeforeEvaluation(this.
|
|
889
|
+
const hookData = executeBeforeEvaluation(this._logger, hooks, hookContext);
|
|
817
890
|
const result = method();
|
|
818
|
-
executeAfterEvaluation(this.
|
|
891
|
+
executeAfterEvaluation(this._logger, hooks, hookContext, hookData, result);
|
|
819
892
|
return result;
|
|
820
893
|
}
|
|
821
894
|
identify(context, timeout) {
|
|
822
|
-
const hooks = [...this.
|
|
895
|
+
const hooks = [...this._hooks];
|
|
823
896
|
const hookContext = {
|
|
824
897
|
context,
|
|
825
898
|
timeout,
|
|
826
899
|
};
|
|
827
|
-
const hookData = executeBeforeIdentify(this.
|
|
900
|
+
const hookData = executeBeforeIdentify(this._logger, hooks, hookContext);
|
|
828
901
|
return (result) => {
|
|
829
|
-
executeAfterIdentify(this.
|
|
902
|
+
executeAfterIdentify(this._logger, hooks, hookContext, hookData, result);
|
|
830
903
|
};
|
|
831
904
|
}
|
|
832
905
|
addHook(hook) {
|
|
833
|
-
this.
|
|
906
|
+
this._hooks.push(hook);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function getInspectorHook(inspectorManager) {
|
|
911
|
+
return {
|
|
912
|
+
getMetadata() {
|
|
913
|
+
return {
|
|
914
|
+
name: 'LaunchDarkly-Inspector-Adapter',
|
|
915
|
+
};
|
|
916
|
+
},
|
|
917
|
+
afterEvaluation: (hookContext, data, detail) => {
|
|
918
|
+
inspectorManager.onFlagUsed(hookContext.flagKey, detail, hookContext.context);
|
|
919
|
+
return data;
|
|
920
|
+
},
|
|
921
|
+
afterIdentify(hookContext, data, _result) {
|
|
922
|
+
inspectorManager.onIdentityChanged(hookContext.context);
|
|
923
|
+
return data;
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function invalidInspector(type, name) {
|
|
929
|
+
return `an inspector: "${name}" of an invalid type (${type}) was configured`;
|
|
930
|
+
}
|
|
931
|
+
function inspectorMethodError(type, name) {
|
|
932
|
+
return `an inspector: "${name}" of type: "${type}" generated an exception`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Wrap an inspector ensuring that calling its methods are safe.
|
|
937
|
+
* @param inspector Inspector to wrap.
|
|
938
|
+
*/
|
|
939
|
+
function createSafeInspector(inspector, logger) {
|
|
940
|
+
let errorLogged = false;
|
|
941
|
+
const wrapper = {
|
|
942
|
+
method: (...args) => {
|
|
943
|
+
try {
|
|
944
|
+
// We are proxying arguments here to the underlying method. Typescript doesn't care
|
|
945
|
+
// for this as it cannot validate the parameters are correct, but we are also the caller
|
|
946
|
+
// in this case and will dispatch things with the correct arguments. The dispatch to this
|
|
947
|
+
// will itself happen with a type guard.
|
|
948
|
+
// @ts-ignore
|
|
949
|
+
inspector.method(...args);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// If something goes wrong in an inspector we want to log that something
|
|
953
|
+
// went wrong. We don't want to flood the logs, so we only log something
|
|
954
|
+
// the first time that something goes wrong.
|
|
955
|
+
// We do not include the exception in the log, because we do not know what
|
|
956
|
+
// kind of data it may contain.
|
|
957
|
+
if (!errorLogged) {
|
|
958
|
+
errorLogged = true;
|
|
959
|
+
logger.warn(inspectorMethodError(wrapper.type, wrapper.name));
|
|
960
|
+
}
|
|
961
|
+
// Prevent errors.
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
type: inspector.type,
|
|
965
|
+
name: inspector.name,
|
|
966
|
+
synchronous: inspector.synchronous,
|
|
967
|
+
};
|
|
968
|
+
return wrapper;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const FLAG_USED_TYPE = 'flag-used';
|
|
972
|
+
const FLAG_DETAILS_CHANGED_TYPE = 'flag-details-changed';
|
|
973
|
+
const FLAG_DETAIL_CHANGED_TYPE = 'flag-detail-changed';
|
|
974
|
+
const IDENTITY_CHANGED_TYPE = 'client-identity-changed';
|
|
975
|
+
const VALID__TYPES = [
|
|
976
|
+
FLAG_USED_TYPE,
|
|
977
|
+
FLAG_DETAILS_CHANGED_TYPE,
|
|
978
|
+
FLAG_DETAIL_CHANGED_TYPE,
|
|
979
|
+
IDENTITY_CHANGED_TYPE,
|
|
980
|
+
];
|
|
981
|
+
function validateInspector(inspector, logger) {
|
|
982
|
+
const valid = VALID__TYPES.includes(inspector.type) &&
|
|
983
|
+
inspector.method &&
|
|
984
|
+
typeof inspector.method === 'function';
|
|
985
|
+
if (!valid) {
|
|
986
|
+
logger.warn(invalidInspector(inspector.type, inspector.name));
|
|
987
|
+
}
|
|
988
|
+
return valid;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Manages dispatching of inspection data to registered inspectors.
|
|
992
|
+
*/
|
|
993
|
+
class InspectorManager {
|
|
994
|
+
constructor(inspectors, logger) {
|
|
995
|
+
this._safeInspectors = [];
|
|
996
|
+
const validInspectors = inspectors.filter((inspector) => validateInspector(inspector, logger));
|
|
997
|
+
this._safeInspectors = validInspectors.map((inspector) => createSafeInspector(inspector, logger));
|
|
998
|
+
}
|
|
999
|
+
hasInspectors() {
|
|
1000
|
+
return this._safeInspectors.length !== 0;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Notify registered inspectors of a flag being used.
|
|
1004
|
+
*
|
|
1005
|
+
* @param flagKey The key for the flag.
|
|
1006
|
+
* @param detail The LDEvaluationDetail for the flag.
|
|
1007
|
+
* @param context The LDContext for the flag.
|
|
1008
|
+
*/
|
|
1009
|
+
onFlagUsed(flagKey, detail, context) {
|
|
1010
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1011
|
+
if (inspector.type === FLAG_USED_TYPE) {
|
|
1012
|
+
inspector.method(flagKey, detail, context);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Notify registered inspectors that the flags have been replaced.
|
|
1018
|
+
*
|
|
1019
|
+
* @param flags The current flags as a Record<string, LDEvaluationDetail>.
|
|
1020
|
+
*/
|
|
1021
|
+
onFlagsChanged(flags) {
|
|
1022
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1023
|
+
if (inspector.type === FLAG_DETAILS_CHANGED_TYPE) {
|
|
1024
|
+
inspector.method(flags);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Notify registered inspectors that a flag value has changed.
|
|
1030
|
+
*
|
|
1031
|
+
* @param flagKey The key for the flag that changed.
|
|
1032
|
+
* @param flag An `LDEvaluationDetail` for the flag.
|
|
1033
|
+
*/
|
|
1034
|
+
onFlagChanged(flagKey, flag) {
|
|
1035
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1036
|
+
if (inspector.type === FLAG_DETAIL_CHANGED_TYPE) {
|
|
1037
|
+
inspector.method(flagKey, flag);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Notify the registered inspectors that the context identity has changed.
|
|
1043
|
+
*
|
|
1044
|
+
* The notification itself will be dispatched asynchronously.
|
|
1045
|
+
*
|
|
1046
|
+
* @param context The `LDContext` which is now identified.
|
|
1047
|
+
*/
|
|
1048
|
+
onIdentityChanged(context) {
|
|
1049
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1050
|
+
if (inspector.type === IDENTITY_CHANGED_TYPE) {
|
|
1051
|
+
inspector.method(context);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
834
1054
|
}
|
|
835
1055
|
}
|
|
836
1056
|
|
|
@@ -841,16 +1061,16 @@ class HookRunner {
|
|
|
841
1061
|
* a system to allow listeners which have counts independent of the primary listener counts.
|
|
842
1062
|
*/
|
|
843
1063
|
class LDEmitter {
|
|
844
|
-
constructor(
|
|
845
|
-
this.
|
|
846
|
-
this.
|
|
1064
|
+
constructor(_logger) {
|
|
1065
|
+
this._logger = _logger;
|
|
1066
|
+
this._listeners = new Map();
|
|
847
1067
|
}
|
|
848
1068
|
on(name, listener) {
|
|
849
|
-
if (!this.
|
|
850
|
-
this.
|
|
1069
|
+
if (!this._listeners.has(name)) {
|
|
1070
|
+
this._listeners.set(name, [listener]);
|
|
851
1071
|
}
|
|
852
1072
|
else {
|
|
853
|
-
this.
|
|
1073
|
+
this._listeners.get(name)?.push(listener);
|
|
854
1074
|
}
|
|
855
1075
|
}
|
|
856
1076
|
/**
|
|
@@ -860,7 +1080,7 @@ class LDEmitter {
|
|
|
860
1080
|
* @param listener Optional. If unspecified, all listeners for the event will be removed.
|
|
861
1081
|
*/
|
|
862
1082
|
off(name, listener) {
|
|
863
|
-
const existingListeners = this.
|
|
1083
|
+
const existingListeners = this._listeners.get(name);
|
|
864
1084
|
if (!existingListeners) {
|
|
865
1085
|
return;
|
|
866
1086
|
}
|
|
@@ -868,33 +1088,33 @@ class LDEmitter {
|
|
|
868
1088
|
// remove from internal cache
|
|
869
1089
|
const updated = existingListeners.filter((fn) => fn !== listener);
|
|
870
1090
|
if (updated.length === 0) {
|
|
871
|
-
this.
|
|
1091
|
+
this._listeners.delete(name);
|
|
872
1092
|
}
|
|
873
1093
|
else {
|
|
874
|
-
this.
|
|
1094
|
+
this._listeners.set(name, updated);
|
|
875
1095
|
}
|
|
876
1096
|
return;
|
|
877
1097
|
}
|
|
878
1098
|
// listener was not specified, so remove them all for that event
|
|
879
|
-
this.
|
|
1099
|
+
this._listeners.delete(name);
|
|
880
1100
|
}
|
|
881
|
-
|
|
1101
|
+
_invokeListener(listener, name, ...detail) {
|
|
882
1102
|
try {
|
|
883
1103
|
listener(...detail);
|
|
884
1104
|
}
|
|
885
1105
|
catch (err) {
|
|
886
|
-
this.
|
|
1106
|
+
this._logger?.error(`Encountered error invoking handler for "${name}", detail: "${err}"`);
|
|
887
1107
|
}
|
|
888
1108
|
}
|
|
889
1109
|
emit(name, ...detail) {
|
|
890
|
-
const listeners = this.
|
|
891
|
-
listeners?.forEach((listener) => this.
|
|
1110
|
+
const listeners = this._listeners.get(name);
|
|
1111
|
+
listeners?.forEach((listener) => this._invokeListener(listener, name, ...detail));
|
|
892
1112
|
}
|
|
893
1113
|
eventNames() {
|
|
894
|
-
return [...this.
|
|
1114
|
+
return [...this._listeners.keys()];
|
|
895
1115
|
}
|
|
896
1116
|
listenerCount(name) {
|
|
897
|
-
return this.
|
|
1117
|
+
return this._listeners.get(name)?.length ?? 0;
|
|
898
1118
|
}
|
|
899
1119
|
}
|
|
900
1120
|
|
|
@@ -907,40 +1127,45 @@ class LDClientImpl {
|
|
|
907
1127
|
this.sdkKey = sdkKey;
|
|
908
1128
|
this.autoEnvAttributes = autoEnvAttributes;
|
|
909
1129
|
this.platform = platform;
|
|
910
|
-
this.
|
|
911
|
-
this.
|
|
912
|
-
this.
|
|
913
|
-
this.
|
|
914
|
-
this.
|
|
1130
|
+
this._identifyTimeout = 5;
|
|
1131
|
+
this._highTimeoutThreshold = 15;
|
|
1132
|
+
this._eventFactoryDefault = new EventFactory(false);
|
|
1133
|
+
this._eventFactoryWithReasons = new EventFactory(true);
|
|
1134
|
+
this._eventSendingEnabled = false;
|
|
915
1135
|
if (!sdkKey) {
|
|
916
1136
|
throw new Error('You must configure the client with a client-side SDK key');
|
|
917
1137
|
}
|
|
918
1138
|
if (!platform.encoding) {
|
|
919
1139
|
throw new Error('Platform must implement Encoding because btoa is required.');
|
|
920
1140
|
}
|
|
921
|
-
this.
|
|
922
|
-
this.logger = this.
|
|
923
|
-
this.
|
|
924
|
-
this.
|
|
925
|
-
this.
|
|
926
|
-
this.
|
|
1141
|
+
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1142
|
+
this.logger = this._config.logger;
|
|
1143
|
+
this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1144
|
+
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.logger);
|
|
1145
|
+
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
1146
|
+
this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
|
|
927
1147
|
this.emitter = new LDEmitter();
|
|
928
1148
|
this.emitter.on('error', (c, err) => {
|
|
929
1149
|
this.logger.error(`error: ${err}, context: ${JSON.stringify(c)}`);
|
|
930
1150
|
});
|
|
931
|
-
this.
|
|
1151
|
+
this._flagManager.on((context, flagKeys, type) => {
|
|
1152
|
+
this._handleInspectionChanged(flagKeys, type);
|
|
932
1153
|
const ldContext = Context.toLDContext(context);
|
|
933
1154
|
this.emitter.emit('change', ldContext, flagKeys);
|
|
934
1155
|
flagKeys.forEach((it) => {
|
|
935
1156
|
this.emitter.emit(`change:${it}`, ldContext);
|
|
936
1157
|
});
|
|
937
1158
|
});
|
|
938
|
-
this.dataManager = dataManagerFactory(this.
|
|
939
|
-
this.
|
|
1159
|
+
this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
|
|
1160
|
+
this._hookRunner = new HookRunner(this.logger, this._config.hooks);
|
|
1161
|
+
this._inspectorManager = new InspectorManager(this._config.inspectors, this.logger);
|
|
1162
|
+
if (this._inspectorManager.hasInspectors()) {
|
|
1163
|
+
this._hookRunner.addHook(getInspectorHook(this._inspectorManager));
|
|
1164
|
+
}
|
|
940
1165
|
}
|
|
941
1166
|
allFlags() {
|
|
942
1167
|
// extracting all flag values
|
|
943
|
-
const result = Object.entries(this.
|
|
1168
|
+
const result = Object.entries(this._flagManager.getAll()).reduce((acc, [key, descriptor]) => {
|
|
944
1169
|
if (descriptor.flag !== null && descriptor.flag !== undefined && !descriptor.flag.deleted) {
|
|
945
1170
|
acc[key] = descriptor.flag.value;
|
|
946
1171
|
}
|
|
@@ -950,13 +1175,13 @@ class LDClientImpl {
|
|
|
950
1175
|
}
|
|
951
1176
|
async close() {
|
|
952
1177
|
await this.flush();
|
|
953
|
-
this.
|
|
954
|
-
this.
|
|
1178
|
+
this._eventProcessor?.close();
|
|
1179
|
+
this._updateProcessor?.close();
|
|
955
1180
|
this.logger.debug('Closed event processor and data source.');
|
|
956
1181
|
}
|
|
957
1182
|
async flush() {
|
|
958
1183
|
try {
|
|
959
|
-
await this.
|
|
1184
|
+
await this._eventProcessor?.flush();
|
|
960
1185
|
this.logger.debug('Successfully flushed event processor.');
|
|
961
1186
|
}
|
|
962
1187
|
catch (e) {
|
|
@@ -971,12 +1196,12 @@ class LDClientImpl {
|
|
|
971
1196
|
// code. We are returned the unchecked context so that if a consumer identifies with an invalid context
|
|
972
1197
|
// and then calls getContext, they get back the same context they provided, without any assertion about
|
|
973
1198
|
// validity.
|
|
974
|
-
return this.
|
|
1199
|
+
return this._uncheckedContext ? clone(this._uncheckedContext) : undefined;
|
|
975
1200
|
}
|
|
976
1201
|
getInternalContext() {
|
|
977
|
-
return this.
|
|
1202
|
+
return this._checkedContext;
|
|
978
1203
|
}
|
|
979
|
-
|
|
1204
|
+
_createIdentifyPromise(timeout) {
|
|
980
1205
|
let res;
|
|
981
1206
|
let rej;
|
|
982
1207
|
const slow = new Promise((resolve, reject) => {
|
|
@@ -1009,16 +1234,16 @@ class LDClientImpl {
|
|
|
1009
1234
|
*/
|
|
1010
1235
|
async identify(pristineContext, identifyOptions) {
|
|
1011
1236
|
if (identifyOptions?.timeout) {
|
|
1012
|
-
this.
|
|
1237
|
+
this._identifyTimeout = identifyOptions.timeout;
|
|
1013
1238
|
}
|
|
1014
|
-
if (this.
|
|
1239
|
+
if (this._identifyTimeout > this._highTimeoutThreshold) {
|
|
1015
1240
|
this.logger.warn('The identify function was called with a timeout greater than ' +
|
|
1016
|
-
`${this.
|
|
1017
|
-
`${this.
|
|
1241
|
+
`${this._highTimeoutThreshold} seconds. We recommend a timeout of less than ` +
|
|
1242
|
+
`${this._highTimeoutThreshold} seconds.`);
|
|
1018
1243
|
}
|
|
1019
1244
|
let context = await ensureKey(pristineContext, this.platform);
|
|
1020
1245
|
if (this.autoEnvAttributes === AutoEnvAttributes.Enabled) {
|
|
1021
|
-
context = await addAutoEnv(context, this.platform, this.
|
|
1246
|
+
context = await addAutoEnv(context, this.platform, this._config);
|
|
1022
1247
|
}
|
|
1023
1248
|
const checkedContext = Context.fromLDContext(context);
|
|
1024
1249
|
if (!checkedContext.valid) {
|
|
@@ -1026,12 +1251,12 @@ class LDClientImpl {
|
|
|
1026
1251
|
this.emitter.emit('error', context, error);
|
|
1027
1252
|
return Promise.reject(error);
|
|
1028
1253
|
}
|
|
1029
|
-
this.
|
|
1030
|
-
this.
|
|
1031
|
-
this.
|
|
1032
|
-
const { identifyPromise, identifyResolve, identifyReject } = this.
|
|
1033
|
-
this.logger.debug(`Identifying ${JSON.stringify(this.
|
|
1034
|
-
const afterIdentify = this.
|
|
1254
|
+
this._uncheckedContext = context;
|
|
1255
|
+
this._checkedContext = checkedContext;
|
|
1256
|
+
this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(this._checkedContext));
|
|
1257
|
+
const { identifyPromise, identifyResolve, identifyReject } = this._createIdentifyPromise(this._identifyTimeout);
|
|
1258
|
+
this.logger.debug(`Identifying ${JSON.stringify(this._checkedContext)}`);
|
|
1259
|
+
const afterIdentify = this._hookRunner.identify(context, identifyOptions?.timeout);
|
|
1035
1260
|
await this.dataManager.identify(identifyResolve, identifyReject, checkedContext, identifyOptions);
|
|
1036
1261
|
return identifyPromise.then((res) => {
|
|
1037
1262
|
afterIdentify({ status: 'completed' });
|
|
@@ -1048,38 +1273,38 @@ class LDClientImpl {
|
|
|
1048
1273
|
this.emitter.off(eventName, listener);
|
|
1049
1274
|
}
|
|
1050
1275
|
track(key, data, metricValue) {
|
|
1051
|
-
if (!this.
|
|
1052
|
-
this.logger.warn(ClientMessages.
|
|
1276
|
+
if (!this._checkedContext || !this._checkedContext.valid) {
|
|
1277
|
+
this.logger.warn(ClientMessages.MissingContextKeyNoEvent);
|
|
1053
1278
|
return;
|
|
1054
1279
|
}
|
|
1055
1280
|
// 0 is valid, so do not truthy check the metric value
|
|
1056
1281
|
if (metricValue !== undefined && !TypeValidators.Number.is(metricValue)) {
|
|
1057
1282
|
this.logger?.warn(ClientMessages.invalidMetricValue(typeof metricValue));
|
|
1058
1283
|
}
|
|
1059
|
-
this.
|
|
1284
|
+
this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._checkedContext, data, metricValue)));
|
|
1060
1285
|
}
|
|
1061
|
-
|
|
1062
|
-
if (!this.
|
|
1063
|
-
this.logger.debug(ClientMessages.
|
|
1286
|
+
_variationInternal(flagKey, defaultValue, eventFactory, typeChecker) {
|
|
1287
|
+
if (!this._uncheckedContext) {
|
|
1288
|
+
this.logger.debug(ClientMessages.MissingContextKeyNoEvent);
|
|
1064
1289
|
return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue);
|
|
1065
1290
|
}
|
|
1066
|
-
const evalContext = Context.fromLDContext(this.
|
|
1067
|
-
const foundItem = this.
|
|
1291
|
+
const evalContext = Context.fromLDContext(this._uncheckedContext);
|
|
1292
|
+
const foundItem = this._flagManager.get(flagKey);
|
|
1068
1293
|
if (foundItem === undefined || foundItem.flag.deleted) {
|
|
1069
1294
|
const defVal = defaultValue ?? null;
|
|
1070
1295
|
const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
|
|
1071
|
-
this.emitter.emit('error', this.
|
|
1072
|
-
this.
|
|
1296
|
+
this.emitter.emit('error', this._uncheckedContext, error);
|
|
1297
|
+
this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
|
|
1073
1298
|
return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue);
|
|
1074
1299
|
}
|
|
1075
|
-
const { reason, value, variation } = foundItem.flag;
|
|
1300
|
+
const { reason, value, variation, prerequisites } = foundItem.flag;
|
|
1076
1301
|
if (typeChecker) {
|
|
1077
1302
|
const [matched, type] = typeChecker(value);
|
|
1078
1303
|
if (!matched) {
|
|
1079
|
-
this.
|
|
1304
|
+
this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
|
|
1080
1305
|
defaultValue, foundItem.flag, evalContext, reason));
|
|
1081
1306
|
const error = new LDClientError(`Wrong type "${type}" for feature flag "${flagKey}"; returning default value`);
|
|
1082
|
-
this.emitter.emit('error', this.
|
|
1307
|
+
this.emitter.emit('error', this._uncheckedContext, error);
|
|
1083
1308
|
return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue);
|
|
1084
1309
|
}
|
|
1085
1310
|
}
|
|
@@ -1088,21 +1313,24 @@ class LDClientImpl {
|
|
|
1088
1313
|
this.logger.debug('Result value is null. Providing default value.');
|
|
1089
1314
|
successDetail.value = defaultValue;
|
|
1090
1315
|
}
|
|
1091
|
-
|
|
1316
|
+
prerequisites?.forEach((prereqKey) => {
|
|
1317
|
+
this.variation(prereqKey, undefined);
|
|
1318
|
+
});
|
|
1319
|
+
this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
|
|
1092
1320
|
return successDetail;
|
|
1093
1321
|
}
|
|
1094
1322
|
variation(flagKey, defaultValue) {
|
|
1095
|
-
const { value } = this.
|
|
1323
|
+
const { value } = this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
|
|
1096
1324
|
return value;
|
|
1097
1325
|
}
|
|
1098
1326
|
variationDetail(flagKey, defaultValue) {
|
|
1099
|
-
return this.
|
|
1327
|
+
return this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
|
|
1100
1328
|
}
|
|
1101
|
-
|
|
1102
|
-
return this.
|
|
1329
|
+
_typedEval(key, defaultValue, eventFactory, typeChecker) {
|
|
1330
|
+
return this._hookRunner.withEvaluation(key, this._uncheckedContext, defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
|
|
1103
1331
|
}
|
|
1104
1332
|
boolVariation(key, defaultValue) {
|
|
1105
|
-
return this.
|
|
1333
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1106
1334
|
TypeValidators.Boolean.is(value),
|
|
1107
1335
|
TypeValidators.Boolean.getType(),
|
|
1108
1336
|
]).value;
|
|
@@ -1111,31 +1339,31 @@ class LDClientImpl {
|
|
|
1111
1339
|
return this.variation(key, defaultValue);
|
|
1112
1340
|
}
|
|
1113
1341
|
numberVariation(key, defaultValue) {
|
|
1114
|
-
return this.
|
|
1342
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1115
1343
|
TypeValidators.Number.is(value),
|
|
1116
1344
|
TypeValidators.Number.getType(),
|
|
1117
1345
|
]).value;
|
|
1118
1346
|
}
|
|
1119
1347
|
stringVariation(key, defaultValue) {
|
|
1120
|
-
return this.
|
|
1348
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1121
1349
|
TypeValidators.String.is(value),
|
|
1122
1350
|
TypeValidators.String.getType(),
|
|
1123
1351
|
]).value;
|
|
1124
1352
|
}
|
|
1125
1353
|
boolVariationDetail(key, defaultValue) {
|
|
1126
|
-
return this.
|
|
1354
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1127
1355
|
TypeValidators.Boolean.is(value),
|
|
1128
1356
|
TypeValidators.Boolean.getType(),
|
|
1129
1357
|
]);
|
|
1130
1358
|
}
|
|
1131
1359
|
numberVariationDetail(key, defaultValue) {
|
|
1132
|
-
return this.
|
|
1360
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1133
1361
|
TypeValidators.Number.is(value),
|
|
1134
1362
|
TypeValidators.Number.getType(),
|
|
1135
1363
|
]);
|
|
1136
1364
|
}
|
|
1137
1365
|
stringVariationDetail(key, defaultValue) {
|
|
1138
|
-
return this.
|
|
1366
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1139
1367
|
TypeValidators.String.is(value),
|
|
1140
1368
|
TypeValidators.String.getType(),
|
|
1141
1369
|
]);
|
|
@@ -1144,7 +1372,7 @@ class LDClientImpl {
|
|
|
1144
1372
|
return this.variationDetail(key, defaultValue);
|
|
1145
1373
|
}
|
|
1146
1374
|
addHook(hook) {
|
|
1147
|
-
this.
|
|
1375
|
+
this._hookRunner.addHook(hook);
|
|
1148
1376
|
}
|
|
1149
1377
|
/**
|
|
1150
1378
|
* Enable/Disable event sending.
|
|
@@ -1152,13 +1380,13 @@ class LDClientImpl {
|
|
|
1152
1380
|
* @param flush True to flush while disabling. Useful to flush on certain state transitions.
|
|
1153
1381
|
*/
|
|
1154
1382
|
setEventSendingEnabled(enabled, flush) {
|
|
1155
|
-
if (this.
|
|
1383
|
+
if (this._eventSendingEnabled === enabled) {
|
|
1156
1384
|
return;
|
|
1157
1385
|
}
|
|
1158
|
-
this.
|
|
1386
|
+
this._eventSendingEnabled = enabled;
|
|
1159
1387
|
if (enabled) {
|
|
1160
1388
|
this.logger.debug('Starting event processor');
|
|
1161
|
-
this.
|
|
1389
|
+
this._eventProcessor?.start();
|
|
1162
1390
|
}
|
|
1163
1391
|
else if (flush) {
|
|
1164
1392
|
this.logger?.debug('Flushing event processor before disabling.');
|
|
@@ -1166,92 +1394,70 @@ class LDClientImpl {
|
|
|
1166
1394
|
this.flush().then(() => {
|
|
1167
1395
|
// While waiting for the flush event sending could be re-enabled, in which case
|
|
1168
1396
|
// we do not want to close the event processor.
|
|
1169
|
-
if (!this.
|
|
1397
|
+
if (!this._eventSendingEnabled) {
|
|
1170
1398
|
this.logger?.debug('Stopping event processor.');
|
|
1171
|
-
this.
|
|
1399
|
+
this._eventProcessor?.close();
|
|
1172
1400
|
}
|
|
1173
1401
|
});
|
|
1174
1402
|
}
|
|
1175
1403
|
else {
|
|
1176
1404
|
// Just disabled.
|
|
1177
1405
|
this.logger?.debug('Stopping event processor.');
|
|
1178
|
-
this.
|
|
1406
|
+
this._eventProcessor?.close();
|
|
1179
1407
|
}
|
|
1180
1408
|
}
|
|
1181
1409
|
sendEvent(event) {
|
|
1182
|
-
this.
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function isOk(status) {
|
|
1187
|
-
return status >= 200 && status <= 299;
|
|
1188
|
-
}
|
|
1189
|
-
class LDRequestError extends Error {
|
|
1190
|
-
constructor(message, status) {
|
|
1191
|
-
super(message);
|
|
1192
|
-
this.status = status;
|
|
1193
|
-
this.name = 'LaunchDarklyRequestError';
|
|
1410
|
+
this._eventProcessor?.sendEvent(event);
|
|
1194
1411
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
this.method = method;
|
|
1206
|
-
this.body = body;
|
|
1207
|
-
}
|
|
1208
|
-
async requestPayload() {
|
|
1209
|
-
let status;
|
|
1210
|
-
try {
|
|
1211
|
-
const res = await this.requests.fetch(this.uri, {
|
|
1212
|
-
method: this.method,
|
|
1213
|
-
headers: this.headers,
|
|
1214
|
-
body: this.body,
|
|
1215
|
-
});
|
|
1216
|
-
if (isOk(res.status)) {
|
|
1217
|
-
return await res.text();
|
|
1412
|
+
_handleInspectionChanged(flagKeys, type) {
|
|
1413
|
+
if (!this._inspectorManager.hasInspectors()) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const details = {};
|
|
1417
|
+
flagKeys.forEach((flagKey) => {
|
|
1418
|
+
const item = this._flagManager.get(flagKey);
|
|
1419
|
+
if (item?.flag && !item.flag.deleted) {
|
|
1420
|
+
const { reason, value, variation } = item.flag;
|
|
1421
|
+
details[flagKey] = createSuccessEvaluationDetail(value, variation, reason);
|
|
1218
1422
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1423
|
+
});
|
|
1424
|
+
if (type === 'init') {
|
|
1425
|
+
this._inspectorManager.onFlagsChanged(details);
|
|
1221
1426
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1427
|
+
else if (type === 'patch') {
|
|
1428
|
+
Object.entries(details).forEach(([flagKey, detail]) => {
|
|
1429
|
+
this._inspectorManager.onFlagChanged(flagKey, detail);
|
|
1430
|
+
});
|
|
1224
1431
|
}
|
|
1225
|
-
throw new LDRequestError(`Unexpected status code: ${status}`, status);
|
|
1226
1432
|
}
|
|
1227
1433
|
}
|
|
1228
1434
|
|
|
1229
1435
|
class DataSourceEventHandler {
|
|
1230
|
-
constructor(
|
|
1231
|
-
this.
|
|
1232
|
-
this.
|
|
1233
|
-
this.
|
|
1436
|
+
constructor(_flagManager, _statusManager, _logger) {
|
|
1437
|
+
this._flagManager = _flagManager;
|
|
1438
|
+
this._statusManager = _statusManager;
|
|
1439
|
+
this._logger = _logger;
|
|
1234
1440
|
}
|
|
1235
1441
|
async handlePut(context, flags) {
|
|
1236
|
-
this.
|
|
1442
|
+
this._logger.debug(`Got PUT: ${Object.keys(flags)}`);
|
|
1237
1443
|
// mapping flags to item descriptors
|
|
1238
1444
|
const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
|
|
1239
1445
|
acc[key] = { version: flag.version, flag };
|
|
1240
1446
|
return acc;
|
|
1241
1447
|
}, {});
|
|
1242
|
-
await this.
|
|
1243
|
-
this.
|
|
1448
|
+
await this._flagManager.init(context, descriptors);
|
|
1449
|
+
this._statusManager.requestStateUpdate(DataSourceState.Valid);
|
|
1244
1450
|
}
|
|
1245
1451
|
async handlePatch(context, patchFlag) {
|
|
1246
|
-
this.
|
|
1247
|
-
this.
|
|
1452
|
+
this._logger.debug(`Got PATCH ${JSON.stringify(patchFlag, null, 2)}`);
|
|
1453
|
+
this._flagManager.upsert(context, patchFlag.key, {
|
|
1248
1454
|
version: patchFlag.version,
|
|
1249
1455
|
flag: patchFlag,
|
|
1250
1456
|
});
|
|
1251
1457
|
}
|
|
1252
1458
|
async handleDelete(context, deleteFlag) {
|
|
1253
|
-
this.
|
|
1254
|
-
this.
|
|
1459
|
+
this._logger.debug(`Got DELETE ${JSON.stringify(deleteFlag, null, 2)}`);
|
|
1460
|
+
this._flagManager.upsert(context, deleteFlag.key, {
|
|
1255
1461
|
version: deleteFlag.version,
|
|
1256
1462
|
flag: {
|
|
1257
1463
|
...deleteFlag,
|
|
@@ -1266,10 +1472,10 @@ class DataSourceEventHandler {
|
|
|
1266
1472
|
});
|
|
1267
1473
|
}
|
|
1268
1474
|
handleStreamingError(error) {
|
|
1269
|
-
this.
|
|
1475
|
+
this._statusManager.reportError(error.kind, error.message, error.code, error.recoverable);
|
|
1270
1476
|
}
|
|
1271
1477
|
handlePollingError(error) {
|
|
1272
|
-
this.
|
|
1478
|
+
this._statusManager.reportError(error.kind, error.message, error.status, error.recoverable);
|
|
1273
1479
|
}
|
|
1274
1480
|
}
|
|
1275
1481
|
|
|
@@ -1277,17 +1483,17 @@ class DataSourceEventHandler {
|
|
|
1277
1483
|
* Tracks the current data source status and emits updates when the status changes.
|
|
1278
1484
|
*/
|
|
1279
1485
|
class DataSourceStatusManager {
|
|
1280
|
-
constructor(
|
|
1281
|
-
this.
|
|
1282
|
-
this.
|
|
1283
|
-
this.
|
|
1284
|
-
this.
|
|
1486
|
+
constructor(_emitter, timeStamper = () => Date.now()) {
|
|
1487
|
+
this._emitter = _emitter;
|
|
1488
|
+
this._state = DataSourceState.Closed;
|
|
1489
|
+
this._stateSinceMillis = timeStamper();
|
|
1490
|
+
this._timeStamper = timeStamper;
|
|
1285
1491
|
}
|
|
1286
1492
|
get status() {
|
|
1287
1493
|
return {
|
|
1288
|
-
state: this.
|
|
1289
|
-
stateSince: this.
|
|
1290
|
-
lastError: this.
|
|
1494
|
+
state: this._state,
|
|
1495
|
+
stateSince: this._stateSinceMillis,
|
|
1496
|
+
lastError: this._errorInfo,
|
|
1291
1497
|
};
|
|
1292
1498
|
}
|
|
1293
1499
|
/**
|
|
@@ -1296,17 +1502,17 @@ class DataSourceStatusManager {
|
|
|
1296
1502
|
* @param requestedState to track
|
|
1297
1503
|
* @param isError to indicate that the state update is a result of an error occurring.
|
|
1298
1504
|
*/
|
|
1299
|
-
|
|
1300
|
-
const newState = requestedState === DataSourceState.Interrupted && this.
|
|
1505
|
+
_updateState(requestedState, isError = false) {
|
|
1506
|
+
const newState = requestedState === DataSourceState.Interrupted && this._state === DataSourceState.Initializing // don't go to interrupted from initializing (recoverable errors when initializing are not noteworthy)
|
|
1301
1507
|
? DataSourceState.Initializing
|
|
1302
1508
|
: requestedState;
|
|
1303
|
-
const changedState = this.
|
|
1509
|
+
const changedState = this._state !== newState;
|
|
1304
1510
|
if (changedState) {
|
|
1305
|
-
this.
|
|
1306
|
-
this.
|
|
1511
|
+
this._state = newState;
|
|
1512
|
+
this._stateSinceMillis = this._timeStamper();
|
|
1307
1513
|
}
|
|
1308
1514
|
if (changedState || isError) {
|
|
1309
|
-
this.
|
|
1515
|
+
this._emitter.emit('dataSourceStatus', this.status);
|
|
1310
1516
|
}
|
|
1311
1517
|
}
|
|
1312
1518
|
/**
|
|
@@ -1315,7 +1521,7 @@ class DataSourceStatusManager {
|
|
|
1315
1521
|
* @param state that is requested
|
|
1316
1522
|
*/
|
|
1317
1523
|
requestStateUpdate(state) {
|
|
1318
|
-
this.
|
|
1524
|
+
this._updateState(state);
|
|
1319
1525
|
}
|
|
1320
1526
|
/**
|
|
1321
1527
|
* Reports a datasource error to this manager. Since the {@link DataSourceStatus} includes error
|
|
@@ -1332,10 +1538,10 @@ class DataSourceStatusManager {
|
|
|
1332
1538
|
kind,
|
|
1333
1539
|
message,
|
|
1334
1540
|
statusCode,
|
|
1335
|
-
time: this.
|
|
1541
|
+
time: this._timeStamper(),
|
|
1336
1542
|
};
|
|
1337
|
-
this.
|
|
1338
|
-
this.
|
|
1543
|
+
this._errorInfo = errorInfo;
|
|
1544
|
+
this._updateState(recoverable ? DataSourceState.Interrupted : DataSourceState.Closed, true);
|
|
1339
1545
|
}
|
|
1340
1546
|
}
|
|
1341
1547
|
|
|
@@ -1343,54 +1549,34 @@ class DataSourceStatusManager {
|
|
|
1343
1549
|
* @internal
|
|
1344
1550
|
*/
|
|
1345
1551
|
class PollingProcessor {
|
|
1346
|
-
constructor(
|
|
1347
|
-
this.
|
|
1348
|
-
this.
|
|
1349
|
-
this.
|
|
1350
|
-
this.
|
|
1351
|
-
this.
|
|
1352
|
-
this.
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
const parameters = [
|
|
1357
|
-
...(dataSourceConfig.queryParameters ?? []),
|
|
1358
|
-
];
|
|
1359
|
-
if (this.dataSourceConfig.withReasons) {
|
|
1360
|
-
parameters.push({ key: 'withReasons', value: 'true' });
|
|
1361
|
-
}
|
|
1362
|
-
const uri = getPollingUri(dataSourceConfig.serviceEndpoints, path, parameters);
|
|
1363
|
-
this.pollInterval = dataSourceConfig.pollInterval;
|
|
1364
|
-
let method = 'GET';
|
|
1365
|
-
const headers = { ...dataSourceConfig.baseHeaders };
|
|
1366
|
-
let body;
|
|
1367
|
-
if (dataSourceConfig.useReport) {
|
|
1368
|
-
method = 'REPORT';
|
|
1369
|
-
headers['content-type'] = 'application/json';
|
|
1370
|
-
body = plainContextString; // context is in body for REPORT
|
|
1371
|
-
}
|
|
1372
|
-
this.requestor = new Requestor(requests, uri, headers, method, body);
|
|
1373
|
-
}
|
|
1374
|
-
async poll() {
|
|
1375
|
-
if (this.stopped) {
|
|
1552
|
+
constructor(_requestor, _pollIntervalSeconds, _dataHandler, _errorHandler, _logger) {
|
|
1553
|
+
this._requestor = _requestor;
|
|
1554
|
+
this._pollIntervalSeconds = _pollIntervalSeconds;
|
|
1555
|
+
this._dataHandler = _dataHandler;
|
|
1556
|
+
this._errorHandler = _errorHandler;
|
|
1557
|
+
this._logger = _logger;
|
|
1558
|
+
this._stopped = false;
|
|
1559
|
+
}
|
|
1560
|
+
async _poll() {
|
|
1561
|
+
if (this._stopped) {
|
|
1376
1562
|
return;
|
|
1377
1563
|
}
|
|
1378
1564
|
const reportJsonError = (data) => {
|
|
1379
|
-
this.
|
|
1380
|
-
this.
|
|
1381
|
-
this.
|
|
1565
|
+
this._logger?.error('Polling received invalid data');
|
|
1566
|
+
this._logger?.debug(`Invalid JSON follows: ${data}`);
|
|
1567
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in polling response'));
|
|
1382
1568
|
};
|
|
1383
|
-
this.
|
|
1569
|
+
this._logger?.debug('Polling LaunchDarkly for feature flag updates');
|
|
1384
1570
|
const startTime = Date.now();
|
|
1385
1571
|
try {
|
|
1386
|
-
const res = await this.
|
|
1572
|
+
const res = await this._requestor.requestPayload();
|
|
1387
1573
|
try {
|
|
1388
1574
|
const flags = JSON.parse(res);
|
|
1389
1575
|
try {
|
|
1390
|
-
this.
|
|
1576
|
+
this._dataHandler?.(flags);
|
|
1391
1577
|
}
|
|
1392
1578
|
catch (err) {
|
|
1393
|
-
this.
|
|
1579
|
+
this._logger?.error(`Exception from data handler: ${err}`);
|
|
1394
1580
|
}
|
|
1395
1581
|
}
|
|
1396
1582
|
catch {
|
|
@@ -1401,29 +1587,29 @@ class PollingProcessor {
|
|
|
1401
1587
|
const requestError = err;
|
|
1402
1588
|
if (requestError.status !== undefined) {
|
|
1403
1589
|
if (!isHttpRecoverable(requestError.status)) {
|
|
1404
|
-
this.
|
|
1405
|
-
this.
|
|
1590
|
+
this._logger?.error(httpErrorMessage(err, 'polling request'));
|
|
1591
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.ErrorResponse, requestError.message, requestError.status));
|
|
1406
1592
|
return;
|
|
1407
1593
|
}
|
|
1408
1594
|
}
|
|
1409
|
-
this.
|
|
1595
|
+
this._logger?.error(httpErrorMessage(err, 'polling request', 'will retry'));
|
|
1410
1596
|
}
|
|
1411
1597
|
const elapsed = Date.now() - startTime;
|
|
1412
|
-
const sleepFor = Math.max(this.
|
|
1413
|
-
this.
|
|
1414
|
-
this.
|
|
1415
|
-
this.
|
|
1598
|
+
const sleepFor = Math.max(this._pollIntervalSeconds * 1000 - elapsed, 0);
|
|
1599
|
+
this._logger?.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
|
|
1600
|
+
this._timeoutHandle = setTimeout(() => {
|
|
1601
|
+
this._poll();
|
|
1416
1602
|
}, sleepFor);
|
|
1417
1603
|
}
|
|
1418
1604
|
start() {
|
|
1419
|
-
this.
|
|
1605
|
+
this._poll();
|
|
1420
1606
|
}
|
|
1421
1607
|
stop() {
|
|
1422
|
-
if (this.
|
|
1423
|
-
clearTimeout(this.
|
|
1424
|
-
this.
|
|
1608
|
+
if (this._timeoutHandle) {
|
|
1609
|
+
clearTimeout(this._timeoutHandle);
|
|
1610
|
+
this._timeoutHandle = undefined;
|
|
1425
1611
|
}
|
|
1426
|
-
this.
|
|
1612
|
+
this._stopped = true;
|
|
1427
1613
|
}
|
|
1428
1614
|
close() {
|
|
1429
1615
|
this.stop();
|
|
@@ -1436,40 +1622,43 @@ const reportJsonError = (type, data, logger, errorHandler) => {
|
|
|
1436
1622
|
errorHandler?.(new LDStreamingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in event stream'));
|
|
1437
1623
|
};
|
|
1438
1624
|
class StreamingProcessor {
|
|
1439
|
-
constructor(
|
|
1440
|
-
this.
|
|
1441
|
-
this.
|
|
1442
|
-
this.
|
|
1443
|
-
this.
|
|
1444
|
-
this.
|
|
1445
|
-
this.
|
|
1446
|
-
this.
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1625
|
+
constructor(_plainContextString, _dataSourceConfig, _listeners, _requests, encoding, _pollingRequestor, _diagnosticsManager, _errorHandler, _logger) {
|
|
1626
|
+
this._plainContextString = _plainContextString;
|
|
1627
|
+
this._dataSourceConfig = _dataSourceConfig;
|
|
1628
|
+
this._listeners = _listeners;
|
|
1629
|
+
this._requests = _requests;
|
|
1630
|
+
this._pollingRequestor = _pollingRequestor;
|
|
1631
|
+
this._diagnosticsManager = _diagnosticsManager;
|
|
1632
|
+
this._errorHandler = _errorHandler;
|
|
1633
|
+
this._logger = _logger;
|
|
1634
|
+
let path;
|
|
1635
|
+
if (_dataSourceConfig.useReport && !_requests.getEventSourceCapabilities().customMethod) {
|
|
1636
|
+
path = _dataSourceConfig.paths.pathPing(encoding, _plainContextString);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
path = _dataSourceConfig.useReport
|
|
1640
|
+
? _dataSourceConfig.paths.pathReport(encoding, _plainContextString)
|
|
1641
|
+
: _dataSourceConfig.paths.pathGet(encoding, _plainContextString);
|
|
1642
|
+
}
|
|
1454
1643
|
const parameters = [
|
|
1455
|
-
...(
|
|
1644
|
+
...(_dataSourceConfig.queryParameters ?? []),
|
|
1456
1645
|
];
|
|
1457
|
-
if (this.
|
|
1646
|
+
if (this._dataSourceConfig.withReasons) {
|
|
1458
1647
|
parameters.push({ key: 'withReasons', value: 'true' });
|
|
1459
1648
|
}
|
|
1460
|
-
this.
|
|
1461
|
-
this.
|
|
1462
|
-
this.
|
|
1463
|
-
this.
|
|
1649
|
+
this._requests = _requests;
|
|
1650
|
+
this._headers = { ..._dataSourceConfig.baseHeaders };
|
|
1651
|
+
this._logger = _logger;
|
|
1652
|
+
this._streamUri = getStreamingUri(_dataSourceConfig.serviceEndpoints, path, parameters);
|
|
1464
1653
|
}
|
|
1465
|
-
|
|
1466
|
-
this.
|
|
1654
|
+
_logConnectionStarted() {
|
|
1655
|
+
this._connectionAttemptStartTime = Date.now();
|
|
1467
1656
|
}
|
|
1468
|
-
|
|
1469
|
-
if (this.
|
|
1470
|
-
this.
|
|
1657
|
+
_logConnectionResult(success) {
|
|
1658
|
+
if (this._connectionAttemptStartTime && this._diagnosticsManager) {
|
|
1659
|
+
this._diagnosticsManager.recordStreamInit(this._connectionAttemptStartTime, !success, Date.now() - this._connectionAttemptStartTime);
|
|
1471
1660
|
}
|
|
1472
|
-
this.
|
|
1661
|
+
this._connectionAttemptStartTime = undefined;
|
|
1473
1662
|
}
|
|
1474
1663
|
/**
|
|
1475
1664
|
* This is a wrapper around the passed errorHandler which adds additional
|
|
@@ -1480,75 +1669,101 @@ class StreamingProcessor {
|
|
|
1480
1669
|
*
|
|
1481
1670
|
* @private
|
|
1482
1671
|
*/
|
|
1483
|
-
|
|
1672
|
+
_retryAndHandleError(err) {
|
|
1484
1673
|
if (!shouldRetry(err)) {
|
|
1485
|
-
this.
|
|
1486
|
-
this.
|
|
1487
|
-
this.
|
|
1674
|
+
this._logConnectionResult(false);
|
|
1675
|
+
this._errorHandler?.(new LDStreamingError(DataSourceErrorKind.ErrorResponse, err.message, err.status, false));
|
|
1676
|
+
this._logger?.error(httpErrorMessage(err, 'streaming request'));
|
|
1488
1677
|
return false;
|
|
1489
1678
|
}
|
|
1490
|
-
this.
|
|
1491
|
-
this.
|
|
1492
|
-
this.
|
|
1679
|
+
this._logger?.warn(httpErrorMessage(err, 'streaming request', 'will retry'));
|
|
1680
|
+
this._logConnectionResult(false);
|
|
1681
|
+
this._logConnectionStarted();
|
|
1493
1682
|
return true;
|
|
1494
1683
|
}
|
|
1495
1684
|
start() {
|
|
1496
|
-
this.
|
|
1685
|
+
this._logConnectionStarted();
|
|
1497
1686
|
let methodAndBodyOverrides;
|
|
1498
|
-
if (this.
|
|
1687
|
+
if (this._dataSourceConfig.useReport) {
|
|
1499
1688
|
// REPORT will include a body, so content type is required.
|
|
1500
|
-
this.
|
|
1689
|
+
this._headers['content-type'] = 'application/json';
|
|
1501
1690
|
// orverrides default method with REPORT and adds body.
|
|
1502
|
-
methodAndBodyOverrides = { method: 'REPORT', body: this.
|
|
1691
|
+
methodAndBodyOverrides = { method: 'REPORT', body: this._plainContextString };
|
|
1503
1692
|
}
|
|
1504
1693
|
else {
|
|
1505
1694
|
// no method or body override
|
|
1506
1695
|
methodAndBodyOverrides = {};
|
|
1507
1696
|
}
|
|
1508
1697
|
// TLS is handled by the platform implementation.
|
|
1509
|
-
const eventSource = this.
|
|
1510
|
-
headers: this.
|
|
1698
|
+
const eventSource = this._requests.createEventSource(this._streamUri, {
|
|
1699
|
+
headers: this._headers,
|
|
1511
1700
|
...methodAndBodyOverrides,
|
|
1512
|
-
errorFilter: (error) => this.
|
|
1513
|
-
initialRetryDelayMillis: this.
|
|
1701
|
+
errorFilter: (error) => this._retryAndHandleError(error),
|
|
1702
|
+
initialRetryDelayMillis: this._dataSourceConfig.initialRetryDelayMillis,
|
|
1514
1703
|
readTimeoutMillis: 5 * 60 * 1000,
|
|
1515
1704
|
retryResetIntervalMillis: 60 * 1000,
|
|
1516
1705
|
});
|
|
1517
|
-
this.
|
|
1706
|
+
this._eventSource = eventSource;
|
|
1518
1707
|
eventSource.onclose = () => {
|
|
1519
|
-
this.
|
|
1708
|
+
this._logger?.info('Closed LaunchDarkly stream connection');
|
|
1520
1709
|
};
|
|
1521
1710
|
eventSource.onerror = () => {
|
|
1522
1711
|
// The work is done by `errorFilter`.
|
|
1523
1712
|
};
|
|
1524
1713
|
eventSource.onopen = () => {
|
|
1525
|
-
this.
|
|
1714
|
+
this._logger?.info('Opened LaunchDarkly stream connection');
|
|
1526
1715
|
};
|
|
1527
1716
|
eventSource.onretrying = (e) => {
|
|
1528
|
-
this.
|
|
1717
|
+
this._logger?.info(`Will retry stream connection in ${e.delayMillis} milliseconds`);
|
|
1529
1718
|
};
|
|
1530
|
-
this.
|
|
1719
|
+
this._listeners.forEach(({ deserializeData, processJson }, eventName) => {
|
|
1531
1720
|
eventSource.addEventListener(eventName, (event) => {
|
|
1532
|
-
this.
|
|
1721
|
+
this._logger?.debug(`Received ${eventName} event`);
|
|
1533
1722
|
if (event?.data) {
|
|
1534
|
-
this.
|
|
1723
|
+
this._logConnectionResult(true);
|
|
1535
1724
|
const { data } = event;
|
|
1536
1725
|
const dataJson = deserializeData(data);
|
|
1537
1726
|
if (!dataJson) {
|
|
1538
|
-
reportJsonError(eventName, data, this.
|
|
1727
|
+
reportJsonError(eventName, data, this._logger, this._errorHandler);
|
|
1539
1728
|
return;
|
|
1540
1729
|
}
|
|
1541
1730
|
processJson(dataJson);
|
|
1542
1731
|
}
|
|
1543
1732
|
else {
|
|
1544
|
-
this.
|
|
1733
|
+
this._errorHandler?.(new LDStreamingError(DataSourceErrorKind.InvalidData, 'Unexpected payload from event stream'));
|
|
1545
1734
|
}
|
|
1546
1735
|
});
|
|
1547
1736
|
});
|
|
1737
|
+
// here we set up a listener that will poll when ping is received
|
|
1738
|
+
eventSource.addEventListener('ping', async () => {
|
|
1739
|
+
this._logger?.debug('Got PING, going to poll LaunchDarkly for feature flag updates');
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await this._pollingRequestor.requestPayload();
|
|
1742
|
+
try {
|
|
1743
|
+
const payload = JSON.parse(res);
|
|
1744
|
+
try {
|
|
1745
|
+
// forward the payload on to the PUT listener
|
|
1746
|
+
this._listeners.get('put')?.processJson(payload);
|
|
1747
|
+
}
|
|
1748
|
+
catch (err) {
|
|
1749
|
+
this._logger?.error(`Exception from data handler: ${err}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
catch {
|
|
1753
|
+
this._logger?.error('Polling after ping received invalid data');
|
|
1754
|
+
this._logger?.debug(`Invalid JSON follows: ${res}`);
|
|
1755
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in ping polling response'));
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
const requestError = err;
|
|
1760
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.ErrorResponse, requestError.message, requestError.status));
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1548
1763
|
}
|
|
1549
1764
|
stop() {
|
|
1550
|
-
this.
|
|
1551
|
-
this.
|
|
1765
|
+
this._eventSource?.close();
|
|
1766
|
+
this._eventSource = undefined;
|
|
1552
1767
|
}
|
|
1553
1768
|
close() {
|
|
1554
1769
|
this.stop();
|
|
@@ -1568,35 +1783,26 @@ class BaseDataManager {
|
|
|
1568
1783
|
this.diagnosticsManager = diagnosticsManager;
|
|
1569
1784
|
this.logger = config.logger;
|
|
1570
1785
|
this.dataSourceStatusManager = new DataSourceStatusManager(emitter);
|
|
1571
|
-
this.
|
|
1786
|
+
this._dataSourceEventHandler = new DataSourceEventHandler(flagManager, this.dataSourceStatusManager, this.config.logger);
|
|
1572
1787
|
}
|
|
1573
1788
|
/**
|
|
1574
1789
|
* Set additional connection parameters for requests polling/streaming.
|
|
1575
1790
|
*/
|
|
1576
1791
|
setConnectionParams(connectionParams) {
|
|
1577
|
-
this.
|
|
1792
|
+
this._connectionParams = connectionParams;
|
|
1578
1793
|
}
|
|
1579
|
-
createPollingProcessor(context, checkedContext, identifyResolve, identifyReject) {
|
|
1580
|
-
const processor = new PollingProcessor(
|
|
1581
|
-
|
|
1582
|
-
serviceEndpoints: this.config.serviceEndpoints,
|
|
1583
|
-
paths: this.getPollingPaths(),
|
|
1584
|
-
baseHeaders: this.baseHeaders,
|
|
1585
|
-
pollInterval: this.config.pollInterval,
|
|
1586
|
-
withReasons: this.config.withReasons,
|
|
1587
|
-
useReport: this.config.useReport,
|
|
1588
|
-
queryParameters: this.connectionParams?.queryParameters,
|
|
1589
|
-
}, this.platform.requests, this.platform.encoding, async (flags) => {
|
|
1590
|
-
await this.dataSourceEventHandler.handlePut(checkedContext, flags);
|
|
1794
|
+
createPollingProcessor(context, checkedContext, requestor, identifyResolve, identifyReject) {
|
|
1795
|
+
const processor = new PollingProcessor(requestor, this.config.pollInterval, async (flags) => {
|
|
1796
|
+
await this._dataSourceEventHandler.handlePut(checkedContext, flags);
|
|
1591
1797
|
identifyResolve?.();
|
|
1592
1798
|
}, (err) => {
|
|
1593
1799
|
this.emitter.emit('error', context, err);
|
|
1594
|
-
this.
|
|
1800
|
+
this._dataSourceEventHandler.handlePollingError(err);
|
|
1595
1801
|
identifyReject?.(err);
|
|
1596
|
-
});
|
|
1597
|
-
this.updateProcessor = this.
|
|
1802
|
+
}, this.logger);
|
|
1803
|
+
this.updateProcessor = this._decorateProcessorWithStatusReporting(processor, this.dataSourceStatusManager);
|
|
1598
1804
|
}
|
|
1599
|
-
createStreamingProcessor(context, checkedContext, identifyResolve, identifyReject) {
|
|
1805
|
+
createStreamingProcessor(context, checkedContext, pollingRequestor, identifyResolve, identifyReject) {
|
|
1600
1806
|
const processor = new StreamingProcessor(JSON.stringify(context), {
|
|
1601
1807
|
credential: this.credential,
|
|
1602
1808
|
serviceEndpoints: this.config.serviceEndpoints,
|
|
@@ -1605,38 +1811,38 @@ class BaseDataManager {
|
|
|
1605
1811
|
initialRetryDelayMillis: this.config.streamInitialReconnectDelay * 1000,
|
|
1606
1812
|
withReasons: this.config.withReasons,
|
|
1607
1813
|
useReport: this.config.useReport,
|
|
1608
|
-
queryParameters: this.
|
|
1609
|
-
}, this.createStreamListeners(checkedContext, identifyResolve), this.platform.requests, this.platform.encoding, this.diagnosticsManager, (e) => {
|
|
1814
|
+
queryParameters: this._connectionParams?.queryParameters,
|
|
1815
|
+
}, this.createStreamListeners(checkedContext, identifyResolve), this.platform.requests, this.platform.encoding, pollingRequestor, this.diagnosticsManager, (e) => {
|
|
1610
1816
|
this.emitter.emit('error', context, e);
|
|
1611
|
-
this.
|
|
1817
|
+
this._dataSourceEventHandler.handleStreamingError(e);
|
|
1612
1818
|
identifyReject?.(e);
|
|
1613
|
-
});
|
|
1614
|
-
this.updateProcessor = this.
|
|
1819
|
+
}, this.logger);
|
|
1820
|
+
this.updateProcessor = this._decorateProcessorWithStatusReporting(processor, this.dataSourceStatusManager);
|
|
1615
1821
|
}
|
|
1616
1822
|
createStreamListeners(context, identifyResolve) {
|
|
1617
1823
|
const listeners = new Map();
|
|
1618
1824
|
listeners.set('put', {
|
|
1619
1825
|
deserializeData: JSON.parse,
|
|
1620
1826
|
processJson: async (flags) => {
|
|
1621
|
-
await this.
|
|
1827
|
+
await this._dataSourceEventHandler.handlePut(context, flags);
|
|
1622
1828
|
identifyResolve?.();
|
|
1623
1829
|
},
|
|
1624
1830
|
});
|
|
1625
1831
|
listeners.set('patch', {
|
|
1626
1832
|
deserializeData: JSON.parse,
|
|
1627
1833
|
processJson: async (patchFlag) => {
|
|
1628
|
-
this.
|
|
1834
|
+
this._dataSourceEventHandler.handlePatch(context, patchFlag);
|
|
1629
1835
|
},
|
|
1630
1836
|
});
|
|
1631
1837
|
listeners.set('delete', {
|
|
1632
1838
|
deserializeData: JSON.parse,
|
|
1633
1839
|
processJson: async (deleteFlag) => {
|
|
1634
|
-
this.
|
|
1840
|
+
this._dataSourceEventHandler.handleDelete(context, deleteFlag);
|
|
1635
1841
|
},
|
|
1636
1842
|
});
|
|
1637
1843
|
return listeners;
|
|
1638
1844
|
}
|
|
1639
|
-
|
|
1845
|
+
_decorateProcessorWithStatusReporting(processor, statusManager) {
|
|
1640
1846
|
return {
|
|
1641
1847
|
start: () => {
|
|
1642
1848
|
// update status before starting processor to ensure potential errors are reported after initializing
|
|
@@ -1655,5 +1861,5 @@ class BaseDataManager {
|
|
|
1655
1861
|
}
|
|
1656
1862
|
}
|
|
1657
1863
|
|
|
1658
|
-
export { BaseDataManager, DataSourceState, LDClientImpl, Requestor };
|
|
1864
|
+
export { BaseDataManager, DataSourceState, LDClientImpl, Requestor, makeRequestor };
|
|
1659
1865
|
//# sourceMappingURL=index.mjs.map
|