@launchdarkly/js-client-sdk-common 1.24.0 → 1.26.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 +26 -0
- package/dist/cjs/LDClientImpl.d.ts +19 -8
- package/dist/cjs/LDClientImpl.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/index.d.ts +1 -0
- package/dist/cjs/api/index.d.ts.map +1 -1
- package/dist/cjs/configuration/Configuration.d.ts +10 -0
- package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
- package/dist/cjs/context/ensureKey.d.ts.map +1 -1
- package/dist/cjs/datasource/FDv2DataManagerBase.d.ts.map +1 -1
- package/dist/cjs/datasource/SourceFactoryProvider.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/SourceManager.d.ts +28 -10
- package/dist/cjs/datasource/fdv2/SourceManager.d.ts.map +1 -1
- package/dist/cjs/index.cjs +231 -95
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/storage/getOrGenerateKey.d.ts +4 -1
- package/dist/cjs/storage/getOrGenerateKey.d.ts.map +1 -1
- package/dist/cjs/storage/namespaceUtils.d.ts +3 -6
- package/dist/cjs/storage/namespaceUtils.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/LDStartOptions.d.ts +19 -0
- package/dist/esm/api/LDStartOptions.d.ts.map +1 -0
- 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 +10 -0
- package/dist/esm/configuration/Configuration.d.ts.map +1 -1
- package/dist/esm/context/ensureKey.d.ts.map +1 -1
- package/dist/esm/datasource/FDv2DataManagerBase.d.ts.map +1 -1
- package/dist/esm/datasource/SourceFactoryProvider.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/SourceManager.d.ts +28 -10
- package/dist/esm/datasource/fdv2/SourceManager.d.ts.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.mjs +231 -95
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/storage/getOrGenerateKey.d.ts +4 -1
- package/dist/esm/storage/getOrGenerateKey.d.ts.map +1 -1
- package/dist/esm/storage/namespaceUtils.d.ts +3 -6
- package/dist/esm/storage/namespaceUtils.d.ts.map +1 -1
- package/package.json +1 -3
package/dist/esm/index.mjs
CHANGED
|
@@ -680,13 +680,28 @@ async function digest(hasher, encoding) {
|
|
|
680
680
|
* @param storageKey keyed storage location where the generated key should live. See {@link namespaceForGeneratedContextKey}
|
|
681
681
|
* for related exmaples of generating a storage key and usage.
|
|
682
682
|
* @param platform crypto and storage implementations for necessary operations
|
|
683
|
+
* @param legacyStorageKey optional legacy storage key to migrate from. If the key is not found
|
|
684
|
+
* under {@link storageKey} but exists under this legacy key, it will be migrated to the new
|
|
685
|
+
* location and the legacy key will be cleared.
|
|
683
686
|
* @returns the generated key
|
|
684
687
|
*/
|
|
685
|
-
const getOrGenerateKey = async (storageKey, { crypto, storage }) => {
|
|
688
|
+
const getOrGenerateKey = async (storageKey, { crypto, storage }, legacyStorageKey) => {
|
|
686
689
|
let generatedKey = await storage?.get(storageKey);
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
+
if (generatedKey == null) {
|
|
691
|
+
if (legacyStorageKey) {
|
|
692
|
+
generatedKey = await storage?.get(legacyStorageKey);
|
|
693
|
+
if (generatedKey != null) {
|
|
694
|
+
await storage?.set(storageKey, generatedKey);
|
|
695
|
+
const verified = await storage?.get(storageKey);
|
|
696
|
+
if (verified != null) {
|
|
697
|
+
await storage?.clear(legacyStorageKey);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (generatedKey == null) {
|
|
702
|
+
generatedKey = crypto.randomUUID();
|
|
703
|
+
await storage?.set(storageKey, generatedKey);
|
|
704
|
+
}
|
|
690
705
|
}
|
|
691
706
|
return generatedKey;
|
|
692
707
|
};
|
|
@@ -709,12 +724,9 @@ async function namespaceForEnvironment(crypto, sdkKey) {
|
|
|
709
724
|
]);
|
|
710
725
|
}
|
|
711
726
|
/**
|
|
712
|
-
* @deprecated
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* feature and those were namespaced in LaunchDarkly_ContextKeys. This function can be removed
|
|
716
|
-
* when the data under the LaunchDarkly_AnonymousKeys namespace is merged with data under the
|
|
717
|
-
* LaunchDarkly_ContextKeys namespace.
|
|
727
|
+
* @deprecated Used only for migration in ensureKey. Data stored under LaunchDarkly_AnonymousKeys
|
|
728
|
+
* is now migrated to LaunchDarkly_ContextKeys on first access. This function can be removed once
|
|
729
|
+
* all clients have had the chance to run the migration.
|
|
718
730
|
*/
|
|
719
731
|
async function namespaceForAnonymousGeneratedContextKey(kind) {
|
|
720
732
|
return concatNamespacesAndValues([
|
|
@@ -897,10 +909,11 @@ const { isLegacyUser, isMultiKind, isSingleKind } = internal;
|
|
|
897
909
|
const ensureKeyCommon = async (kind, c, platform) => {
|
|
898
910
|
const { anonymous, key } = c;
|
|
899
911
|
if (anonymous && !key) {
|
|
900
|
-
const storageKey = await
|
|
912
|
+
const storageKey = await namespaceForGeneratedContextKey(kind);
|
|
913
|
+
const legacyStorageKey = await namespaceForAnonymousGeneratedContextKey(kind);
|
|
901
914
|
// This mutates a cloned copy of the original context from ensureyKey so this is safe.
|
|
902
915
|
// eslint-disable-next-line no-param-reassign
|
|
903
|
-
c.key = await getOrGenerateKey(storageKey, platform);
|
|
916
|
+
c.key = await getOrGenerateKey(storageKey, platform, legacyStorageKey);
|
|
904
917
|
}
|
|
905
918
|
};
|
|
906
919
|
const ensureKeySingle = async (c, platform) => {
|
|
@@ -999,6 +1012,46 @@ class EventFactory extends internal.EventFactoryBase {
|
|
|
999
1012
|
}
|
|
1000
1013
|
}
|
|
1001
1014
|
|
|
1015
|
+
function readFlagsFromBootstrap(logger, data) {
|
|
1016
|
+
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
1017
|
+
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
1018
|
+
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
1019
|
+
const keys = Object.keys(data);
|
|
1020
|
+
const metadataKey = '$flagsState';
|
|
1021
|
+
const validKey = '$valid';
|
|
1022
|
+
const metadata = data[metadataKey];
|
|
1023
|
+
if (!metadata && keys.length) {
|
|
1024
|
+
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
1025
|
+
' metadata. Events may not be sent correctly.');
|
|
1026
|
+
}
|
|
1027
|
+
if (data[validKey] === false) {
|
|
1028
|
+
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
1029
|
+
}
|
|
1030
|
+
const ret = {};
|
|
1031
|
+
keys.forEach((key) => {
|
|
1032
|
+
if (key !== metadataKey && key !== validKey) {
|
|
1033
|
+
let flag;
|
|
1034
|
+
if (metadata && metadata[key]) {
|
|
1035
|
+
flag = {
|
|
1036
|
+
value: data[key],
|
|
1037
|
+
...metadata[key],
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
flag = {
|
|
1042
|
+
value: data[key],
|
|
1043
|
+
version: 0,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
ret[key] = {
|
|
1047
|
+
version: flag.version,
|
|
1048
|
+
flag,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
return ret;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1002
1055
|
/**
|
|
1003
1056
|
* Suffix appended to context storage keys to form the freshness storage key.
|
|
1004
1057
|
*/
|
|
@@ -1927,6 +1980,10 @@ class LDClientImpl {
|
|
|
1927
1980
|
this._eventFactoryWithReasons = new EventFactory(true);
|
|
1928
1981
|
this._eventSendingEnabled = false;
|
|
1929
1982
|
this._identifyQueue = createAsyncTaskQueue();
|
|
1983
|
+
// NOTE: this is used to ease the transition to the new initialization pattern.
|
|
1984
|
+
// All client SDKs should set this to true with the exception of React Native which is
|
|
1985
|
+
// still using the deprecated construct + identify pattern.
|
|
1986
|
+
this._requiresStart = false;
|
|
1930
1987
|
if (!sdkKey) {
|
|
1931
1988
|
throw new Error('You must configure the client with a client-side SDK key');
|
|
1932
1989
|
}
|
|
@@ -1935,6 +1992,8 @@ class LDClientImpl {
|
|
|
1935
1992
|
}
|
|
1936
1993
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1937
1994
|
this.logger = this._config.logger;
|
|
1995
|
+
this._requiresStart = internalOptions?.requiresStart ?? false;
|
|
1996
|
+
this.initialContext = internalOptions?.initialContext;
|
|
1938
1997
|
this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1939
1998
|
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1940
1999
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
@@ -1952,6 +2011,8 @@ class LDClientImpl {
|
|
|
1952
2011
|
});
|
|
1953
2012
|
});
|
|
1954
2013
|
this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
|
|
2014
|
+
this.isFDv2 = !!this._config.dataSystem;
|
|
2015
|
+
this.dataSystemConfig = this._config.dataSystem;
|
|
1955
2016
|
const hooks = [...this._config.hooks];
|
|
1956
2017
|
this.environmentMetadata = createPluginEnvironmentMetadata(this.sdkKey, this.platform, this._config);
|
|
1957
2018
|
this._config.getImplementationHooks(this.environmentMetadata).forEach((hook) => {
|
|
@@ -2029,6 +2090,57 @@ class LDClientImpl {
|
|
|
2029
2090
|
presetFlags(newFlags) {
|
|
2030
2091
|
this._flagManager.presetFlags(newFlags);
|
|
2031
2092
|
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Starts the client and returns a promise that resolves to the initialization result.
|
|
2095
|
+
*
|
|
2096
|
+
* This method is idempotent - calling it multiple times returns the same promise.
|
|
2097
|
+
*
|
|
2098
|
+
* @param options Optional configuration. See {@link LDStartOptions}.
|
|
2099
|
+
* @returns A promise that resolves to the initialization result.
|
|
2100
|
+
*/
|
|
2101
|
+
start(options) {
|
|
2102
|
+
if (this.initializeResult) {
|
|
2103
|
+
return Promise.resolve(this.initializeResult);
|
|
2104
|
+
}
|
|
2105
|
+
if (this.startPromise) {
|
|
2106
|
+
return this.startPromise;
|
|
2107
|
+
}
|
|
2108
|
+
if (!this.initialContext) {
|
|
2109
|
+
this.logger.error('Initial context not set');
|
|
2110
|
+
return Promise.resolve({ status: 'failed', error: new Error('Initial context not set') });
|
|
2111
|
+
}
|
|
2112
|
+
const identifyOptions = {
|
|
2113
|
+
...(options?.identifyOptions ?? {}),
|
|
2114
|
+
// Initial identify operations are not sheddable.
|
|
2115
|
+
sheddable: false,
|
|
2116
|
+
};
|
|
2117
|
+
// If the bootstrap data is provided in the start options, and the identify options do not
|
|
2118
|
+
// have bootstrap data, then use the bootstrap data from the start options.
|
|
2119
|
+
if (options?.bootstrap && !identifyOptions.bootstrap) {
|
|
2120
|
+
identifyOptions.bootstrap = options.bootstrap;
|
|
2121
|
+
}
|
|
2122
|
+
if (identifyOptions.bootstrap) {
|
|
2123
|
+
try {
|
|
2124
|
+
if (!identifyOptions.bootstrapParsed) {
|
|
2125
|
+
identifyOptions.bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap);
|
|
2126
|
+
}
|
|
2127
|
+
if (identifyOptions.bootstrapParsed) {
|
|
2128
|
+
this.presetFlags(identifyOptions.bootstrapParsed);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
catch (error) {
|
|
2132
|
+
this.logger.error('Failed to bootstrap data', error);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (!this.initializedPromise) {
|
|
2136
|
+
this.initializedPromise = new Promise((resolve) => {
|
|
2137
|
+
this.initResolve = resolve;
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
this.startPromise = this._promiseWithTimeout(this.initializedPromise, options?.timeout ?? 5, 'start');
|
|
2141
|
+
this.identifyResult(this.initialContext, identifyOptions);
|
|
2142
|
+
return this.startPromise;
|
|
2143
|
+
}
|
|
2032
2144
|
/**
|
|
2033
2145
|
* Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
|
|
2034
2146
|
*
|
|
@@ -2066,6 +2178,10 @@ class LDClientImpl {
|
|
|
2066
2178
|
// If completed or shed, then we are done.
|
|
2067
2179
|
}
|
|
2068
2180
|
async identifyResult(pristineContext, identifyOptions) {
|
|
2181
|
+
if (this._requiresStart && !this.startPromise) {
|
|
2182
|
+
this.logger.error('The client must be started before a context can be identified. Call start() prior to identifying a context.');
|
|
2183
|
+
return { status: 'error', error: new Error('Identify called before start') };
|
|
2184
|
+
}
|
|
2069
2185
|
const identifyTimeout = identifyOptions?.timeout ?? DEFAULT_IDENTIFY_TIMEOUT_SECONDS;
|
|
2070
2186
|
const noTimeout = identifyOptions?.timeout === undefined && identifyOptions?.noTimeout === true;
|
|
2071
2187
|
// When noTimeout is specified, and a timeout is not specified, then this condition cannot
|
|
@@ -2175,7 +2291,7 @@ class LDClientImpl {
|
|
|
2175
2291
|
// If waitForInitialization was previously called, then return the promise with a timeout.
|
|
2176
2292
|
// This condition should only be triggered if waitForInitialization was called multiple times.
|
|
2177
2293
|
if (this.initializedPromise) {
|
|
2178
|
-
return this.
|
|
2294
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2179
2295
|
}
|
|
2180
2296
|
// Create a new promise for tracking initialization
|
|
2181
2297
|
if (!this.initializedPromise) {
|
|
@@ -2183,22 +2299,18 @@ class LDClientImpl {
|
|
|
2183
2299
|
this.initResolve = resolve;
|
|
2184
2300
|
});
|
|
2185
2301
|
}
|
|
2186
|
-
return this.
|
|
2302
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2187
2303
|
}
|
|
2188
2304
|
/**
|
|
2189
|
-
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2305
|
+
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2306
|
+
* and start.
|
|
2190
2307
|
*
|
|
2191
2308
|
* @param basePromise The promise to race against a timeout.
|
|
2192
2309
|
* @param timeout The timeout in seconds.
|
|
2193
2310
|
* @returns A promise that resolves to the initialization result or timeout.
|
|
2194
|
-
*
|
|
2195
|
-
* @privateRemarks
|
|
2196
|
-
* This method is protected because it is used by the browser SDK's `start` method.
|
|
2197
|
-
* Eventually, the start method will be moved to this common implementation and this method will
|
|
2198
|
-
* be made private.
|
|
2199
2311
|
*/
|
|
2200
|
-
|
|
2201
|
-
const cancelableTimeout = cancelableTimedPromise(timeout,
|
|
2312
|
+
_promiseWithTimeout(basePromise, timeout, label = 'waitForInitialization') {
|
|
2313
|
+
const cancelableTimeout = cancelableTimedPromise(timeout, label);
|
|
2202
2314
|
return Promise.race([
|
|
2203
2315
|
basePromise.then((res) => {
|
|
2204
2316
|
cancelableTimeout.cancel();
|
|
@@ -2428,46 +2540,6 @@ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
|
|
|
2428
2540
|
});
|
|
2429
2541
|
}
|
|
2430
2542
|
|
|
2431
|
-
function readFlagsFromBootstrap(logger, data) {
|
|
2432
|
-
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
2433
|
-
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
2434
|
-
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
2435
|
-
const keys = Object.keys(data);
|
|
2436
|
-
const metadataKey = '$flagsState';
|
|
2437
|
-
const validKey = '$valid';
|
|
2438
|
-
const metadata = data[metadataKey];
|
|
2439
|
-
if (!metadata && keys.length) {
|
|
2440
|
-
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
2441
|
-
' metadata. Events may not be sent correctly.');
|
|
2442
|
-
}
|
|
2443
|
-
if (data[validKey] === false) {
|
|
2444
|
-
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
2445
|
-
}
|
|
2446
|
-
const ret = {};
|
|
2447
|
-
keys.forEach((key) => {
|
|
2448
|
-
if (key !== metadataKey && key !== validKey) {
|
|
2449
|
-
let flag;
|
|
2450
|
-
if (metadata && metadata[key]) {
|
|
2451
|
-
flag = {
|
|
2452
|
-
value: data[key],
|
|
2453
|
-
...metadata[key],
|
|
2454
|
-
};
|
|
2455
|
-
}
|
|
2456
|
-
else {
|
|
2457
|
-
flag = {
|
|
2458
|
-
value: data[key],
|
|
2459
|
-
version: 0,
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
ret[key] = {
|
|
2463
|
-
version: flag.version,
|
|
2464
|
-
flag,
|
|
2465
|
-
};
|
|
2466
|
-
}
|
|
2467
|
-
});
|
|
2468
|
-
return ret;
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
2543
|
/**
|
|
2472
2544
|
* Creates endpoint paths for browser (client-side ID) FDv1 evaluation.
|
|
2473
2545
|
*
|
|
@@ -3190,12 +3262,12 @@ async function loadFromCache(config) {
|
|
|
3190
3262
|
const { storage, crypto, environmentNamespace, context, logger } = config;
|
|
3191
3263
|
if (!storage) {
|
|
3192
3264
|
logger?.debug('No storage available for cache initializer');
|
|
3193
|
-
return
|
|
3265
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3194
3266
|
}
|
|
3195
3267
|
const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
|
|
3196
3268
|
if (!cached) {
|
|
3197
3269
|
logger?.debug('Cache miss for context');
|
|
3198
|
-
return
|
|
3270
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3199
3271
|
}
|
|
3200
3272
|
const updates = Object.entries(cached.flags).map(([key, flag]) => ({
|
|
3201
3273
|
kind: 'flag-eval',
|
|
@@ -3228,21 +3300,24 @@ async function loadFromCache(config) {
|
|
|
3228
3300
|
* @internal
|
|
3229
3301
|
*/
|
|
3230
3302
|
function createCacheInitializerFactory(config) {
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
shutdownResolve
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3303
|
+
return {
|
|
3304
|
+
isCache: true,
|
|
3305
|
+
// The selectorGetter is ignored -- cache data has no selector.
|
|
3306
|
+
create(_selectorGetter) {
|
|
3307
|
+
let shutdownResolve;
|
|
3308
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
3309
|
+
shutdownResolve = resolve;
|
|
3310
|
+
});
|
|
3311
|
+
return {
|
|
3312
|
+
async run() {
|
|
3313
|
+
return Promise.race([shutdownPromise, loadFromCache(config)]);
|
|
3314
|
+
},
|
|
3315
|
+
close() {
|
|
3316
|
+
shutdownResolve?.(shutdown());
|
|
3317
|
+
shutdownResolve = undefined;
|
|
3318
|
+
},
|
|
3319
|
+
};
|
|
3320
|
+
},
|
|
3246
3321
|
};
|
|
3247
3322
|
}
|
|
3248
3323
|
|
|
@@ -3710,7 +3785,7 @@ function createSourceManager(initializerFactories, synchronizerSlots, selectorGe
|
|
|
3710
3785
|
return undefined;
|
|
3711
3786
|
}
|
|
3712
3787
|
closeActiveSource();
|
|
3713
|
-
const initializer = initializerFactories[initializerIndex](selectorGetter);
|
|
3788
|
+
const initializer = initializerFactories[initializerIndex].create(selectorGetter);
|
|
3714
3789
|
activeSource = initializer;
|
|
3715
3790
|
return initializer;
|
|
3716
3791
|
},
|
|
@@ -3730,7 +3805,7 @@ function createSourceManager(initializerFactories, synchronizerSlots, selectorGe
|
|
|
3730
3805
|
const candidate = synchronizerSlots[synchronizerIndex];
|
|
3731
3806
|
if (candidate.state === 'available') {
|
|
3732
3807
|
closeActiveSource();
|
|
3733
|
-
const synchronizer = candidate.factory(selectorGetter);
|
|
3808
|
+
const synchronizer = candidate.factory.create(selectorGetter);
|
|
3734
3809
|
activeSource = synchronizer;
|
|
3735
3810
|
return synchronizer;
|
|
3736
3811
|
}
|
|
@@ -4120,10 +4195,14 @@ function createDefaultSourceFactoryProvider() {
|
|
|
4120
4195
|
switch (entry.type) {
|
|
4121
4196
|
case 'polling': {
|
|
4122
4197
|
const requestor = resolvePollingRequestor(ctx, entry.endpoints);
|
|
4123
|
-
return
|
|
4198
|
+
return {
|
|
4199
|
+
create: (sg) => createPollingInitializer(requestor, ctx.logger, sg),
|
|
4200
|
+
};
|
|
4124
4201
|
}
|
|
4125
4202
|
case 'streaming':
|
|
4126
|
-
return
|
|
4203
|
+
return {
|
|
4204
|
+
create: (sg) => createStreamingInitializer(buildStreamingBase(entry, ctx, sg)),
|
|
4205
|
+
};
|
|
4127
4206
|
case 'cache':
|
|
4128
4207
|
return createCacheInitializerFactory({
|
|
4129
4208
|
storage: ctx.storage,
|
|
@@ -4141,13 +4220,14 @@ function createDefaultSourceFactoryProvider() {
|
|
|
4141
4220
|
case 'polling': {
|
|
4142
4221
|
const intervalMs = (entry.pollInterval ?? ctx.polling.intervalSeconds) * 1000;
|
|
4143
4222
|
const requestor = resolvePollingRequestor(ctx, entry.endpoints);
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
case 'streaming': {
|
|
4148
|
-
const factory = (sg) => createStreamingSynchronizer(buildStreamingBase(entry, ctx, sg));
|
|
4149
|
-
return createSynchronizerSlot(factory);
|
|
4223
|
+
return createSynchronizerSlot({
|
|
4224
|
+
create: (sg) => createPollingSynchronizer(requestor, ctx.logger, sg, intervalMs),
|
|
4225
|
+
});
|
|
4150
4226
|
}
|
|
4227
|
+
case 'streaming':
|
|
4228
|
+
return createSynchronizerSlot({
|
|
4229
|
+
create: (sg) => createStreamingSynchronizer(buildStreamingBase(entry, ctx, sg)),
|
|
4230
|
+
});
|
|
4151
4231
|
default:
|
|
4152
4232
|
return undefined;
|
|
4153
4233
|
}
|
|
@@ -4418,6 +4498,15 @@ function createFDv2DataSource(config) {
|
|
|
4418
4498
|
let dataReceived = false;
|
|
4419
4499
|
let initResolve;
|
|
4420
4500
|
let initReject;
|
|
4501
|
+
// When every initializer is a cache initializer and there are no
|
|
4502
|
+
// synchronizers, the cache is the only possible data source. A cache miss
|
|
4503
|
+
// in that configuration must not fail initialization -- there is nowhere
|
|
4504
|
+
// else for data to come from, and reporting an error would be meaningless.
|
|
4505
|
+
// Mirrors the Android SDK's InitializerFromCache / hasAvailableSources
|
|
4506
|
+
// behavior.
|
|
4507
|
+
const cacheOnlyDataSystem = initializerFactories.length > 0 &&
|
|
4508
|
+
initializerFactories.every((f) => f.isCache === true) &&
|
|
4509
|
+
synchronizerSlots.length === 0;
|
|
4421
4510
|
const sourceManager = createSourceManager(initializerFactories, synchronizerSlots, selectorGetter);
|
|
4422
4511
|
function markInitialized() {
|
|
4423
4512
|
if (!initialized) {
|
|
@@ -4447,6 +4536,10 @@ function createFDv2DataSource(config) {
|
|
|
4447
4536
|
// The orchestration loops intentionally use await-in-loop for sequential
|
|
4448
4537
|
// state machine processing — one result at a time.
|
|
4449
4538
|
async function runInitializers() {
|
|
4539
|
+
// Tracks whether any initializer reported interrupted/terminal_error.
|
|
4540
|
+
// Used below so the cache-only exhaustion branch does not overwrite
|
|
4541
|
+
// that error status with VALID.
|
|
4542
|
+
let errorReportedDuringInit = false;
|
|
4450
4543
|
while (!closed) {
|
|
4451
4544
|
const initializer = sourceManager.getNextInitializerAndSetActive();
|
|
4452
4545
|
if (initializer === undefined) {
|
|
@@ -4456,16 +4549,16 @@ function createFDv2DataSource(config) {
|
|
|
4456
4549
|
if (closed) {
|
|
4457
4550
|
return;
|
|
4458
4551
|
}
|
|
4459
|
-
if (result.type === 'changeSet') {
|
|
4552
|
+
if (result.type === 'changeSet' && result.payload.type !== 'none') {
|
|
4460
4553
|
applyChangeSet(result);
|
|
4461
4554
|
if (handleFdv1Fallback(result)) {
|
|
4462
|
-
// FDv1 fallback triggered during initialization
|
|
4555
|
+
// FDv1 fallback triggered during initialization -- data was received
|
|
4463
4556
|
// but we should move to synchronizers where the FDv1 adapter will run.
|
|
4464
4557
|
dataReceived = true;
|
|
4465
4558
|
break;
|
|
4466
4559
|
}
|
|
4467
4560
|
if (result.payload.state) {
|
|
4468
|
-
// Got basis data with a selector
|
|
4561
|
+
// Got basis data with a selector -- initialization is complete.
|
|
4469
4562
|
markInitialized();
|
|
4470
4563
|
return;
|
|
4471
4564
|
}
|
|
@@ -4479,6 +4572,7 @@ function createFDv2DataSource(config) {
|
|
|
4479
4572
|
case 'terminal_error':
|
|
4480
4573
|
logger?.warn(`Initializer failed: ${result.errorInfo?.message ?? 'unknown error'}`);
|
|
4481
4574
|
reportStatusError(result);
|
|
4575
|
+
errorReportedDuringInit = true;
|
|
4482
4576
|
break;
|
|
4483
4577
|
case 'shutdown':
|
|
4484
4578
|
return;
|
|
@@ -4486,8 +4580,30 @@ function createFDv2DataSource(config) {
|
|
|
4486
4580
|
handleFdv1Fallback(result);
|
|
4487
4581
|
}
|
|
4488
4582
|
}
|
|
4583
|
+
// close() between the last loop iteration and the exhaustion branch.
|
|
4584
|
+
// Exit without marking initialized or emitting a spurious VALID; the
|
|
4585
|
+
// start() promise will be rejected by the post-orchestration handler
|
|
4586
|
+
// with "closed before initialization completed."
|
|
4587
|
+
if (closed) {
|
|
4588
|
+
return;
|
|
4589
|
+
}
|
|
4489
4590
|
// All initializers exhausted.
|
|
4490
|
-
if (
|
|
4591
|
+
if (cacheOnlyDataSystem) {
|
|
4592
|
+
// Cache-only data system with no synchronizer to produce a VALID
|
|
4593
|
+
// status on its own. On a cache miss with no errors, nothing else
|
|
4594
|
+
// has asserted VALID yet, so do it here. Skip the update if:
|
|
4595
|
+
// - dataReceived (cache hit): applyChangeSet already asserted VALID.
|
|
4596
|
+
// - errorReportedDuringInit: reportError set an error status that
|
|
4597
|
+
// must not be silently overwritten.
|
|
4598
|
+
if (!dataReceived && !errorReportedDuringInit) {
|
|
4599
|
+
statusManager.requestStateUpdate('VALID');
|
|
4600
|
+
}
|
|
4601
|
+
markInitialized();
|
|
4602
|
+
}
|
|
4603
|
+
else if (dataReceived) {
|
|
4604
|
+
// At least one initializer delivered data. Do not overwrite any
|
|
4605
|
+
// error status that a subsequent failed initializer may have
|
|
4606
|
+
// reported -- the status will be driven by the synchronizers.
|
|
4491
4607
|
markInitialized();
|
|
4492
4608
|
}
|
|
4493
4609
|
}
|
|
@@ -4718,6 +4834,13 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4718
4834
|
let bootstrapped = false;
|
|
4719
4835
|
let closed = false;
|
|
4720
4836
|
let flushCallback;
|
|
4837
|
+
/**
|
|
4838
|
+
* The minimum data availability required before the identify promise
|
|
4839
|
+
* resolves. Maps from the public `waitForNetworkResults` option:
|
|
4840
|
+
* - `'cached'` -- resolve as soon as any data (e.g. from cache) is delivered
|
|
4841
|
+
* - `'fresh'` -- wait for network data with a selector (REFRESHED state)
|
|
4842
|
+
*/
|
|
4843
|
+
let minimumDataAvailability = 'fresh';
|
|
4721
4844
|
// Explicit connection mode override — bypasses transition table entirely.
|
|
4722
4845
|
let connectionModeOverride;
|
|
4723
4846
|
// Forced/automatic streaming state for browser listener-driven streaming.
|
|
@@ -4820,7 +4943,9 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4820
4943
|
? new 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)
|
|
4821
4944
|
: ctx.serviceEndpoints;
|
|
4822
4945
|
const fdv1RequestorFactory = () => makeRequestor(ctx.plainContextString, fallbackServiceEndpoints, fdv1Endpoints.polling(), ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams, config.withReasons, config.useReport);
|
|
4823
|
-
const fdv1SyncFactory =
|
|
4946
|
+
const fdv1SyncFactory = {
|
|
4947
|
+
create: () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger),
|
|
4948
|
+
};
|
|
4824
4949
|
synchronizerSlots.push(createSynchronizerSlot(fdv1SyncFactory, { isFDv1Fallback: true }));
|
|
4825
4950
|
}
|
|
4826
4951
|
return { initializerFactories, synchronizerSlots };
|
|
@@ -4839,11 +4964,21 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4839
4964
|
}
|
|
4840
4965
|
const descriptors = flagEvalPayloadToItemDescriptors(payload.updates ?? []);
|
|
4841
4966
|
// Flag updates and change events happen synchronously inside applyChanges.
|
|
4842
|
-
// The returned promise is only for async cache persistence
|
|
4967
|
+
// The returned promise is only for async cache persistence -- we intentionally
|
|
4843
4968
|
// do not await it so the data source pipeline is not blocked by storage I/O.
|
|
4844
4969
|
flagManager.applyChanges(context, descriptors, payload.type).catch((e) => {
|
|
4845
4970
|
logger.warn(`${logTag} Failed to persist flag cache: ${e}`);
|
|
4846
4971
|
});
|
|
4972
|
+
// When the minimum data availability is 'cached', resolve the identify
|
|
4973
|
+
// promise as soon as any data (e.g. from cache) has been delivered. The
|
|
4974
|
+
// data source continues its initialization pipeline normally --
|
|
4975
|
+
// subsequent changesets update flags and fire change events.
|
|
4976
|
+
if (minimumDataAvailability === 'cached' && !initialized && pendingIdentifyResolve) {
|
|
4977
|
+
initialized = true;
|
|
4978
|
+
pendingIdentifyResolve();
|
|
4979
|
+
pendingIdentifyResolve = undefined;
|
|
4980
|
+
pendingIdentifyReject = undefined;
|
|
4981
|
+
}
|
|
4847
4982
|
}
|
|
4848
4983
|
/**
|
|
4849
4984
|
* Create and start a new FDv2DataSource for the given mode.
|
|
@@ -4950,6 +5085,7 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4950
5085
|
selector = undefined;
|
|
4951
5086
|
initialized = false;
|
|
4952
5087
|
bootstrapped = false;
|
|
5088
|
+
minimumDataAvailability = identifyOptions?.waitForNetworkResults ? 'fresh' : 'cached';
|
|
4953
5089
|
identifiedContext = context;
|
|
4954
5090
|
pendingIdentifyResolve = identifyResolve;
|
|
4955
5091
|
pendingIdentifyReject = identifyReject;
|