@launchdarkly/js-client-sdk-common 1.23.0 → 1.25.0
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/CHANGELOG.md +37 -0
- package/dist/cjs/DataManager.d.ts +9 -0
- package/dist/cjs/DataManager.d.ts.map +1 -1
- package/dist/cjs/LDClientImpl.d.ts +19 -8
- package/dist/cjs/LDClientImpl.d.ts.map +1 -1
- package/dist/cjs/api/LDOptions.d.ts +17 -5
- package/dist/cjs/api/LDOptions.d.ts.map +1 -1
- package/dist/cjs/api/LDStartOptions.d.ts +19 -0
- package/dist/cjs/api/LDStartOptions.d.ts.map +1 -0
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts +46 -0
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts +5 -0
- package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/api/datasource/ModeDefinition.d.ts +17 -1
- package/dist/cjs/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/cjs/api/datasource/index.d.ts +2 -2
- package/dist/cjs/api/datasource/index.d.ts.map +1 -1
- package/dist/cjs/api/index.d.ts +1 -0
- package/dist/cjs/api/index.d.ts.map +1 -1
- package/dist/cjs/configuration/Configuration.d.ts +13 -3
- package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
- package/dist/cjs/configuration/validateOptions.d.ts +18 -2
- package/dist/cjs/configuration/validateOptions.d.ts.map +1 -1
- package/dist/cjs/configuration/validators.d.ts +1 -1
- package/dist/cjs/configuration/validators.d.ts.map +1 -1
- package/dist/cjs/datasource/ConnectionModeConfig.d.ts +4 -2
- package/dist/cjs/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/cjs/datasource/FDv2DataManagerBase.d.ts +87 -0
- package/dist/cjs/datasource/FDv2DataManagerBase.d.ts.map +1 -0
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts +37 -3
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
- package/dist/cjs/index.cjs +1295 -104
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +8 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/esm/DataManager.d.ts +9 -0
- package/dist/esm/DataManager.d.ts.map +1 -1
- package/dist/esm/LDClientImpl.d.ts +19 -8
- package/dist/esm/LDClientImpl.d.ts.map +1 -1
- package/dist/esm/api/LDOptions.d.ts +17 -5
- package/dist/esm/api/LDOptions.d.ts.map +1 -1
- package/dist/esm/api/LDStartOptions.d.ts +19 -0
- package/dist/esm/api/LDStartOptions.d.ts.map +1 -0
- package/dist/esm/api/datasource/DataSourceEntry.d.ts +46 -0
- package/dist/esm/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts +5 -0
- package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/api/datasource/ModeDefinition.d.ts +17 -1
- package/dist/esm/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/esm/api/datasource/index.d.ts +2 -2
- package/dist/esm/api/datasource/index.d.ts.map +1 -1
- package/dist/esm/api/index.d.ts +1 -0
- package/dist/esm/api/index.d.ts.map +1 -1
- package/dist/esm/configuration/Configuration.d.ts +13 -3
- package/dist/esm/configuration/Configuration.d.ts.map +1 -1
- package/dist/esm/configuration/validateOptions.d.ts +18 -2
- package/dist/esm/configuration/validateOptions.d.ts.map +1 -1
- package/dist/esm/configuration/validators.d.ts +1 -1
- package/dist/esm/configuration/validators.d.ts.map +1 -1
- package/dist/esm/datasource/ConnectionModeConfig.d.ts +4 -2
- package/dist/esm/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/esm/datasource/FDv2DataManagerBase.d.ts +87 -0
- package/dist/esm/datasource/FDv2DataManagerBase.d.ts.map +1 -0
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts +37 -3
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
- package/dist/esm/index.d.ts +8 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.mjs +1294 -106
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -293,10 +293,18 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
|
|
|
293
293
|
* Creates a validator for nested objects. When used in a validator map,
|
|
294
294
|
* `validateOptions` will recursively validate the nested object's properties.
|
|
295
295
|
* Defaults for nested fields are passed through from the parent.
|
|
296
|
+
*
|
|
297
|
+
* @param validators - Validator map for the nested object's fields.
|
|
298
|
+
* @param options - Optional configuration.
|
|
299
|
+
* @param options.defaults - Built-in defaults for nested fields.
|
|
300
|
+
* @param options.is - Custom `is` predicate. When provided, replaces the
|
|
301
|
+
* default "is object" check. Use this to discriminate between object shapes
|
|
302
|
+
* in an `anyOf` (e.g., matching on a `type` discriminant field).
|
|
296
303
|
*/
|
|
297
|
-
function validatorOf(validators,
|
|
304
|
+
function validatorOf(validators, options) {
|
|
305
|
+
const builtInDefaults = options?.defaults;
|
|
298
306
|
return {
|
|
299
|
-
is: (u) => jsSdkCommon.TypeValidators.Object.is(u),
|
|
307
|
+
is: options?.is ?? ((u) => jsSdkCommon.TypeValidators.Object.is(u)),
|
|
300
308
|
getType: () => 'object',
|
|
301
309
|
validate(value, name, logger, defaults) {
|
|
302
310
|
if (!jsSdkCommon.TypeValidators.Object.is(value)) {
|
|
@@ -404,6 +412,10 @@ function anyOf(...validators) {
|
|
|
404
412
|
*
|
|
405
413
|
* @param keyValidator - Validates that each key is an allowed value.
|
|
406
414
|
* @param valueValidator - Validates each value in the record.
|
|
415
|
+
* @param options - Optional configuration.
|
|
416
|
+
* @param options.defaults - Built-in defaults for the record entries. When
|
|
417
|
+
* provided, takes priority over defaults passed from the parent at
|
|
418
|
+
* validation time (same precedence as {@link validatorOf}).
|
|
407
419
|
*
|
|
408
420
|
* @example
|
|
409
421
|
* ```ts
|
|
@@ -415,7 +427,8 @@ function anyOf(...validators) {
|
|
|
415
427
|
* )
|
|
416
428
|
* ```
|
|
417
429
|
*/
|
|
418
|
-
function recordOf(keyValidator, valueValidator) {
|
|
430
|
+
function recordOf(keyValidator, valueValidator, options) {
|
|
431
|
+
const builtInDefaults = options?.defaults;
|
|
419
432
|
return {
|
|
420
433
|
is: (u) => jsSdkCommon.TypeValidators.Object.is(u),
|
|
421
434
|
getType: () => 'object',
|
|
@@ -440,14 +453,14 @@ function recordOf(keyValidator, valueValidator) {
|
|
|
440
453
|
logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, keyValidator.getType(), key));
|
|
441
454
|
}
|
|
442
455
|
});
|
|
443
|
-
const recordDefaults =
|
|
444
|
-
? defaults
|
|
445
|
-
: {};
|
|
456
|
+
const recordDefaults = builtInDefaults ??
|
|
457
|
+
(jsSdkCommon.TypeValidators.Object.is(defaults) ? defaults : {});
|
|
446
458
|
return { value: validateOptions(filtered, validatorMap, recordDefaults, logger, name) };
|
|
447
459
|
},
|
|
448
460
|
};
|
|
449
461
|
}
|
|
450
462
|
|
|
463
|
+
const DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS = 300;
|
|
451
464
|
const BACKGROUND_POLL_INTERVAL_SECONDS = 3600;
|
|
452
465
|
const dataSourceTypeValidator = jsSdkCommon.TypeValidators.oneOf('cache', 'polling', 'streaming');
|
|
453
466
|
const connectionModeValidator = jsSdkCommon.TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
|
|
@@ -477,19 +490,25 @@ const synchronizerEntryArrayValidator = arrayOf('type', {
|
|
|
477
490
|
polling: pollingEntryValidators,
|
|
478
491
|
streaming: streamingEntryValidators,
|
|
479
492
|
});
|
|
493
|
+
const fdv1FallbackValidators = {
|
|
494
|
+
pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
|
|
495
|
+
endpoints: validatorOf(endpointValidators),
|
|
496
|
+
};
|
|
480
497
|
const modeDefinitionValidators = {
|
|
481
498
|
initializers: initializerEntryArrayValidator,
|
|
482
499
|
synchronizers: synchronizerEntryArrayValidator,
|
|
500
|
+
fdv1Fallback: validatorOf(fdv1FallbackValidators),
|
|
483
501
|
};
|
|
484
|
-
const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
|
|
485
502
|
const MODE_TABLE = {
|
|
486
503
|
streaming: {
|
|
487
504
|
initializers: [{ type: 'cache' }, { type: 'polling' }],
|
|
488
505
|
synchronizers: [{ type: 'streaming' }, { type: 'polling' }],
|
|
506
|
+
fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
|
|
489
507
|
},
|
|
490
508
|
polling: {
|
|
491
509
|
initializers: [{ type: 'cache' }],
|
|
492
510
|
synchronizers: [{ type: 'polling' }],
|
|
511
|
+
fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
|
|
493
512
|
},
|
|
494
513
|
offline: {
|
|
495
514
|
initializers: [{ type: 'cache' }],
|
|
@@ -502,24 +521,33 @@ const MODE_TABLE = {
|
|
|
502
521
|
background: {
|
|
503
522
|
initializers: [{ type: 'cache' }],
|
|
504
523
|
synchronizers: [{ type: 'polling', pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS }],
|
|
524
|
+
fdv1Fallback: { pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS },
|
|
505
525
|
},
|
|
506
526
|
};
|
|
527
|
+
const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
|
|
507
528
|
|
|
508
|
-
|
|
529
|
+
function hasType(u, type) {
|
|
530
|
+
return jsSdkCommon.TypeValidators.Object.is(u) && u.type === type;
|
|
531
|
+
}
|
|
532
|
+
const automaticModeValidators = {
|
|
533
|
+
type: jsSdkCommon.TypeValidators.oneOf('automatic'),
|
|
509
534
|
lifecycle: jsSdkCommon.TypeValidators.Boolean,
|
|
510
535
|
network: jsSdkCommon.TypeValidators.Boolean,
|
|
511
536
|
};
|
|
512
|
-
const
|
|
537
|
+
const manualModeValidators = {
|
|
538
|
+
type: jsSdkCommon.TypeValidators.oneOf('manual'),
|
|
513
539
|
initialConnectionMode: connectionModeValidator,
|
|
540
|
+
};
|
|
541
|
+
const dataSystemValidators = {
|
|
514
542
|
backgroundConnectionMode: connectionModeValidator,
|
|
515
|
-
automaticModeSwitching: anyOf(jsSdkCommon.TypeValidators.Boolean, validatorOf(
|
|
543
|
+
automaticModeSwitching: anyOf(jsSdkCommon.TypeValidators.Boolean, validatorOf(automaticModeValidators, { is: (u) => hasType(u, 'automatic') }), validatorOf(manualModeValidators, { is: (u) => hasType(u, 'manual') })),
|
|
516
544
|
connectionModes: connectionModesValidator,
|
|
517
545
|
};
|
|
518
546
|
/**
|
|
519
547
|
* Default FDv2 data system configuration for browser SDKs.
|
|
520
548
|
*/
|
|
521
549
|
const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
522
|
-
|
|
550
|
+
foregroundConnectionMode: 'one-shot',
|
|
523
551
|
backgroundConnectionMode: undefined,
|
|
524
552
|
automaticModeSwitching: false,
|
|
525
553
|
};
|
|
@@ -527,7 +555,7 @@ const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
|
527
555
|
* Default FDv2 data system configuration for mobile (React Native) SDKs.
|
|
528
556
|
*/
|
|
529
557
|
const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
530
|
-
|
|
558
|
+
foregroundConnectionMode: 'streaming',
|
|
531
559
|
backgroundConnectionMode: 'background',
|
|
532
560
|
automaticModeSwitching: true,
|
|
533
561
|
};
|
|
@@ -535,10 +563,24 @@ const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
|
535
563
|
* Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
|
|
536
564
|
*/
|
|
537
565
|
const DESKTOP_DATA_SYSTEM_DEFAULTS = {
|
|
538
|
-
|
|
566
|
+
foregroundConnectionMode: 'streaming',
|
|
539
567
|
backgroundConnectionMode: undefined,
|
|
540
568
|
automaticModeSwitching: false,
|
|
541
569
|
};
|
|
570
|
+
function isManualModeSwitching(value) {
|
|
571
|
+
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual';
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Resolve the foreground connection mode from a validated data system config
|
|
575
|
+
* and platform defaults. Uses the mode from `ManualModeSwitching` when present,
|
|
576
|
+
* otherwise falls back to the platform default.
|
|
577
|
+
*/
|
|
578
|
+
function resolveForegroundMode(dataSystem, defaults) {
|
|
579
|
+
if (isManualModeSwitching(dataSystem.automaticModeSwitching)) {
|
|
580
|
+
return dataSystem.automaticModeSwitching.initialConnectionMode;
|
|
581
|
+
}
|
|
582
|
+
return dataSystem.foregroundConnectionMode ?? defaults.foregroundConnectionMode;
|
|
583
|
+
}
|
|
542
584
|
|
|
543
585
|
function createValidators(options) {
|
|
544
586
|
return {
|
|
@@ -568,7 +610,12 @@ function createValidators(options) {
|
|
|
568
610
|
inspectors: jsSdkCommon.TypeValidators.createTypeArray('LDInspection', {}),
|
|
569
611
|
cleanOldPersistentData: jsSdkCommon.TypeValidators.Boolean,
|
|
570
612
|
dataSystem: options?.dataSystemDefaults
|
|
571
|
-
? validatorOf(dataSystemValidators,
|
|
613
|
+
? validatorOf(dataSystemValidators, {
|
|
614
|
+
defaults: {
|
|
615
|
+
...options.dataSystemDefaults,
|
|
616
|
+
connectionModes: MODE_TABLE,
|
|
617
|
+
},
|
|
618
|
+
})
|
|
572
619
|
: jsSdkCommon.TypeValidators.Object,
|
|
573
620
|
};
|
|
574
621
|
}
|
|
@@ -970,6 +1017,46 @@ class EventFactory extends jsSdkCommon.internal.EventFactoryBase {
|
|
|
970
1017
|
}
|
|
971
1018
|
}
|
|
972
1019
|
|
|
1020
|
+
function readFlagsFromBootstrap(logger, data) {
|
|
1021
|
+
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
1022
|
+
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
1023
|
+
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
1024
|
+
const keys = Object.keys(data);
|
|
1025
|
+
const metadataKey = '$flagsState';
|
|
1026
|
+
const validKey = '$valid';
|
|
1027
|
+
const metadata = data[metadataKey];
|
|
1028
|
+
if (!metadata && keys.length) {
|
|
1029
|
+
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
1030
|
+
' metadata. Events may not be sent correctly.');
|
|
1031
|
+
}
|
|
1032
|
+
if (data[validKey] === false) {
|
|
1033
|
+
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
1034
|
+
}
|
|
1035
|
+
const ret = {};
|
|
1036
|
+
keys.forEach((key) => {
|
|
1037
|
+
if (key !== metadataKey && key !== validKey) {
|
|
1038
|
+
let flag;
|
|
1039
|
+
if (metadata && metadata[key]) {
|
|
1040
|
+
flag = {
|
|
1041
|
+
value: data[key],
|
|
1042
|
+
...metadata[key],
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
flag = {
|
|
1047
|
+
value: data[key],
|
|
1048
|
+
version: 0,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
ret[key] = {
|
|
1052
|
+
version: flag.version,
|
|
1053
|
+
flag,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
return ret;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
973
1060
|
/**
|
|
974
1061
|
* Suffix appended to context storage keys to form the freshness storage key.
|
|
975
1062
|
*/
|
|
@@ -1898,6 +1985,10 @@ class LDClientImpl {
|
|
|
1898
1985
|
this._eventFactoryWithReasons = new EventFactory(true);
|
|
1899
1986
|
this._eventSendingEnabled = false;
|
|
1900
1987
|
this._identifyQueue = createAsyncTaskQueue();
|
|
1988
|
+
// NOTE: this is used to ease the transition to the new initialization pattern.
|
|
1989
|
+
// All client SDKs should set this to true with the exception of React Native which is
|
|
1990
|
+
// still using the deprecated construct + identify pattern.
|
|
1991
|
+
this._requiresStart = false;
|
|
1901
1992
|
if (!sdkKey) {
|
|
1902
1993
|
throw new Error('You must configure the client with a client-side SDK key');
|
|
1903
1994
|
}
|
|
@@ -1906,6 +1997,8 @@ class LDClientImpl {
|
|
|
1906
1997
|
}
|
|
1907
1998
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1908
1999
|
this.logger = this._config.logger;
|
|
2000
|
+
this._requiresStart = internalOptions?.requiresStart ?? false;
|
|
2001
|
+
this.initialContext = internalOptions?.initialContext;
|
|
1909
2002
|
this._baseHeaders = jsSdkCommon.defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1910
2003
|
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1911
2004
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
@@ -1923,6 +2016,8 @@ class LDClientImpl {
|
|
|
1923
2016
|
});
|
|
1924
2017
|
});
|
|
1925
2018
|
this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
|
|
2019
|
+
this.isFDv2 = !!this._config.dataSystem;
|
|
2020
|
+
this.dataSystemConfig = this._config.dataSystem;
|
|
1926
2021
|
const hooks = [...this._config.hooks];
|
|
1927
2022
|
this.environmentMetadata = createPluginEnvironmentMetadata(this.sdkKey, this.platform, this._config);
|
|
1928
2023
|
this._config.getImplementationHooks(this.environmentMetadata).forEach((hook) => {
|
|
@@ -2000,6 +2095,57 @@ class LDClientImpl {
|
|
|
2000
2095
|
presetFlags(newFlags) {
|
|
2001
2096
|
this._flagManager.presetFlags(newFlags);
|
|
2002
2097
|
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Starts the client and returns a promise that resolves to the initialization result.
|
|
2100
|
+
*
|
|
2101
|
+
* This method is idempotent - calling it multiple times returns the same promise.
|
|
2102
|
+
*
|
|
2103
|
+
* @param options Optional configuration. See {@link LDStartOptions}.
|
|
2104
|
+
* @returns A promise that resolves to the initialization result.
|
|
2105
|
+
*/
|
|
2106
|
+
start(options) {
|
|
2107
|
+
if (this.initializeResult) {
|
|
2108
|
+
return Promise.resolve(this.initializeResult);
|
|
2109
|
+
}
|
|
2110
|
+
if (this.startPromise) {
|
|
2111
|
+
return this.startPromise;
|
|
2112
|
+
}
|
|
2113
|
+
if (!this.initialContext) {
|
|
2114
|
+
this.logger.error('Initial context not set');
|
|
2115
|
+
return Promise.resolve({ status: 'failed', error: new Error('Initial context not set') });
|
|
2116
|
+
}
|
|
2117
|
+
const identifyOptions = {
|
|
2118
|
+
...(options?.identifyOptions ?? {}),
|
|
2119
|
+
// Initial identify operations are not sheddable.
|
|
2120
|
+
sheddable: false,
|
|
2121
|
+
};
|
|
2122
|
+
// If the bootstrap data is provided in the start options, and the identify options do not
|
|
2123
|
+
// have bootstrap data, then use the bootstrap data from the start options.
|
|
2124
|
+
if (options?.bootstrap && !identifyOptions.bootstrap) {
|
|
2125
|
+
identifyOptions.bootstrap = options.bootstrap;
|
|
2126
|
+
}
|
|
2127
|
+
if (identifyOptions.bootstrap) {
|
|
2128
|
+
try {
|
|
2129
|
+
if (!identifyOptions.bootstrapParsed) {
|
|
2130
|
+
identifyOptions.bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap);
|
|
2131
|
+
}
|
|
2132
|
+
if (identifyOptions.bootstrapParsed) {
|
|
2133
|
+
this.presetFlags(identifyOptions.bootstrapParsed);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
catch (error) {
|
|
2137
|
+
this.logger.error('Failed to bootstrap data', error);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (!this.initializedPromise) {
|
|
2141
|
+
this.initializedPromise = new Promise((resolve) => {
|
|
2142
|
+
this.initResolve = resolve;
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
this.startPromise = this._promiseWithTimeout(this.initializedPromise, options?.timeout ?? 5, 'start');
|
|
2146
|
+
this.identifyResult(this.initialContext, identifyOptions);
|
|
2147
|
+
return this.startPromise;
|
|
2148
|
+
}
|
|
2003
2149
|
/**
|
|
2004
2150
|
* Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
|
|
2005
2151
|
*
|
|
@@ -2037,6 +2183,10 @@ class LDClientImpl {
|
|
|
2037
2183
|
// If completed or shed, then we are done.
|
|
2038
2184
|
}
|
|
2039
2185
|
async identifyResult(pristineContext, identifyOptions) {
|
|
2186
|
+
if (this._requiresStart && !this.startPromise) {
|
|
2187
|
+
this.logger.error('The client must be started before a context can be identified. Call start() prior to identifying a context.');
|
|
2188
|
+
return { status: 'error', error: new Error('Identify called before start') };
|
|
2189
|
+
}
|
|
2040
2190
|
const identifyTimeout = identifyOptions?.timeout ?? DEFAULT_IDENTIFY_TIMEOUT_SECONDS;
|
|
2041
2191
|
const noTimeout = identifyOptions?.timeout === undefined && identifyOptions?.noTimeout === true;
|
|
2042
2192
|
// When noTimeout is specified, and a timeout is not specified, then this condition cannot
|
|
@@ -2146,7 +2296,7 @@ class LDClientImpl {
|
|
|
2146
2296
|
// If waitForInitialization was previously called, then return the promise with a timeout.
|
|
2147
2297
|
// This condition should only be triggered if waitForInitialization was called multiple times.
|
|
2148
2298
|
if (this.initializedPromise) {
|
|
2149
|
-
return this.
|
|
2299
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2150
2300
|
}
|
|
2151
2301
|
// Create a new promise for tracking initialization
|
|
2152
2302
|
if (!this.initializedPromise) {
|
|
@@ -2154,22 +2304,18 @@ class LDClientImpl {
|
|
|
2154
2304
|
this.initResolve = resolve;
|
|
2155
2305
|
});
|
|
2156
2306
|
}
|
|
2157
|
-
return this.
|
|
2307
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2158
2308
|
}
|
|
2159
2309
|
/**
|
|
2160
|
-
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2310
|
+
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2311
|
+
* and start.
|
|
2161
2312
|
*
|
|
2162
2313
|
* @param basePromise The promise to race against a timeout.
|
|
2163
2314
|
* @param timeout The timeout in seconds.
|
|
2164
2315
|
* @returns A promise that resolves to the initialization result or timeout.
|
|
2165
|
-
*
|
|
2166
|
-
* @privateRemarks
|
|
2167
|
-
* This method is protected because it is used by the browser SDK's `start` method.
|
|
2168
|
-
* Eventually, the start method will be moved to this common implementation and this method will
|
|
2169
|
-
* be made private.
|
|
2170
2316
|
*/
|
|
2171
|
-
|
|
2172
|
-
const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout,
|
|
2317
|
+
_promiseWithTimeout(basePromise, timeout, label = 'waitForInitialization') {
|
|
2318
|
+
const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout, label);
|
|
2173
2319
|
return Promise.race([
|
|
2174
2320
|
basePromise.then((res) => {
|
|
2175
2321
|
cancelableTimeout.cancel();
|
|
@@ -2399,46 +2545,6 @@ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
|
|
|
2399
2545
|
});
|
|
2400
2546
|
}
|
|
2401
2547
|
|
|
2402
|
-
function readFlagsFromBootstrap(logger, data) {
|
|
2403
|
-
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
2404
|
-
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
2405
|
-
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
2406
|
-
const keys = Object.keys(data);
|
|
2407
|
-
const metadataKey = '$flagsState';
|
|
2408
|
-
const validKey = '$valid';
|
|
2409
|
-
const metadata = data[metadataKey];
|
|
2410
|
-
if (!metadata && keys.length) {
|
|
2411
|
-
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
2412
|
-
' metadata. Events may not be sent correctly.');
|
|
2413
|
-
}
|
|
2414
|
-
if (data[validKey] === false) {
|
|
2415
|
-
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
2416
|
-
}
|
|
2417
|
-
const ret = {};
|
|
2418
|
-
keys.forEach((key) => {
|
|
2419
|
-
if (key !== metadataKey && key !== validKey) {
|
|
2420
|
-
let flag;
|
|
2421
|
-
if (metadata && metadata[key]) {
|
|
2422
|
-
flag = {
|
|
2423
|
-
value: data[key],
|
|
2424
|
-
...metadata[key],
|
|
2425
|
-
};
|
|
2426
|
-
}
|
|
2427
|
-
else {
|
|
2428
|
-
flag = {
|
|
2429
|
-
value: data[key],
|
|
2430
|
-
version: 0,
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
ret[key] = {
|
|
2434
|
-
version: flag.version,
|
|
2435
|
-
flag,
|
|
2436
|
-
};
|
|
2437
|
-
}
|
|
2438
|
-
});
|
|
2439
|
-
return ret;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
2548
|
/**
|
|
2443
2549
|
* Creates endpoint paths for browser (client-side ID) FDv1 evaluation.
|
|
2444
2550
|
*
|
|
@@ -3161,12 +3267,12 @@ async function loadFromCache(config) {
|
|
|
3161
3267
|
const { storage, crypto, environmentNamespace, context, logger } = config;
|
|
3162
3268
|
if (!storage) {
|
|
3163
3269
|
logger?.debug('No storage available for cache initializer');
|
|
3164
|
-
return
|
|
3270
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3165
3271
|
}
|
|
3166
3272
|
const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
|
|
3167
3273
|
if (!cached) {
|
|
3168
3274
|
logger?.debug('Cache miss for context');
|
|
3169
|
-
return
|
|
3275
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3170
3276
|
}
|
|
3171
3277
|
const updates = Object.entries(cached.flags).map(([key, flag]) => ({
|
|
3172
3278
|
kind: 'flag-eval',
|
|
@@ -3175,7 +3281,6 @@ async function loadFromCache(config) {
|
|
|
3175
3281
|
object: flagToEvaluationResult(flag),
|
|
3176
3282
|
}));
|
|
3177
3283
|
const payload = {
|
|
3178
|
-
id: 'cache',
|
|
3179
3284
|
version: 0,
|
|
3180
3285
|
// No `state` field. The orchestrator sees a changeSet without a selector,
|
|
3181
3286
|
// records dataReceived=true, and continues to the next initializer.
|
|
@@ -3279,6 +3384,51 @@ function makeFDv2Requestor(plainContextString, serviceEndpoints, paths, requests
|
|
|
3279
3384
|
function processFlagEval(object) {
|
|
3280
3385
|
return object;
|
|
3281
3386
|
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Converts an FDv2 {@link internal.Update} with `kind: 'flag-eval'` into an
|
|
3389
|
+
* {@link ItemDescriptor} suitable for {@link FlagManager}.
|
|
3390
|
+
*
|
|
3391
|
+
* For put updates the envelope `version` is used as the {@link ItemDescriptor.version}
|
|
3392
|
+
* and as {@link Flag.version}. The rest of the fields are spread from the
|
|
3393
|
+
* {@link FlagEvaluationResult} object.
|
|
3394
|
+
*
|
|
3395
|
+
* For delete updates a tombstone descriptor is created with `deleted: true`.
|
|
3396
|
+
*/
|
|
3397
|
+
function flagEvalUpdateToItemDescriptor(update) {
|
|
3398
|
+
if (update.deleted) {
|
|
3399
|
+
return {
|
|
3400
|
+
version: update.version,
|
|
3401
|
+
flag: {
|
|
3402
|
+
version: update.version,
|
|
3403
|
+
deleted: true,
|
|
3404
|
+
value: undefined,
|
|
3405
|
+
trackEvents: false,
|
|
3406
|
+
},
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
const evalResult = update.object;
|
|
3410
|
+
return {
|
|
3411
|
+
version: update.version,
|
|
3412
|
+
flag: {
|
|
3413
|
+
...evalResult,
|
|
3414
|
+
version: update.version,
|
|
3415
|
+
},
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
/**
|
|
3419
|
+
* Converts an array of FDv2 payload updates into a map of flag key to
|
|
3420
|
+
* {@link ItemDescriptor}. Only `flag-eval` kind updates are processed;
|
|
3421
|
+
* unrecognized kinds are silently ignored.
|
|
3422
|
+
*/
|
|
3423
|
+
function flagEvalPayloadToItemDescriptors(updates) {
|
|
3424
|
+
const descriptors = {};
|
|
3425
|
+
updates.forEach((update) => {
|
|
3426
|
+
if (update.kind === 'flag-eval') {
|
|
3427
|
+
descriptors[update.key] = flagEvalUpdateToItemDescriptor(update);
|
|
3428
|
+
}
|
|
3429
|
+
});
|
|
3430
|
+
return descriptors;
|
|
3431
|
+
}
|
|
3282
3432
|
|
|
3283
3433
|
function getFallback(headers) {
|
|
3284
3434
|
const value = headers.get('x-ld-fd-fallback');
|
|
@@ -3295,7 +3445,7 @@ function getEnvironmentId(headers) {
|
|
|
3295
3445
|
* it only forwards payloads and actionable errors. For polling results,
|
|
3296
3446
|
* we need full control over all protocol action types.
|
|
3297
3447
|
*/
|
|
3298
|
-
function processEvents(events,
|
|
3448
|
+
function processEvents(events, fdv1Fallback, environmentId, logger) {
|
|
3299
3449
|
const handler = jsSdkCommon.internal.createProtocolHandler({
|
|
3300
3450
|
'flag-eval': processFlagEval,
|
|
3301
3451
|
}, logger);
|
|
@@ -3315,9 +3465,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3315
3465
|
case 'serverError': {
|
|
3316
3466
|
const errorInfo = errorInfoFromUnknown(action.reason);
|
|
3317
3467
|
logger?.error(`Server error during polling: ${action.reason}`);
|
|
3318
|
-
earlyResult =
|
|
3319
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3320
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3468
|
+
earlyResult = interrupted(errorInfo, fdv1Fallback);
|
|
3321
3469
|
break;
|
|
3322
3470
|
}
|
|
3323
3471
|
case 'error': {
|
|
@@ -3325,9 +3473,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3325
3473
|
if (action.kind === 'MISSING_PAYLOAD' || action.kind === 'PROTOCOL_ERROR') {
|
|
3326
3474
|
const errorInfo = errorInfoFromInvalidData(action.message);
|
|
3327
3475
|
logger?.warn(`Protocol error during polling: ${action.message}`);
|
|
3328
|
-
earlyResult =
|
|
3329
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3330
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3476
|
+
earlyResult = interrupted(errorInfo, fdv1Fallback);
|
|
3331
3477
|
}
|
|
3332
3478
|
else {
|
|
3333
3479
|
// Non-actionable errors (UNKNOWN_EVENT) are logged but don't stop processing
|
|
@@ -3343,19 +3489,18 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3343
3489
|
// Events didn't produce a result
|
|
3344
3490
|
const errorInfo = errorInfoFromUnknown('Unexpected end of polling response');
|
|
3345
3491
|
logger?.error('Unexpected end of polling response');
|
|
3346
|
-
return
|
|
3492
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3347
3493
|
}
|
|
3348
3494
|
/**
|
|
3349
3495
|
* Performs a single FDv2 poll request, processes the protocol response, and
|
|
3350
3496
|
* returns an {@link FDv2SourceResult}.
|
|
3351
3497
|
*
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
* produce interrupted results.
|
|
3498
|
+
* Recoverable errors produce interrupted results; unrecoverable HTTP errors
|
|
3499
|
+
* produce terminal errors.
|
|
3355
3500
|
*
|
|
3356
3501
|
* @internal
|
|
3357
3502
|
*/
|
|
3358
|
-
async function poll(requestor, basis,
|
|
3503
|
+
async function poll(requestor, basis, logger) {
|
|
3359
3504
|
let fdv1Fallback = false;
|
|
3360
3505
|
let environmentId;
|
|
3361
3506
|
try {
|
|
@@ -3366,7 +3511,6 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3366
3511
|
// (Spec Requirement 10.1.2)
|
|
3367
3512
|
if (response.status === 304) {
|
|
3368
3513
|
const nonePayload = {
|
|
3369
|
-
id: '',
|
|
3370
3514
|
version: 0,
|
|
3371
3515
|
type: 'none',
|
|
3372
3516
|
updates: [],
|
|
@@ -3377,9 +3521,6 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3377
3521
|
if (response.status < 200 || response.status >= 300) {
|
|
3378
3522
|
const errorInfo = errorInfoFromHttpError(response.status);
|
|
3379
3523
|
logger?.error(`Polling request failed with HTTP error: ${response.status}`);
|
|
3380
|
-
if (oneShot) {
|
|
3381
|
-
return terminalError(errorInfo, fdv1Fallback);
|
|
3382
|
-
}
|
|
3383
3524
|
const recoverable = response.status <= 0 || jsSdkCommon.isHttpRecoverable(response.status);
|
|
3384
3525
|
return recoverable
|
|
3385
3526
|
? interrupted(errorInfo, fdv1Fallback)
|
|
@@ -3389,9 +3530,7 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3389
3530
|
if (!response.body) {
|
|
3390
3531
|
const errorInfo = errorInfoFromInvalidData('Empty response body');
|
|
3391
3532
|
logger?.error('Polling request received empty response body');
|
|
3392
|
-
return
|
|
3393
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3394
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3533
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3395
3534
|
}
|
|
3396
3535
|
let parsed;
|
|
3397
3536
|
try {
|
|
@@ -3400,34 +3539,36 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3400
3539
|
catch {
|
|
3401
3540
|
const errorInfo = errorInfoFromInvalidData('Malformed JSON data in polling response');
|
|
3402
3541
|
logger?.error('Polling request received malformed data');
|
|
3403
|
-
return
|
|
3404
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3405
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3542
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3406
3543
|
}
|
|
3407
3544
|
if (!Array.isArray(parsed.events)) {
|
|
3408
3545
|
const errorInfo = errorInfoFromInvalidData('Invalid polling response: missing or invalid events array');
|
|
3409
3546
|
logger?.error('Polling response does not contain a valid events array');
|
|
3410
|
-
return
|
|
3411
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3412
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3547
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3413
3548
|
}
|
|
3414
|
-
return processEvents(parsed.events,
|
|
3549
|
+
return processEvents(parsed.events, fdv1Fallback, environmentId, logger);
|
|
3415
3550
|
}
|
|
3416
3551
|
catch (err) {
|
|
3417
3552
|
// Network or other I/O error from the fetch itself
|
|
3418
3553
|
const message = err?.message ?? String(err);
|
|
3419
3554
|
logger?.error(`Polling request failed with network error: ${message}`);
|
|
3420
3555
|
const errorInfo = errorInfoFromNetworkError(message);
|
|
3421
|
-
return
|
|
3556
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3422
3557
|
}
|
|
3423
3558
|
}
|
|
3424
3559
|
|
|
3560
|
+
const SHUTDOWN = Symbol('shutdown');
|
|
3425
3561
|
/**
|
|
3426
|
-
* Creates a
|
|
3427
|
-
*
|
|
3562
|
+
* Creates a polling initializer that performs an FDv2 poll request with
|
|
3563
|
+
* retry logic. Retries up to 3 times on recoverable errors with a 1-second
|
|
3564
|
+
* delay between attempts.
|
|
3428
3565
|
*
|
|
3429
|
-
*
|
|
3430
|
-
*
|
|
3566
|
+
* Unrecoverable errors (401, 403, etc.) are returned immediately as terminal
|
|
3567
|
+
* errors. After exhausting retries on recoverable errors, the result is
|
|
3568
|
+
* converted to a terminal error.
|
|
3569
|
+
*
|
|
3570
|
+
* If `close()` is called during a poll or retry delay, the result will be
|
|
3571
|
+
* a shutdown status.
|
|
3431
3572
|
*
|
|
3432
3573
|
* @internal
|
|
3433
3574
|
*/
|
|
@@ -3438,12 +3579,40 @@ function createPollingInitializer(requestor, logger, selectorGetter) {
|
|
|
3438
3579
|
});
|
|
3439
3580
|
return {
|
|
3440
3581
|
async run() {
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3443
|
-
|
|
3582
|
+
const maxRetries = 3;
|
|
3583
|
+
const retryDelayMs = 1000;
|
|
3584
|
+
const selector = selectorGetter();
|
|
3585
|
+
let lastResult;
|
|
3586
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
3587
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3588
|
+
const result = await Promise.race([shutdownPromise, poll(requestor, selector, logger)]);
|
|
3589
|
+
if (result === SHUTDOWN) {
|
|
3590
|
+
return shutdown();
|
|
3591
|
+
}
|
|
3592
|
+
if (result.type === 'changeSet') {
|
|
3593
|
+
return result;
|
|
3594
|
+
}
|
|
3595
|
+
// Non-retryable status (terminal_error, goodbye) -> return immediately
|
|
3596
|
+
if (result.state !== 'interrupted') {
|
|
3597
|
+
return result;
|
|
3598
|
+
}
|
|
3599
|
+
// Recoverable error — save and potentially retry
|
|
3600
|
+
lastResult = result;
|
|
3601
|
+
if (attempt < maxRetries) {
|
|
3602
|
+
logger?.warn(`Recoverable polling error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${retryDelayMs}ms...`);
|
|
3603
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3604
|
+
const sleepResult = await Promise.race([shutdownPromise, jsSdkCommon.sleep(retryDelayMs)]);
|
|
3605
|
+
if (sleepResult === SHUTDOWN) {
|
|
3606
|
+
return shutdown();
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
// Convert final interrupted -> terminal_error
|
|
3611
|
+
const status = lastResult;
|
|
3612
|
+
return terminalError(status.errorInfo, status.fdv1Fallback);
|
|
3444
3613
|
},
|
|
3445
3614
|
close() {
|
|
3446
|
-
shutdownResolve?.(
|
|
3615
|
+
shutdownResolve?.(SHUTDOWN);
|
|
3447
3616
|
shutdownResolve = undefined;
|
|
3448
3617
|
},
|
|
3449
3618
|
};
|
|
@@ -3506,7 +3675,7 @@ function createPollingSynchronizer(requestor, logger, selectorGetter, pollInterv
|
|
|
3506
3675
|
}
|
|
3507
3676
|
const startTime = Date.now();
|
|
3508
3677
|
try {
|
|
3509
|
-
const result = await poll(requestor, selectorGetter(),
|
|
3678
|
+
const result = await poll(requestor, selectorGetter(), logger);
|
|
3510
3679
|
if (stopped) {
|
|
3511
3680
|
return;
|
|
3512
3681
|
}
|
|
@@ -3583,6 +3752,100 @@ function createSynchronizerSlot(factory, options) {
|
|
|
3583
3752
|
const state = options?.initialState ?? (isFDv1Fallback ? 'blocked' : 'available');
|
|
3584
3753
|
return { factory, isFDv1Fallback, state };
|
|
3585
3754
|
}
|
|
3755
|
+
/**
|
|
3756
|
+
* Creates a {@link SourceManager} that coordinates initializer and
|
|
3757
|
+
* synchronizer lifecycle.
|
|
3758
|
+
*
|
|
3759
|
+
* @param initializerFactories Ordered list of initializer factories.
|
|
3760
|
+
* @param synchronizerSlots Ordered list of synchronizer slots with state.
|
|
3761
|
+
* @param selectorGetter Closure that returns the current selector string.
|
|
3762
|
+
*/
|
|
3763
|
+
function createSourceManager(initializerFactories, synchronizerSlots, selectorGetter) {
|
|
3764
|
+
let activeSource;
|
|
3765
|
+
let initializerIndex = -1;
|
|
3766
|
+
let synchronizerIndex = -1;
|
|
3767
|
+
let isShutdown = false;
|
|
3768
|
+
function closeActiveSource() {
|
|
3769
|
+
if (activeSource) {
|
|
3770
|
+
activeSource.close();
|
|
3771
|
+
activeSource = undefined;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
function findFirstAvailableIndex() {
|
|
3775
|
+
return synchronizerSlots.findIndex((slot) => slot.state === 'available');
|
|
3776
|
+
}
|
|
3777
|
+
return {
|
|
3778
|
+
get isShutdown() {
|
|
3779
|
+
return isShutdown;
|
|
3780
|
+
},
|
|
3781
|
+
getNextInitializerAndSetActive() {
|
|
3782
|
+
if (isShutdown) {
|
|
3783
|
+
return undefined;
|
|
3784
|
+
}
|
|
3785
|
+
initializerIndex += 1;
|
|
3786
|
+
if (initializerIndex >= initializerFactories.length) {
|
|
3787
|
+
return undefined;
|
|
3788
|
+
}
|
|
3789
|
+
closeActiveSource();
|
|
3790
|
+
const initializer = initializerFactories[initializerIndex](selectorGetter);
|
|
3791
|
+
activeSource = initializer;
|
|
3792
|
+
return initializer;
|
|
3793
|
+
},
|
|
3794
|
+
getNextAvailableSynchronizerAndSetActive() {
|
|
3795
|
+
if (isShutdown || synchronizerSlots.length === 0) {
|
|
3796
|
+
return undefined;
|
|
3797
|
+
}
|
|
3798
|
+
// Scan all slots starting from the position after the current one,
|
|
3799
|
+
// wrapping around to the beginning if needed. This matches the Java
|
|
3800
|
+
// SourceManager behavior where synchronizers cycle rather than exhausting.
|
|
3801
|
+
let visited = 0;
|
|
3802
|
+
while (visited < synchronizerSlots.length) {
|
|
3803
|
+
synchronizerIndex += 1;
|
|
3804
|
+
if (synchronizerIndex >= synchronizerSlots.length) {
|
|
3805
|
+
synchronizerIndex = 0;
|
|
3806
|
+
}
|
|
3807
|
+
const candidate = synchronizerSlots[synchronizerIndex];
|
|
3808
|
+
if (candidate.state === 'available') {
|
|
3809
|
+
closeActiveSource();
|
|
3810
|
+
const synchronizer = candidate.factory(selectorGetter);
|
|
3811
|
+
activeSource = synchronizer;
|
|
3812
|
+
return synchronizer;
|
|
3813
|
+
}
|
|
3814
|
+
visited += 1;
|
|
3815
|
+
}
|
|
3816
|
+
return undefined;
|
|
3817
|
+
},
|
|
3818
|
+
blockCurrentSynchronizer() {
|
|
3819
|
+
if (synchronizerIndex >= 0 && synchronizerIndex < synchronizerSlots.length) {
|
|
3820
|
+
// eslint-disable-next-line no-param-reassign
|
|
3821
|
+
synchronizerSlots[synchronizerIndex].state = 'blocked';
|
|
3822
|
+
}
|
|
3823
|
+
},
|
|
3824
|
+
resetSourceIndex() {
|
|
3825
|
+
synchronizerIndex = -1;
|
|
3826
|
+
},
|
|
3827
|
+
fdv1Fallback() {
|
|
3828
|
+
synchronizerSlots.forEach((slot) => {
|
|
3829
|
+
// eslint-disable-next-line no-param-reassign
|
|
3830
|
+
slot.state = slot.isFDv1Fallback ? 'available' : 'blocked';
|
|
3831
|
+
});
|
|
3832
|
+
synchronizerIndex = -1;
|
|
3833
|
+
},
|
|
3834
|
+
isPrimeSynchronizer() {
|
|
3835
|
+
return synchronizerIndex === findFirstAvailableIndex();
|
|
3836
|
+
},
|
|
3837
|
+
getAvailableSynchronizerCount() {
|
|
3838
|
+
return synchronizerSlots.filter((slot) => slot.state === 'available').length;
|
|
3839
|
+
},
|
|
3840
|
+
hasFDv1Fallback() {
|
|
3841
|
+
return synchronizerSlots.some((slot) => slot.isFDv1Fallback);
|
|
3842
|
+
},
|
|
3843
|
+
close() {
|
|
3844
|
+
isShutdown = true;
|
|
3845
|
+
closeActiveSource();
|
|
3846
|
+
},
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3586
3849
|
|
|
3587
3850
|
/**
|
|
3588
3851
|
* FDv2 event names to listen for on the EventSource. This must stay in sync
|
|
@@ -3746,6 +4009,7 @@ function createStreamingBase(config) {
|
|
|
3746
4009
|
initialRetryDelayMillis: config.initialRetryDelayMillis,
|
|
3747
4010
|
readTimeoutMillis: 5 * 60 * 1000,
|
|
3748
4011
|
retryResetIntervalMillis: 60 * 1000,
|
|
4012
|
+
urlBuilder: buildStreamUri,
|
|
3749
4013
|
});
|
|
3750
4014
|
eventSource = es;
|
|
3751
4015
|
attachFDv2Listeners(es);
|
|
@@ -3877,7 +4141,7 @@ function createStreamingSynchronizer(base) {
|
|
|
3877
4141
|
|
|
3878
4142
|
function createPingHandler(requestor, selectorGetter, logger) {
|
|
3879
4143
|
return {
|
|
3880
|
-
handlePing: () => poll(requestor, selectorGetter(),
|
|
4144
|
+
handlePing: () => poll(requestor, selectorGetter(), logger),
|
|
3881
4145
|
};
|
|
3882
4146
|
}
|
|
3883
4147
|
/**
|
|
@@ -3968,6 +4232,930 @@ function createDefaultSourceFactoryProvider() {
|
|
|
3968
4232
|
};
|
|
3969
4233
|
}
|
|
3970
4234
|
|
|
4235
|
+
function flagsToPayload(flags) {
|
|
4236
|
+
const updates = Object.entries(flags).map(([key, flag]) => ({
|
|
4237
|
+
kind: 'flag',
|
|
4238
|
+
key,
|
|
4239
|
+
version: flag.version ?? 1,
|
|
4240
|
+
object: flag,
|
|
4241
|
+
}));
|
|
4242
|
+
return {
|
|
4243
|
+
version: 1,
|
|
4244
|
+
type: 'full',
|
|
4245
|
+
updates,
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Creates a polling synchronizer that polls an FDv1 endpoint and produces
|
|
4250
|
+
* results in the FDv2 {@link Synchronizer} pull model.
|
|
4251
|
+
*
|
|
4252
|
+
* This is a standalone implementation (not a wrapper around the existing FDv1
|
|
4253
|
+
* `PollingProcessor`) because the FDv1 fallback is temporary and will be
|
|
4254
|
+
* removed in the next major version. A focused implementation is simpler than
|
|
4255
|
+
* an adapter layer.
|
|
4256
|
+
*
|
|
4257
|
+
* Polling starts lazily on the first call to `next()`. Each poll returns the
|
|
4258
|
+
* complete flag set as a `changeSet` result with `type: 'full'` and an empty
|
|
4259
|
+
* selector.
|
|
4260
|
+
*
|
|
4261
|
+
* @param requestor FDv1 requestor configured with the appropriate endpoint.
|
|
4262
|
+
* @param pollIntervalMs Interval between polls in milliseconds.
|
|
4263
|
+
* @param logger Optional logger.
|
|
4264
|
+
* @internal
|
|
4265
|
+
*/
|
|
4266
|
+
function createFDv1PollingSynchronizer(requestor, pollIntervalMs, logger) {
|
|
4267
|
+
const resultQueue = createAsyncQueue();
|
|
4268
|
+
let shutdownResolve;
|
|
4269
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
4270
|
+
shutdownResolve = resolve;
|
|
4271
|
+
});
|
|
4272
|
+
let timeoutHandle;
|
|
4273
|
+
let stopped = false;
|
|
4274
|
+
let started = false;
|
|
4275
|
+
function scheduleNextPoll(startTime) {
|
|
4276
|
+
if (!stopped) {
|
|
4277
|
+
const elapsed = Date.now() - startTime;
|
|
4278
|
+
const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
|
|
4279
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4280
|
+
timeoutHandle = setTimeout(doPoll, sleepFor);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
async function doPoll() {
|
|
4284
|
+
if (stopped) {
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
logger?.debug('Polling FDv1 endpoint for feature flag updates');
|
|
4288
|
+
const startTime = Date.now();
|
|
4289
|
+
try {
|
|
4290
|
+
const body = await requestor.requestPayload();
|
|
4291
|
+
if (stopped) {
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
let payload;
|
|
4295
|
+
try {
|
|
4296
|
+
const flags = JSON.parse(body);
|
|
4297
|
+
payload = flagsToPayload(flags);
|
|
4298
|
+
}
|
|
4299
|
+
catch {
|
|
4300
|
+
logger?.error('FDv1 polling received malformed data');
|
|
4301
|
+
resultQueue.put({
|
|
4302
|
+
type: 'status',
|
|
4303
|
+
state: 'interrupted',
|
|
4304
|
+
errorInfo: errorInfoFromInvalidData('Malformed data in FDv1 polling response'),
|
|
4305
|
+
fdv1Fallback: false,
|
|
4306
|
+
});
|
|
4307
|
+
scheduleNextPoll(startTime);
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
resultQueue.put(changeSet(payload, false));
|
|
4311
|
+
}
|
|
4312
|
+
catch (err) {
|
|
4313
|
+
if (stopped) {
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
const requestError = err;
|
|
4317
|
+
if (requestError.status !== undefined) {
|
|
4318
|
+
if (!jsSdkCommon.isHttpRecoverable(requestError.status)) {
|
|
4319
|
+
logger?.error(jsSdkCommon.httpErrorMessage(err, 'FDv1 polling request'));
|
|
4320
|
+
stopped = true;
|
|
4321
|
+
shutdownResolve?.(terminalError(errorInfoFromHttpError(requestError.status), false));
|
|
4322
|
+
shutdownResolve = undefined;
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
logger?.warn(jsSdkCommon.httpErrorMessage(err, 'FDv1 polling request', 'will retry'));
|
|
4327
|
+
resultQueue.put({
|
|
4328
|
+
type: 'status',
|
|
4329
|
+
state: 'interrupted',
|
|
4330
|
+
errorInfo: requestError.status
|
|
4331
|
+
? errorInfoFromHttpError(requestError.status)
|
|
4332
|
+
: errorInfoFromNetworkError(requestError.message),
|
|
4333
|
+
fdv1Fallback: false,
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
scheduleNextPoll(startTime);
|
|
4337
|
+
}
|
|
4338
|
+
return {
|
|
4339
|
+
next() {
|
|
4340
|
+
if (!started) {
|
|
4341
|
+
started = true;
|
|
4342
|
+
doPoll();
|
|
4343
|
+
}
|
|
4344
|
+
return Promise.race([shutdownPromise, resultQueue.take()]);
|
|
4345
|
+
},
|
|
4346
|
+
close() {
|
|
4347
|
+
stopped = true;
|
|
4348
|
+
if (timeoutHandle !== undefined) {
|
|
4349
|
+
clearTimeout(timeoutHandle);
|
|
4350
|
+
timeoutHandle = undefined;
|
|
4351
|
+
}
|
|
4352
|
+
shutdownResolve?.(shutdown());
|
|
4353
|
+
shutdownResolve = undefined;
|
|
4354
|
+
},
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
const DEFAULT_FALLBACK_TIMEOUT_MS = 2 * 60 * 1000; // 120 seconds
|
|
4359
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 5 * 60 * 1000; // 300 seconds
|
|
4360
|
+
/**
|
|
4361
|
+
* Creates a cancelable timer that resolves with the given {@link ConditionType}
|
|
4362
|
+
* when it fires. Wraps {@link cancelableTimedPromise} to convert its
|
|
4363
|
+
* reject-on-timeout semantics into resolve-with-type semantics.
|
|
4364
|
+
*/
|
|
4365
|
+
function conditionTimer(timeoutMs, type, taskName) {
|
|
4366
|
+
const timed = jsSdkCommon.cancelableTimedPromise(timeoutMs / 1000, taskName);
|
|
4367
|
+
return {
|
|
4368
|
+
promise: timed.promise.then(() => new Promise(() => { }), // cancelled — never settle
|
|
4369
|
+
() => type),
|
|
4370
|
+
cancel: timed.cancel,
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
/**
|
|
4374
|
+
* Creates a timed {@link Condition}.
|
|
4375
|
+
*
|
|
4376
|
+
* @param timeoutMs Time in milliseconds before the condition fires.
|
|
4377
|
+
* @param type The {@link ConditionType} to resolve with when the timer fires.
|
|
4378
|
+
* @param informHandler Optional callback invoked on each `inform()` call.
|
|
4379
|
+
* When omitted, the timer starts immediately and `inform()` is a no-op.
|
|
4380
|
+
* When provided, the timer must be started explicitly via `controls.start()`.
|
|
4381
|
+
*/
|
|
4382
|
+
function createCondition(timeoutMs, type, informHandler) {
|
|
4383
|
+
let resolve;
|
|
4384
|
+
let timer;
|
|
4385
|
+
let closed = false;
|
|
4386
|
+
const promise = new Promise((res) => {
|
|
4387
|
+
resolve = res;
|
|
4388
|
+
});
|
|
4389
|
+
function startTimer() {
|
|
4390
|
+
if (!timer && !closed) {
|
|
4391
|
+
timer = conditionTimer(timeoutMs, type, `${type} condition`);
|
|
4392
|
+
timer.promise.then((t) => {
|
|
4393
|
+
timer = undefined;
|
|
4394
|
+
resolve?.(t);
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
function cancelTimer() {
|
|
4399
|
+
timer?.cancel();
|
|
4400
|
+
timer = undefined;
|
|
4401
|
+
}
|
|
4402
|
+
// No inform handler — start immediately (recovery behavior).
|
|
4403
|
+
if (!informHandler) {
|
|
4404
|
+
startTimer();
|
|
4405
|
+
}
|
|
4406
|
+
return {
|
|
4407
|
+
promise,
|
|
4408
|
+
inform(result) {
|
|
4409
|
+
if (closed) {
|
|
4410
|
+
return;
|
|
4411
|
+
}
|
|
4412
|
+
informHandler?.(result, { start: startTimer, cancel: cancelTimer });
|
|
4413
|
+
},
|
|
4414
|
+
close() {
|
|
4415
|
+
closed = true;
|
|
4416
|
+
cancelTimer();
|
|
4417
|
+
},
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Creates a fallback condition. The condition starts a timer when an
|
|
4422
|
+
* `interrupted` status is received and cancels it when a `changeSet` is
|
|
4423
|
+
* received. If the timer fires, the condition resolves with `'fallback'`.
|
|
4424
|
+
*/
|
|
4425
|
+
function createFallbackCondition(timeoutMs) {
|
|
4426
|
+
return createCondition(timeoutMs, 'fallback', (result, { start, cancel }) => {
|
|
4427
|
+
if (result.type === 'changeSet') {
|
|
4428
|
+
cancel();
|
|
4429
|
+
}
|
|
4430
|
+
else if (result.type === 'status' && result.state === 'interrupted') {
|
|
4431
|
+
start();
|
|
4432
|
+
}
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* Creates a recovery condition. The condition starts a timer immediately
|
|
4437
|
+
* and resolves with `'recovery'` when it fires. It ignores all `inform()`
|
|
4438
|
+
* calls.
|
|
4439
|
+
*/
|
|
4440
|
+
function createRecoveryCondition(timeoutMs) {
|
|
4441
|
+
return createCondition(timeoutMs, 'recovery');
|
|
4442
|
+
}
|
|
4443
|
+
/**
|
|
4444
|
+
* Creates a group of conditions that are managed together.
|
|
4445
|
+
*
|
|
4446
|
+
* @param conditions The conditions to group.
|
|
4447
|
+
*/
|
|
4448
|
+
function createConditionGroup(conditions) {
|
|
4449
|
+
return {
|
|
4450
|
+
promise: conditions.length === 0 ? undefined : Promise.race(conditions.map((c) => c.promise)),
|
|
4451
|
+
inform(result) {
|
|
4452
|
+
conditions.forEach((condition) => condition.inform(result));
|
|
4453
|
+
},
|
|
4454
|
+
close() {
|
|
4455
|
+
conditions.forEach((condition) => condition.close());
|
|
4456
|
+
},
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Determines which conditions to create based on the synchronizer's position
|
|
4461
|
+
* and availability.
|
|
4462
|
+
*
|
|
4463
|
+
* - If there is only one available synchronizer, no conditions are needed
|
|
4464
|
+
* (there is nowhere to fall back to).
|
|
4465
|
+
* - If the current synchronizer is the primary (first available), only a
|
|
4466
|
+
* fallback condition is created.
|
|
4467
|
+
* - If the current synchronizer is non-primary, both fallback and recovery
|
|
4468
|
+
* conditions are created.
|
|
4469
|
+
*
|
|
4470
|
+
* @param availableSyncCount Number of available (non-blocked) synchronizers.
|
|
4471
|
+
* @param isPrime Whether the current synchronizer is the primary.
|
|
4472
|
+
* @param fallbackTimeoutMs Fallback condition timeout.
|
|
4473
|
+
* @param recoveryTimeoutMs Recovery condition timeout.
|
|
4474
|
+
*/
|
|
4475
|
+
function getConditions(availableSyncCount, isPrime, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS) {
|
|
4476
|
+
if (availableSyncCount <= 1) {
|
|
4477
|
+
return createConditionGroup([]);
|
|
4478
|
+
}
|
|
4479
|
+
if (isPrime) {
|
|
4480
|
+
return createConditionGroup([createFallbackCondition(fallbackTimeoutMs)]);
|
|
4481
|
+
}
|
|
4482
|
+
return createConditionGroup([
|
|
4483
|
+
createFallbackCondition(fallbackTimeoutMs),
|
|
4484
|
+
createRecoveryCondition(recoveryTimeoutMs),
|
|
4485
|
+
]);
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
/**
|
|
4489
|
+
* Creates an {@link FDv2DataSource} orchestrator.
|
|
4490
|
+
*/
|
|
4491
|
+
function createFDv2DataSource(config) {
|
|
4492
|
+
const { initializerFactories, synchronizerSlots, dataCallback, statusManager, selectorGetter, logger, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS, } = config;
|
|
4493
|
+
let initialized = false;
|
|
4494
|
+
let closed = false;
|
|
4495
|
+
let dataReceived = false;
|
|
4496
|
+
let initResolve;
|
|
4497
|
+
let initReject;
|
|
4498
|
+
const sourceManager = createSourceManager(initializerFactories, synchronizerSlots, selectorGetter);
|
|
4499
|
+
function markInitialized() {
|
|
4500
|
+
if (!initialized) {
|
|
4501
|
+
initialized = true;
|
|
4502
|
+
initResolve?.();
|
|
4503
|
+
initResolve = undefined;
|
|
4504
|
+
initReject = undefined;
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
function applyChangeSet(result) {
|
|
4508
|
+
dataCallback(result.payload);
|
|
4509
|
+
statusManager.requestStateUpdate('VALID');
|
|
4510
|
+
}
|
|
4511
|
+
function reportStatusError(result) {
|
|
4512
|
+
if (result.errorInfo) {
|
|
4513
|
+
statusManager.reportError(result.errorInfo.kind, result.errorInfo.message, result.errorInfo.statusCode, result.state === 'interrupted');
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
function handleFdv1Fallback(result) {
|
|
4517
|
+
if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) {
|
|
4518
|
+
sourceManager.fdv1Fallback();
|
|
4519
|
+
return true;
|
|
4520
|
+
}
|
|
4521
|
+
return false;
|
|
4522
|
+
}
|
|
4523
|
+
/* eslint-disable no-await-in-loop */
|
|
4524
|
+
// The orchestration loops intentionally use await-in-loop for sequential
|
|
4525
|
+
// state machine processing — one result at a time.
|
|
4526
|
+
async function runInitializers() {
|
|
4527
|
+
while (!closed) {
|
|
4528
|
+
const initializer = sourceManager.getNextInitializerAndSetActive();
|
|
4529
|
+
if (initializer === undefined) {
|
|
4530
|
+
break;
|
|
4531
|
+
}
|
|
4532
|
+
const result = await initializer.run();
|
|
4533
|
+
if (closed) {
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4536
|
+
if (result.type === 'changeSet' && result.payload.type !== 'none') {
|
|
4537
|
+
applyChangeSet(result);
|
|
4538
|
+
if (handleFdv1Fallback(result)) {
|
|
4539
|
+
// FDv1 fallback triggered during initialization -- data was received
|
|
4540
|
+
// but we should move to synchronizers where the FDv1 adapter will run.
|
|
4541
|
+
dataReceived = true;
|
|
4542
|
+
break;
|
|
4543
|
+
}
|
|
4544
|
+
if (result.payload.state) {
|
|
4545
|
+
// Got basis data with a selector -- initialization is complete.
|
|
4546
|
+
markInitialized();
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
// Got data but no selector (e.g., cache). Record that data was
|
|
4550
|
+
// received and continue to the next initializer.
|
|
4551
|
+
dataReceived = true;
|
|
4552
|
+
}
|
|
4553
|
+
else if (result.type === 'status') {
|
|
4554
|
+
switch (result.state) {
|
|
4555
|
+
case 'interrupted':
|
|
4556
|
+
case 'terminal_error':
|
|
4557
|
+
logger?.warn(`Initializer failed: ${result.errorInfo?.message ?? 'unknown error'}`);
|
|
4558
|
+
reportStatusError(result);
|
|
4559
|
+
break;
|
|
4560
|
+
case 'shutdown':
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
handleFdv1Fallback(result);
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
// All initializers exhausted.
|
|
4567
|
+
if (dataReceived) {
|
|
4568
|
+
markInitialized();
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
async function runSynchronizers() {
|
|
4572
|
+
while (!closed) {
|
|
4573
|
+
const synchronizer = sourceManager.getNextAvailableSynchronizerAndSetActive();
|
|
4574
|
+
if (synchronizer === undefined) {
|
|
4575
|
+
if (!initialized) {
|
|
4576
|
+
initReject?.(new Error('All data sources exhausted without receiving data.'));
|
|
4577
|
+
initResolve = undefined;
|
|
4578
|
+
initReject = undefined;
|
|
4579
|
+
}
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
const conditions = getConditions(sourceManager.getAvailableSynchronizerCount(), sourceManager.isPrimeSynchronizer(), fallbackTimeoutMs, recoveryTimeoutMs);
|
|
4583
|
+
if (conditions.promise) {
|
|
4584
|
+
logger?.debug('Fallback condition active for current synchronizer.');
|
|
4585
|
+
}
|
|
4586
|
+
// try/finally ensures conditions are closed on all code paths.
|
|
4587
|
+
let synchronizerRunning = true;
|
|
4588
|
+
try {
|
|
4589
|
+
while (!closed && synchronizerRunning) {
|
|
4590
|
+
const syncPromise = synchronizer
|
|
4591
|
+
.next()
|
|
4592
|
+
.then((value) => ({ source: 'sync', value }));
|
|
4593
|
+
const racers = [syncPromise];
|
|
4594
|
+
if (conditions.promise !== undefined) {
|
|
4595
|
+
racers.push(conditions.promise.then((value) => ({ source: 'condition', value })));
|
|
4596
|
+
}
|
|
4597
|
+
const winner = await Promise.race(racers);
|
|
4598
|
+
if (closed) {
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
if (winner.source === 'condition') {
|
|
4602
|
+
const conditionType = winner.value;
|
|
4603
|
+
if (conditionType === 'fallback') {
|
|
4604
|
+
logger?.warn('Fallback condition fired, moving to next synchronizer.');
|
|
4605
|
+
}
|
|
4606
|
+
else if (conditionType === 'recovery') {
|
|
4607
|
+
logger?.info('Recovery condition fired, resetting to primary synchronizer.');
|
|
4608
|
+
sourceManager.resetSourceIndex();
|
|
4609
|
+
}
|
|
4610
|
+
synchronizerRunning = false;
|
|
4611
|
+
}
|
|
4612
|
+
else {
|
|
4613
|
+
// Synchronizer produced a result.
|
|
4614
|
+
const syncResult = winner.value;
|
|
4615
|
+
conditions.inform(syncResult);
|
|
4616
|
+
if (syncResult.type === 'changeSet') {
|
|
4617
|
+
applyChangeSet(syncResult);
|
|
4618
|
+
if (!initialized) {
|
|
4619
|
+
markInitialized();
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
else if (syncResult.type === 'status') {
|
|
4623
|
+
switch (syncResult.state) {
|
|
4624
|
+
case 'interrupted':
|
|
4625
|
+
logger?.warn(`Synchronizer interrupted: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
|
|
4626
|
+
reportStatusError(syncResult);
|
|
4627
|
+
break;
|
|
4628
|
+
case 'terminal_error':
|
|
4629
|
+
logger?.error(`Synchronizer terminal error: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
|
|
4630
|
+
reportStatusError(syncResult);
|
|
4631
|
+
sourceManager.blockCurrentSynchronizer();
|
|
4632
|
+
synchronizerRunning = false;
|
|
4633
|
+
break;
|
|
4634
|
+
case 'shutdown':
|
|
4635
|
+
return;
|
|
4636
|
+
case 'goodbye':
|
|
4637
|
+
// The synchronizer will handle reconnection internally.
|
|
4638
|
+
break;
|
|
4639
|
+
default:
|
|
4640
|
+
break;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
// Check for FDv1 fallback after all result handling — single location.
|
|
4644
|
+
if (handleFdv1Fallback(syncResult)) {
|
|
4645
|
+
synchronizerRunning = false;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
finally {
|
|
4651
|
+
conditions.close();
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
/* eslint-enable no-await-in-loop */
|
|
4656
|
+
async function runOrchestration() {
|
|
4657
|
+
// No sources configured at all — nothing to wait for, immediately valid.
|
|
4658
|
+
if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
|
|
4659
|
+
statusManager.requestStateUpdate('VALID');
|
|
4660
|
+
markInitialized();
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
await runInitializers();
|
|
4664
|
+
if (!closed) {
|
|
4665
|
+
await runSynchronizers();
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
return {
|
|
4669
|
+
start() {
|
|
4670
|
+
return new Promise((resolve, reject) => {
|
|
4671
|
+
initResolve = resolve;
|
|
4672
|
+
initReject = reject;
|
|
4673
|
+
statusManager.requestStateUpdate('INITIALIZING');
|
|
4674
|
+
runOrchestration()
|
|
4675
|
+
.then(() => {
|
|
4676
|
+
// Orchestration completed without error. If the init promise was
|
|
4677
|
+
// never resolved (e.g., close() called during init, or all sources
|
|
4678
|
+
// exhausted without data and no synchronizers), resolve it now.
|
|
4679
|
+
// This prevents the start() promise from hanging forever.
|
|
4680
|
+
if (!initialized) {
|
|
4681
|
+
initReject?.(new Error('Data source closed before initialization completed.'));
|
|
4682
|
+
initResolve = undefined;
|
|
4683
|
+
initReject = undefined;
|
|
4684
|
+
}
|
|
4685
|
+
})
|
|
4686
|
+
.catch((err) => {
|
|
4687
|
+
if (!initialized) {
|
|
4688
|
+
initReject?.(err instanceof Error ? err : new Error(String(err)));
|
|
4689
|
+
initResolve = undefined;
|
|
4690
|
+
initReject = undefined;
|
|
4691
|
+
}
|
|
4692
|
+
else {
|
|
4693
|
+
logger?.error(`Orchestration error: ${err}`);
|
|
4694
|
+
}
|
|
4695
|
+
});
|
|
4696
|
+
});
|
|
4697
|
+
},
|
|
4698
|
+
close() {
|
|
4699
|
+
closed = true;
|
|
4700
|
+
sourceManager.close();
|
|
4701
|
+
},
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
/** Default debounce window duration in milliseconds. */
|
|
4706
|
+
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
4707
|
+
/**
|
|
4708
|
+
* Creates a {@link StateDebounceManager}.
|
|
4709
|
+
*
|
|
4710
|
+
* The manager accumulates state changes from network, lifecycle, and
|
|
4711
|
+
* connection mode events. Each event updates the relevant component
|
|
4712
|
+
* of the pending state and resets the debounce timer. When the timer
|
|
4713
|
+
* fires, the reconciliation callback receives the final combined state.
|
|
4714
|
+
*
|
|
4715
|
+
* @param config Configuration for the debounce manager.
|
|
4716
|
+
*/
|
|
4717
|
+
function createStateDebounceManager(config) {
|
|
4718
|
+
const { initialState, onReconcile, debounceMs = DEFAULT_DEBOUNCE_MS } = config;
|
|
4719
|
+
let { networkState, lifecycleState, requestedMode } = initialState;
|
|
4720
|
+
let timer;
|
|
4721
|
+
let closed = false;
|
|
4722
|
+
function getPendingState() {
|
|
4723
|
+
return { networkState, lifecycleState, requestedMode };
|
|
4724
|
+
}
|
|
4725
|
+
function resetTimer() {
|
|
4726
|
+
if (closed) {
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
if (timer !== undefined) {
|
|
4730
|
+
clearTimeout(timer);
|
|
4731
|
+
}
|
|
4732
|
+
timer = setTimeout(() => {
|
|
4733
|
+
timer = undefined;
|
|
4734
|
+
if (!closed) {
|
|
4735
|
+
onReconcile(getPendingState());
|
|
4736
|
+
}
|
|
4737
|
+
}, debounceMs);
|
|
4738
|
+
}
|
|
4739
|
+
return {
|
|
4740
|
+
setNetworkState(state) {
|
|
4741
|
+
if (networkState === state) {
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
networkState = state;
|
|
4745
|
+
resetTimer();
|
|
4746
|
+
},
|
|
4747
|
+
setLifecycleState(state) {
|
|
4748
|
+
if (lifecycleState === state) {
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
lifecycleState = state;
|
|
4752
|
+
resetTimer();
|
|
4753
|
+
},
|
|
4754
|
+
setRequestedMode(mode) {
|
|
4755
|
+
if (requestedMode === mode) {
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
requestedMode = mode;
|
|
4759
|
+
resetTimer();
|
|
4760
|
+
},
|
|
4761
|
+
close() {
|
|
4762
|
+
closed = true;
|
|
4763
|
+
if (timer !== undefined) {
|
|
4764
|
+
clearTimeout(timer);
|
|
4765
|
+
timer = undefined;
|
|
4766
|
+
}
|
|
4767
|
+
},
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
const logTag = '[FDv2DataManagerBase]';
|
|
4772
|
+
/**
|
|
4773
|
+
* Creates a shared FDv2 data manager that owns mode resolution, debouncing,
|
|
4774
|
+
* selector state, and FDv2DataSource lifecycle. Platform SDKs (browser, RN)
|
|
4775
|
+
* wrap this with platform-specific config and event wiring.
|
|
4776
|
+
*/
|
|
4777
|
+
function createFDv2DataManagerBase(baseConfig) {
|
|
4778
|
+
const { platform, flagManager, config, baseHeaders, emitter, transitionTable, foregroundMode: configuredForegroundMode, backgroundMode, modeTable, sourceFactoryProvider, buildQueryParams, fdv1Endpoints, fallbackTimeoutMs, recoveryTimeoutMs, } = baseConfig;
|
|
4779
|
+
const { logger } = config;
|
|
4780
|
+
const statusManager = createDataSourceStatusManager(emitter);
|
|
4781
|
+
const endpoints = fdv2Endpoints();
|
|
4782
|
+
// Merge user-provided connection mode overrides into the mode table.
|
|
4783
|
+
const effectiveModeTable = config.dataSystem?.connectionModes
|
|
4784
|
+
? { ...modeTable, ...config.dataSystem.connectionModes }
|
|
4785
|
+
: modeTable;
|
|
4786
|
+
// --- Mutable state ---
|
|
4787
|
+
let selector;
|
|
4788
|
+
let currentResolvedMode = configuredForegroundMode;
|
|
4789
|
+
let foregroundMode = configuredForegroundMode;
|
|
4790
|
+
let dataSource;
|
|
4791
|
+
let debounceManager;
|
|
4792
|
+
let identifiedContext;
|
|
4793
|
+
let factoryContext;
|
|
4794
|
+
let initialized = false;
|
|
4795
|
+
let bootstrapped = false;
|
|
4796
|
+
let closed = false;
|
|
4797
|
+
let flushCallback;
|
|
4798
|
+
// Explicit connection mode override — bypasses transition table entirely.
|
|
4799
|
+
let connectionModeOverride;
|
|
4800
|
+
// Forced/automatic streaming state for browser listener-driven streaming.
|
|
4801
|
+
let forcedStreaming;
|
|
4802
|
+
let automaticStreamingState = false;
|
|
4803
|
+
// Outstanding identify promise callbacks — needed so that mode switches
|
|
4804
|
+
// during identify can wire the new data source's completion to the
|
|
4805
|
+
// original identify promise.
|
|
4806
|
+
let pendingIdentifyResolve;
|
|
4807
|
+
let pendingIdentifyReject;
|
|
4808
|
+
// Current debounce input state.
|
|
4809
|
+
let networkState = 'available';
|
|
4810
|
+
let lifecycleState = 'foreground';
|
|
4811
|
+
// --- Helpers ---
|
|
4812
|
+
function getModeDefinition(mode) {
|
|
4813
|
+
return effectiveModeTable[mode];
|
|
4814
|
+
}
|
|
4815
|
+
function buildModeState() {
|
|
4816
|
+
return {
|
|
4817
|
+
lifecycle: lifecycleState,
|
|
4818
|
+
networkAvailable: networkState === 'available',
|
|
4819
|
+
foregroundMode,
|
|
4820
|
+
backgroundMode: backgroundMode ?? 'offline',
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
/**
|
|
4824
|
+
* Resolve the current effective connection mode.
|
|
4825
|
+
*
|
|
4826
|
+
* Priority:
|
|
4827
|
+
* 1. connectionModeOverride (set via setConnectionMode) — bypasses everything
|
|
4828
|
+
* 2. Transition table (network/lifecycle state + foreground/background modes)
|
|
4829
|
+
*/
|
|
4830
|
+
function resolveMode() {
|
|
4831
|
+
if (connectionModeOverride !== undefined) {
|
|
4832
|
+
return connectionModeOverride;
|
|
4833
|
+
}
|
|
4834
|
+
return resolveConnectionMode(transitionTable, buildModeState());
|
|
4835
|
+
}
|
|
4836
|
+
/**
|
|
4837
|
+
* Resolve the foreground mode input for the transition table based on
|
|
4838
|
+
* forced/automatic streaming state.
|
|
4839
|
+
*
|
|
4840
|
+
* Priority: forcedStreaming > automaticStreaming > configuredForegroundMode
|
|
4841
|
+
*/
|
|
4842
|
+
function resolveStreamingForeground() {
|
|
4843
|
+
if (forcedStreaming === true) {
|
|
4844
|
+
return 'streaming';
|
|
4845
|
+
}
|
|
4846
|
+
if (forcedStreaming === false) {
|
|
4847
|
+
return configuredForegroundMode === 'streaming' ? 'one-shot' : configuredForegroundMode;
|
|
4848
|
+
}
|
|
4849
|
+
return automaticStreamingState ? 'streaming' : configuredForegroundMode;
|
|
4850
|
+
}
|
|
4851
|
+
/**
|
|
4852
|
+
* Compute the effective foreground mode from streaming state and push it
|
|
4853
|
+
* through the debounce manager. Used by setForcedStreaming and
|
|
4854
|
+
* setAutomaticStreamingState.
|
|
4855
|
+
*/
|
|
4856
|
+
function pushForegroundMode() {
|
|
4857
|
+
foregroundMode = resolveStreamingForeground();
|
|
4858
|
+
debounceManager?.setRequestedMode(foregroundMode);
|
|
4859
|
+
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Convert a ModeDefinition's entries into concrete InitializerFactory[]
|
|
4862
|
+
* and SynchronizerSlot[] using the source factory provider.
|
|
4863
|
+
*/
|
|
4864
|
+
function buildFactories(modeDef, ctx, includeInitializers) {
|
|
4865
|
+
const initializerFactories = [];
|
|
4866
|
+
if (includeInitializers) {
|
|
4867
|
+
modeDef.initializers
|
|
4868
|
+
// Skip cache when bootstrapped — bootstrap data was applied to the
|
|
4869
|
+
// flag store before identify, so the cache would only load older data.
|
|
4870
|
+
.filter((entry) => !(bootstrapped && entry.type === 'cache'))
|
|
4871
|
+
.forEach((entry) => {
|
|
4872
|
+
const factory = sourceFactoryProvider.createInitializerFactory(entry, ctx);
|
|
4873
|
+
if (factory) {
|
|
4874
|
+
initializerFactories.push(factory);
|
|
4875
|
+
}
|
|
4876
|
+
else {
|
|
4877
|
+
logger.warn(`${logTag} Unsupported initializer type '${entry.type}'. It will be skipped.`);
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4881
|
+
const synchronizerSlots = [];
|
|
4882
|
+
modeDef.synchronizers.forEach((entry) => {
|
|
4883
|
+
const slot = sourceFactoryProvider.createSynchronizerSlot(entry, ctx);
|
|
4884
|
+
if (slot) {
|
|
4885
|
+
synchronizerSlots.push(slot);
|
|
4886
|
+
}
|
|
4887
|
+
else {
|
|
4888
|
+
logger.warn(`${logTag} Unsupported synchronizer type '${entry.type}'. It will be skipped.`);
|
|
4889
|
+
}
|
|
4890
|
+
});
|
|
4891
|
+
// Append a blocked FDv1 fallback synchronizer when configured and
|
|
4892
|
+
// when there are FDv2 synchronizers to fall back from.
|
|
4893
|
+
if (fdv1Endpoints && synchronizerSlots.length > 0) {
|
|
4894
|
+
const fallbackConfig = modeDef.fdv1Fallback;
|
|
4895
|
+
const fallbackPollIntervalMs = (fallbackConfig?.pollInterval ?? config.pollInterval) * 1000;
|
|
4896
|
+
const fallbackServiceEndpoints = fallbackConfig?.endpoints?.pollingBaseUri || fallbackConfig?.endpoints?.streamingBaseUri
|
|
4897
|
+
? new jsSdkCommon.ServiceEndpoints(fallbackConfig.endpoints.streamingBaseUri ?? ctx.serviceEndpoints.streaming, fallbackConfig.endpoints.pollingBaseUri ?? ctx.serviceEndpoints.polling, ctx.serviceEndpoints.events, ctx.serviceEndpoints.analyticsEventPath, ctx.serviceEndpoints.diagnosticEventPath, ctx.serviceEndpoints.includeAuthorizationHeader, ctx.serviceEndpoints.payloadFilterKey)
|
|
4898
|
+
: ctx.serviceEndpoints;
|
|
4899
|
+
const fdv1RequestorFactory = () => makeRequestor(ctx.plainContextString, fallbackServiceEndpoints, fdv1Endpoints.polling(), ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams, config.withReasons, config.useReport);
|
|
4900
|
+
const fdv1SyncFactory = () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger);
|
|
4901
|
+
synchronizerSlots.push(createSynchronizerSlot(fdv1SyncFactory, { isFDv1Fallback: true }));
|
|
4902
|
+
}
|
|
4903
|
+
return { initializerFactories, synchronizerSlots };
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* The data callback shared across all FDv2DataSource instances for
|
|
4907
|
+
* the current identify. Handles selector tracking and flag updates.
|
|
4908
|
+
*/
|
|
4909
|
+
function dataCallback(payload) {
|
|
4910
|
+
logger.debug(`${logTag} dataCallback: type=${payload.type}, updates=${payload.updates.length}, state=${payload.state}`);
|
|
4911
|
+
selector = payload.state;
|
|
4912
|
+
const context = identifiedContext;
|
|
4913
|
+
if (!context) {
|
|
4914
|
+
logger.warn(`${logTag} dataCallback called without an identified context.`);
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
const descriptors = flagEvalPayloadToItemDescriptors(payload.updates ?? []);
|
|
4918
|
+
// Flag updates and change events happen synchronously inside applyChanges.
|
|
4919
|
+
// The returned promise is only for async cache persistence — we intentionally
|
|
4920
|
+
// do not await it so the data source pipeline is not blocked by storage I/O.
|
|
4921
|
+
flagManager.applyChanges(context, descriptors, payload.type).catch((e) => {
|
|
4922
|
+
logger.warn(`${logTag} Failed to persist flag cache: ${e}`);
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Create and start a new FDv2DataSource for the given mode.
|
|
4927
|
+
*
|
|
4928
|
+
* @param mode The connection mode to use.
|
|
4929
|
+
* @param includeInitializers Whether to include initializers (true on
|
|
4930
|
+
* first identify, false on mode switch after initialization).
|
|
4931
|
+
*/
|
|
4932
|
+
function createAndStartDataSource(mode, includeInitializers) {
|
|
4933
|
+
if (!factoryContext) {
|
|
4934
|
+
logger.warn(`${logTag} Cannot create data source without factory context.`);
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4937
|
+
const modeDef = getModeDefinition(mode);
|
|
4938
|
+
const { initializerFactories, synchronizerSlots } = buildFactories(modeDef, factoryContext, includeInitializers);
|
|
4939
|
+
currentResolvedMode = mode;
|
|
4940
|
+
// If there are no sources at all (e.g., offline or one-shot mode
|
|
4941
|
+
// post-initialization), don't create a data source.
|
|
4942
|
+
if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
|
|
4943
|
+
logger.debug(`${logTag} Mode '${mode}' has no sources. No data source created.`);
|
|
4944
|
+
if (!initialized && pendingIdentifyResolve) {
|
|
4945
|
+
// Offline mode during initial identify — resolve immediately.
|
|
4946
|
+
// The SDK will use cached data if any.
|
|
4947
|
+
initialized = true;
|
|
4948
|
+
pendingIdentifyResolve();
|
|
4949
|
+
pendingIdentifyResolve = undefined;
|
|
4950
|
+
pendingIdentifyReject = undefined;
|
|
4951
|
+
}
|
|
4952
|
+
return;
|
|
4953
|
+
}
|
|
4954
|
+
const selectorGetter = () => selector;
|
|
4955
|
+
dataSource = createFDv2DataSource({
|
|
4956
|
+
initializerFactories,
|
|
4957
|
+
synchronizerSlots,
|
|
4958
|
+
dataCallback,
|
|
4959
|
+
statusManager,
|
|
4960
|
+
selectorGetter,
|
|
4961
|
+
logger,
|
|
4962
|
+
fallbackTimeoutMs,
|
|
4963
|
+
recoveryTimeoutMs,
|
|
4964
|
+
});
|
|
4965
|
+
dataSource
|
|
4966
|
+
.start()
|
|
4967
|
+
.then(() => {
|
|
4968
|
+
initialized = true;
|
|
4969
|
+
if (pendingIdentifyResolve) {
|
|
4970
|
+
pendingIdentifyResolve();
|
|
4971
|
+
pendingIdentifyResolve = undefined;
|
|
4972
|
+
pendingIdentifyReject = undefined;
|
|
4973
|
+
}
|
|
4974
|
+
})
|
|
4975
|
+
.catch((err) => {
|
|
4976
|
+
if (pendingIdentifyReject) {
|
|
4977
|
+
pendingIdentifyReject(err instanceof Error ? err : new Error(String(err)));
|
|
4978
|
+
pendingIdentifyResolve = undefined;
|
|
4979
|
+
pendingIdentifyReject = undefined;
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
/**
|
|
4984
|
+
* Reconciliation callback invoked when the debounce timer fires.
|
|
4985
|
+
* Resolves the new mode and switches data sources if needed.
|
|
4986
|
+
*/
|
|
4987
|
+
function onReconcile(pendingState) {
|
|
4988
|
+
if (closed || !factoryContext) {
|
|
4989
|
+
return;
|
|
4990
|
+
}
|
|
4991
|
+
// Update local state from the debounced pending state.
|
|
4992
|
+
networkState = pendingState.networkState;
|
|
4993
|
+
lifecycleState = pendingState.lifecycleState;
|
|
4994
|
+
foregroundMode = pendingState.requestedMode;
|
|
4995
|
+
const newMode = resolveMode();
|
|
4996
|
+
if (newMode === currentResolvedMode) {
|
|
4997
|
+
logger.debug(`${logTag} Reconcile: mode unchanged (${newMode}). No action.`);
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
logger.debug(`${logTag} Reconcile: mode switching from '${currentResolvedMode}' to '${newMode}'.`);
|
|
5001
|
+
// Close the current data source.
|
|
5002
|
+
dataSource?.close();
|
|
5003
|
+
dataSource = undefined;
|
|
5004
|
+
// Include initializers if we don't have a selector yet. This covers:
|
|
5005
|
+
// - Not yet initialized (normal case)
|
|
5006
|
+
// - Initialized from bootstrap (no selector) — need initializers to
|
|
5007
|
+
// get a full payload via poll before starting synchronizers
|
|
5008
|
+
// When we have a selector, only synchronizers change (spec 5.3.8).
|
|
5009
|
+
const includeInitializers = !selector;
|
|
5010
|
+
createAndStartDataSource(newMode, includeInitializers);
|
|
5011
|
+
}
|
|
5012
|
+
// --- Public interface ---
|
|
5013
|
+
return {
|
|
5014
|
+
get configuredForegroundMode() {
|
|
5015
|
+
return configuredForegroundMode;
|
|
5016
|
+
},
|
|
5017
|
+
async identify(identifyResolve, identifyReject, context, identifyOptions) {
|
|
5018
|
+
if (closed) {
|
|
5019
|
+
logger.debug(`${logTag} Identify called after close.`);
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
// Tear down previous state.
|
|
5023
|
+
dataSource?.close();
|
|
5024
|
+
dataSource = undefined;
|
|
5025
|
+
debounceManager?.close();
|
|
5026
|
+
debounceManager = undefined;
|
|
5027
|
+
selector = undefined;
|
|
5028
|
+
initialized = false;
|
|
5029
|
+
bootstrapped = false;
|
|
5030
|
+
identifiedContext = context;
|
|
5031
|
+
pendingIdentifyResolve = identifyResolve;
|
|
5032
|
+
pendingIdentifyReject = identifyReject;
|
|
5033
|
+
const plainContextString = JSON.stringify(jsSdkCommon.Context.toLDContext(context));
|
|
5034
|
+
const queryParams = buildQueryParams(identifyOptions);
|
|
5035
|
+
if (config.withReasons) {
|
|
5036
|
+
queryParams.push({ key: 'withReasons', value: 'true' });
|
|
5037
|
+
}
|
|
5038
|
+
const streamingEndpoints = endpoints.streaming();
|
|
5039
|
+
const pollingEndpoints = endpoints.polling();
|
|
5040
|
+
const requestor = makeFDv2Requestor(plainContextString, config.serviceEndpoints, pollingEndpoints, platform.requests, platform.encoding, baseHeaders, queryParams);
|
|
5041
|
+
const environmentNamespace = await namespaceForEnvironment(platform.crypto, baseConfig.credential);
|
|
5042
|
+
// Re-check after the await — close() may have been called while
|
|
5043
|
+
// namespaceForEnvironment was pending.
|
|
5044
|
+
if (closed) {
|
|
5045
|
+
logger.debug(`${logTag} Identify aborted: closed during async setup.`);
|
|
5046
|
+
return;
|
|
5047
|
+
}
|
|
5048
|
+
factoryContext = {
|
|
5049
|
+
requestor,
|
|
5050
|
+
requests: platform.requests,
|
|
5051
|
+
encoding: platform.encoding,
|
|
5052
|
+
serviceEndpoints: config.serviceEndpoints,
|
|
5053
|
+
baseHeaders,
|
|
5054
|
+
queryParams,
|
|
5055
|
+
plainContextString,
|
|
5056
|
+
logger,
|
|
5057
|
+
polling: {
|
|
5058
|
+
paths: pollingEndpoints,
|
|
5059
|
+
intervalSeconds: config.pollInterval,
|
|
5060
|
+
},
|
|
5061
|
+
streaming: {
|
|
5062
|
+
paths: streamingEndpoints,
|
|
5063
|
+
initialReconnectDelaySeconds: config.streamInitialReconnectDelay,
|
|
5064
|
+
},
|
|
5065
|
+
storage: platform.storage,
|
|
5066
|
+
crypto: platform.crypto,
|
|
5067
|
+
environmentNamespace,
|
|
5068
|
+
context,
|
|
5069
|
+
};
|
|
5070
|
+
// Ensure foreground mode reflects current streaming state before resolving.
|
|
5071
|
+
foregroundMode = resolveStreamingForeground();
|
|
5072
|
+
// Resolve the initial mode.
|
|
5073
|
+
const mode = resolveMode();
|
|
5074
|
+
logger.debug(`${logTag} Identify: initial mode resolved to '${mode}'.`);
|
|
5075
|
+
bootstrapped = identifyOptions?.bootstrap !== undefined;
|
|
5076
|
+
if (bootstrapped) {
|
|
5077
|
+
// Bootstrap data was already applied to the flag store by the
|
|
5078
|
+
// caller (BrowserClient.start → presetFlags) before identify
|
|
5079
|
+
// was called. Resolve immediately — flag evaluations will use
|
|
5080
|
+
// the bootstrap data synchronously.
|
|
5081
|
+
initialized = true;
|
|
5082
|
+
statusManager.requestStateUpdate('VALID');
|
|
5083
|
+
// selector remains undefined — bootstrap data has no selector.
|
|
5084
|
+
pendingIdentifyResolve?.();
|
|
5085
|
+
pendingIdentifyResolve = undefined;
|
|
5086
|
+
pendingIdentifyReject = undefined;
|
|
5087
|
+
// Only create a data source if the mode has synchronizers.
|
|
5088
|
+
// For one-shot (no synchronizers), there's nothing more to do.
|
|
5089
|
+
const modeDef = getModeDefinition(mode);
|
|
5090
|
+
if (modeDef.synchronizers.length > 0) {
|
|
5091
|
+
// Start synchronizers without initializers — we already have
|
|
5092
|
+
// data from bootstrap. Initializers will run on mode switches
|
|
5093
|
+
// if selector is still undefined (see onReconcile).
|
|
5094
|
+
createAndStartDataSource(mode, false);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
else {
|
|
5098
|
+
// Normal identify — create and start the data source with full pipeline.
|
|
5099
|
+
createAndStartDataSource(mode, true);
|
|
5100
|
+
}
|
|
5101
|
+
// Set up debouncing for subsequent state changes.
|
|
5102
|
+
debounceManager = createStateDebounceManager({
|
|
5103
|
+
initialState: {
|
|
5104
|
+
networkState,
|
|
5105
|
+
lifecycleState,
|
|
5106
|
+
requestedMode: foregroundMode,
|
|
5107
|
+
},
|
|
5108
|
+
onReconcile,
|
|
5109
|
+
});
|
|
5110
|
+
},
|
|
5111
|
+
close() {
|
|
5112
|
+
closed = true;
|
|
5113
|
+
dataSource?.close();
|
|
5114
|
+
dataSource = undefined;
|
|
5115
|
+
debounceManager?.close();
|
|
5116
|
+
debounceManager = undefined;
|
|
5117
|
+
pendingIdentifyResolve = undefined;
|
|
5118
|
+
pendingIdentifyReject = undefined;
|
|
5119
|
+
},
|
|
5120
|
+
setNetworkState(state) {
|
|
5121
|
+
networkState = state;
|
|
5122
|
+
debounceManager?.setNetworkState(state);
|
|
5123
|
+
},
|
|
5124
|
+
setLifecycleState(state) {
|
|
5125
|
+
// Flush immediately when going to background — the app may be
|
|
5126
|
+
// about to close. This is not debounced (CONNMODE spec 3.3.1).
|
|
5127
|
+
if (state === 'background' && lifecycleState !== 'background') {
|
|
5128
|
+
flushCallback?.();
|
|
5129
|
+
}
|
|
5130
|
+
lifecycleState = state;
|
|
5131
|
+
debounceManager?.setLifecycleState(state);
|
|
5132
|
+
},
|
|
5133
|
+
setConnectionMode(mode) {
|
|
5134
|
+
connectionModeOverride = mode;
|
|
5135
|
+
if (mode !== undefined) {
|
|
5136
|
+
debounceManager?.setRequestedMode(mode);
|
|
5137
|
+
}
|
|
5138
|
+
else {
|
|
5139
|
+
pushForegroundMode();
|
|
5140
|
+
}
|
|
5141
|
+
},
|
|
5142
|
+
getCurrentMode() {
|
|
5143
|
+
return currentResolvedMode;
|
|
5144
|
+
},
|
|
5145
|
+
setFlushCallback(callback) {
|
|
5146
|
+
flushCallback = callback;
|
|
5147
|
+
},
|
|
5148
|
+
setForcedStreaming(streaming) {
|
|
5149
|
+
forcedStreaming = streaming;
|
|
5150
|
+
pushForegroundMode();
|
|
5151
|
+
},
|
|
5152
|
+
setAutomaticStreamingState(streaming) {
|
|
5153
|
+
automaticStreamingState = streaming;
|
|
5154
|
+
pushForegroundMode();
|
|
5155
|
+
},
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
|
|
3971
5159
|
exports.platform = jsSdkCommon__namespace;
|
|
3972
5160
|
exports.BROWSER_DATA_SYSTEM_DEFAULTS = BROWSER_DATA_SYSTEM_DEFAULTS;
|
|
3973
5161
|
exports.BROWSER_TRANSITION_TABLE = BROWSER_TRANSITION_TABLE;
|
|
@@ -3982,12 +5170,15 @@ exports.MODE_TABLE = MODE_TABLE;
|
|
|
3982
5170
|
exports.browserFdv1Endpoints = browserFdv1Endpoints;
|
|
3983
5171
|
exports.createDataSourceStatusManager = createDataSourceStatusManager;
|
|
3984
5172
|
exports.createDefaultSourceFactoryProvider = createDefaultSourceFactoryProvider;
|
|
5173
|
+
exports.createFDv2DataManagerBase = createFDv2DataManagerBase;
|
|
5174
|
+
exports.createStateDebounceManager = createStateDebounceManager;
|
|
3985
5175
|
exports.dataSystemValidators = dataSystemValidators;
|
|
3986
5176
|
exports.fdv2Endpoints = fdv2Endpoints;
|
|
3987
5177
|
exports.makeRequestor = makeRequestor;
|
|
3988
5178
|
exports.mobileFdv1Endpoints = mobileFdv1Endpoints;
|
|
3989
5179
|
exports.readFlagsFromBootstrap = readFlagsFromBootstrap;
|
|
3990
5180
|
exports.resolveConnectionMode = resolveConnectionMode;
|
|
5181
|
+
exports.resolveForegroundMode = resolveForegroundMode;
|
|
3991
5182
|
exports.safeRegisterDebugOverridePlugins = safeRegisterDebugOverridePlugins;
|
|
3992
5183
|
exports.validateOptions = validateOptions;
|
|
3993
5184
|
Object.keys(jsSdkCommon).forEach(function (k) {
|