@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/cjs/index.cjs
CHANGED
|
@@ -698,13 +698,28 @@ async function digest(hasher, encoding) {
|
|
|
698
698
|
* @param storageKey keyed storage location where the generated key should live. See {@link namespaceForGeneratedContextKey}
|
|
699
699
|
* for related exmaples of generating a storage key and usage.
|
|
700
700
|
* @param platform crypto and storage implementations for necessary operations
|
|
701
|
+
* @param legacyStorageKey optional legacy storage key to migrate from. If the key is not found
|
|
702
|
+
* under {@link storageKey} but exists under this legacy key, it will be migrated to the new
|
|
703
|
+
* location and the legacy key will be cleared.
|
|
701
704
|
* @returns the generated key
|
|
702
705
|
*/
|
|
703
|
-
const getOrGenerateKey = async (storageKey, { crypto, storage }) => {
|
|
706
|
+
const getOrGenerateKey = async (storageKey, { crypto, storage }, legacyStorageKey) => {
|
|
704
707
|
let generatedKey = await storage?.get(storageKey);
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
+
if (generatedKey == null) {
|
|
709
|
+
if (legacyStorageKey) {
|
|
710
|
+
generatedKey = await storage?.get(legacyStorageKey);
|
|
711
|
+
if (generatedKey != null) {
|
|
712
|
+
await storage?.set(storageKey, generatedKey);
|
|
713
|
+
const verified = await storage?.get(storageKey);
|
|
714
|
+
if (verified != null) {
|
|
715
|
+
await storage?.clear(legacyStorageKey);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (generatedKey == null) {
|
|
720
|
+
generatedKey = crypto.randomUUID();
|
|
721
|
+
await storage?.set(storageKey, generatedKey);
|
|
722
|
+
}
|
|
708
723
|
}
|
|
709
724
|
return generatedKey;
|
|
710
725
|
};
|
|
@@ -727,12 +742,9 @@ async function namespaceForEnvironment(crypto, sdkKey) {
|
|
|
727
742
|
]);
|
|
728
743
|
}
|
|
729
744
|
/**
|
|
730
|
-
* @deprecated
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
* feature and those were namespaced in LaunchDarkly_ContextKeys. This function can be removed
|
|
734
|
-
* when the data under the LaunchDarkly_AnonymousKeys namespace is merged with data under the
|
|
735
|
-
* LaunchDarkly_ContextKeys namespace.
|
|
745
|
+
* @deprecated Used only for migration in ensureKey. Data stored under LaunchDarkly_AnonymousKeys
|
|
746
|
+
* is now migrated to LaunchDarkly_ContextKeys on first access. This function can be removed once
|
|
747
|
+
* all clients have had the chance to run the migration.
|
|
736
748
|
*/
|
|
737
749
|
async function namespaceForAnonymousGeneratedContextKey(kind) {
|
|
738
750
|
return concatNamespacesAndValues([
|
|
@@ -915,10 +927,11 @@ const { isLegacyUser, isMultiKind, isSingleKind } = jsSdkCommon.internal;
|
|
|
915
927
|
const ensureKeyCommon = async (kind, c, platform) => {
|
|
916
928
|
const { anonymous, key } = c;
|
|
917
929
|
if (anonymous && !key) {
|
|
918
|
-
const storageKey = await
|
|
930
|
+
const storageKey = await namespaceForGeneratedContextKey(kind);
|
|
931
|
+
const legacyStorageKey = await namespaceForAnonymousGeneratedContextKey(kind);
|
|
919
932
|
// This mutates a cloned copy of the original context from ensureyKey so this is safe.
|
|
920
933
|
// eslint-disable-next-line no-param-reassign
|
|
921
|
-
c.key = await getOrGenerateKey(storageKey, platform);
|
|
934
|
+
c.key = await getOrGenerateKey(storageKey, platform, legacyStorageKey);
|
|
922
935
|
}
|
|
923
936
|
};
|
|
924
937
|
const ensureKeySingle = async (c, platform) => {
|
|
@@ -1017,6 +1030,46 @@ class EventFactory extends jsSdkCommon.internal.EventFactoryBase {
|
|
|
1017
1030
|
}
|
|
1018
1031
|
}
|
|
1019
1032
|
|
|
1033
|
+
function readFlagsFromBootstrap(logger, data) {
|
|
1034
|
+
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
1035
|
+
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
1036
|
+
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
1037
|
+
const keys = Object.keys(data);
|
|
1038
|
+
const metadataKey = '$flagsState';
|
|
1039
|
+
const validKey = '$valid';
|
|
1040
|
+
const metadata = data[metadataKey];
|
|
1041
|
+
if (!metadata && keys.length) {
|
|
1042
|
+
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
1043
|
+
' metadata. Events may not be sent correctly.');
|
|
1044
|
+
}
|
|
1045
|
+
if (data[validKey] === false) {
|
|
1046
|
+
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
1047
|
+
}
|
|
1048
|
+
const ret = {};
|
|
1049
|
+
keys.forEach((key) => {
|
|
1050
|
+
if (key !== metadataKey && key !== validKey) {
|
|
1051
|
+
let flag;
|
|
1052
|
+
if (metadata && metadata[key]) {
|
|
1053
|
+
flag = {
|
|
1054
|
+
value: data[key],
|
|
1055
|
+
...metadata[key],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
flag = {
|
|
1060
|
+
value: data[key],
|
|
1061
|
+
version: 0,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
ret[key] = {
|
|
1065
|
+
version: flag.version,
|
|
1066
|
+
flag,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
return ret;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1020
1073
|
/**
|
|
1021
1074
|
* Suffix appended to context storage keys to form the freshness storage key.
|
|
1022
1075
|
*/
|
|
@@ -1945,6 +1998,10 @@ class LDClientImpl {
|
|
|
1945
1998
|
this._eventFactoryWithReasons = new EventFactory(true);
|
|
1946
1999
|
this._eventSendingEnabled = false;
|
|
1947
2000
|
this._identifyQueue = createAsyncTaskQueue();
|
|
2001
|
+
// NOTE: this is used to ease the transition to the new initialization pattern.
|
|
2002
|
+
// All client SDKs should set this to true with the exception of React Native which is
|
|
2003
|
+
// still using the deprecated construct + identify pattern.
|
|
2004
|
+
this._requiresStart = false;
|
|
1948
2005
|
if (!sdkKey) {
|
|
1949
2006
|
throw new Error('You must configure the client with a client-side SDK key');
|
|
1950
2007
|
}
|
|
@@ -1953,6 +2010,8 @@ class LDClientImpl {
|
|
|
1953
2010
|
}
|
|
1954
2011
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1955
2012
|
this.logger = this._config.logger;
|
|
2013
|
+
this._requiresStart = internalOptions?.requiresStart ?? false;
|
|
2014
|
+
this.initialContext = internalOptions?.initialContext;
|
|
1956
2015
|
this._baseHeaders = jsSdkCommon.defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1957
2016
|
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1958
2017
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
@@ -1970,6 +2029,8 @@ class LDClientImpl {
|
|
|
1970
2029
|
});
|
|
1971
2030
|
});
|
|
1972
2031
|
this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
|
|
2032
|
+
this.isFDv2 = !!this._config.dataSystem;
|
|
2033
|
+
this.dataSystemConfig = this._config.dataSystem;
|
|
1973
2034
|
const hooks = [...this._config.hooks];
|
|
1974
2035
|
this.environmentMetadata = createPluginEnvironmentMetadata(this.sdkKey, this.platform, this._config);
|
|
1975
2036
|
this._config.getImplementationHooks(this.environmentMetadata).forEach((hook) => {
|
|
@@ -2047,6 +2108,57 @@ class LDClientImpl {
|
|
|
2047
2108
|
presetFlags(newFlags) {
|
|
2048
2109
|
this._flagManager.presetFlags(newFlags);
|
|
2049
2110
|
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Starts the client and returns a promise that resolves to the initialization result.
|
|
2113
|
+
*
|
|
2114
|
+
* This method is idempotent - calling it multiple times returns the same promise.
|
|
2115
|
+
*
|
|
2116
|
+
* @param options Optional configuration. See {@link LDStartOptions}.
|
|
2117
|
+
* @returns A promise that resolves to the initialization result.
|
|
2118
|
+
*/
|
|
2119
|
+
start(options) {
|
|
2120
|
+
if (this.initializeResult) {
|
|
2121
|
+
return Promise.resolve(this.initializeResult);
|
|
2122
|
+
}
|
|
2123
|
+
if (this.startPromise) {
|
|
2124
|
+
return this.startPromise;
|
|
2125
|
+
}
|
|
2126
|
+
if (!this.initialContext) {
|
|
2127
|
+
this.logger.error('Initial context not set');
|
|
2128
|
+
return Promise.resolve({ status: 'failed', error: new Error('Initial context not set') });
|
|
2129
|
+
}
|
|
2130
|
+
const identifyOptions = {
|
|
2131
|
+
...(options?.identifyOptions ?? {}),
|
|
2132
|
+
// Initial identify operations are not sheddable.
|
|
2133
|
+
sheddable: false,
|
|
2134
|
+
};
|
|
2135
|
+
// If the bootstrap data is provided in the start options, and the identify options do not
|
|
2136
|
+
// have bootstrap data, then use the bootstrap data from the start options.
|
|
2137
|
+
if (options?.bootstrap && !identifyOptions.bootstrap) {
|
|
2138
|
+
identifyOptions.bootstrap = options.bootstrap;
|
|
2139
|
+
}
|
|
2140
|
+
if (identifyOptions.bootstrap) {
|
|
2141
|
+
try {
|
|
2142
|
+
if (!identifyOptions.bootstrapParsed) {
|
|
2143
|
+
identifyOptions.bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap);
|
|
2144
|
+
}
|
|
2145
|
+
if (identifyOptions.bootstrapParsed) {
|
|
2146
|
+
this.presetFlags(identifyOptions.bootstrapParsed);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
catch (error) {
|
|
2150
|
+
this.logger.error('Failed to bootstrap data', error);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (!this.initializedPromise) {
|
|
2154
|
+
this.initializedPromise = new Promise((resolve) => {
|
|
2155
|
+
this.initResolve = resolve;
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
this.startPromise = this._promiseWithTimeout(this.initializedPromise, options?.timeout ?? 5, 'start');
|
|
2159
|
+
this.identifyResult(this.initialContext, identifyOptions);
|
|
2160
|
+
return this.startPromise;
|
|
2161
|
+
}
|
|
2050
2162
|
/**
|
|
2051
2163
|
* Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
|
|
2052
2164
|
*
|
|
@@ -2084,6 +2196,10 @@ class LDClientImpl {
|
|
|
2084
2196
|
// If completed or shed, then we are done.
|
|
2085
2197
|
}
|
|
2086
2198
|
async identifyResult(pristineContext, identifyOptions) {
|
|
2199
|
+
if (this._requiresStart && !this.startPromise) {
|
|
2200
|
+
this.logger.error('The client must be started before a context can be identified. Call start() prior to identifying a context.');
|
|
2201
|
+
return { status: 'error', error: new Error('Identify called before start') };
|
|
2202
|
+
}
|
|
2087
2203
|
const identifyTimeout = identifyOptions?.timeout ?? DEFAULT_IDENTIFY_TIMEOUT_SECONDS;
|
|
2088
2204
|
const noTimeout = identifyOptions?.timeout === undefined && identifyOptions?.noTimeout === true;
|
|
2089
2205
|
// When noTimeout is specified, and a timeout is not specified, then this condition cannot
|
|
@@ -2193,7 +2309,7 @@ class LDClientImpl {
|
|
|
2193
2309
|
// If waitForInitialization was previously called, then return the promise with a timeout.
|
|
2194
2310
|
// This condition should only be triggered if waitForInitialization was called multiple times.
|
|
2195
2311
|
if (this.initializedPromise) {
|
|
2196
|
-
return this.
|
|
2312
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2197
2313
|
}
|
|
2198
2314
|
// Create a new promise for tracking initialization
|
|
2199
2315
|
if (!this.initializedPromise) {
|
|
@@ -2201,22 +2317,18 @@ class LDClientImpl {
|
|
|
2201
2317
|
this.initResolve = resolve;
|
|
2202
2318
|
});
|
|
2203
2319
|
}
|
|
2204
|
-
return this.
|
|
2320
|
+
return this._promiseWithTimeout(this.initializedPromise, timeout);
|
|
2205
2321
|
}
|
|
2206
2322
|
/**
|
|
2207
|
-
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2323
|
+
* Apply a timeout promise to a base promise. This is for use with waitForInitialization
|
|
2324
|
+
* and start.
|
|
2208
2325
|
*
|
|
2209
2326
|
* @param basePromise The promise to race against a timeout.
|
|
2210
2327
|
* @param timeout The timeout in seconds.
|
|
2211
2328
|
* @returns A promise that resolves to the initialization result or timeout.
|
|
2212
|
-
*
|
|
2213
|
-
* @privateRemarks
|
|
2214
|
-
* This method is protected because it is used by the browser SDK's `start` method.
|
|
2215
|
-
* Eventually, the start method will be moved to this common implementation and this method will
|
|
2216
|
-
* be made private.
|
|
2217
2329
|
*/
|
|
2218
|
-
|
|
2219
|
-
const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout,
|
|
2330
|
+
_promiseWithTimeout(basePromise, timeout, label = 'waitForInitialization') {
|
|
2331
|
+
const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout, label);
|
|
2220
2332
|
return Promise.race([
|
|
2221
2333
|
basePromise.then((res) => {
|
|
2222
2334
|
cancelableTimeout.cancel();
|
|
@@ -2446,46 +2558,6 @@ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
|
|
|
2446
2558
|
});
|
|
2447
2559
|
}
|
|
2448
2560
|
|
|
2449
|
-
function readFlagsFromBootstrap(logger, data) {
|
|
2450
|
-
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
|
|
2451
|
-
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
|
|
2452
|
-
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
|
|
2453
|
-
const keys = Object.keys(data);
|
|
2454
|
-
const metadataKey = '$flagsState';
|
|
2455
|
-
const validKey = '$valid';
|
|
2456
|
-
const metadata = data[metadataKey];
|
|
2457
|
-
if (!metadata && keys.length) {
|
|
2458
|
-
logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
|
|
2459
|
-
' metadata. Events may not be sent correctly.');
|
|
2460
|
-
}
|
|
2461
|
-
if (data[validKey] === false) {
|
|
2462
|
-
logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
|
|
2463
|
-
}
|
|
2464
|
-
const ret = {};
|
|
2465
|
-
keys.forEach((key) => {
|
|
2466
|
-
if (key !== metadataKey && key !== validKey) {
|
|
2467
|
-
let flag;
|
|
2468
|
-
if (metadata && metadata[key]) {
|
|
2469
|
-
flag = {
|
|
2470
|
-
value: data[key],
|
|
2471
|
-
...metadata[key],
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
else {
|
|
2475
|
-
flag = {
|
|
2476
|
-
value: data[key],
|
|
2477
|
-
version: 0,
|
|
2478
|
-
};
|
|
2479
|
-
}
|
|
2480
|
-
ret[key] = {
|
|
2481
|
-
version: flag.version,
|
|
2482
|
-
flag,
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2486
|
-
return ret;
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
2561
|
/**
|
|
2490
2562
|
* Creates endpoint paths for browser (client-side ID) FDv1 evaluation.
|
|
2491
2563
|
*
|
|
@@ -3208,12 +3280,12 @@ async function loadFromCache(config) {
|
|
|
3208
3280
|
const { storage, crypto, environmentNamespace, context, logger } = config;
|
|
3209
3281
|
if (!storage) {
|
|
3210
3282
|
logger?.debug('No storage available for cache initializer');
|
|
3211
|
-
return
|
|
3283
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3212
3284
|
}
|
|
3213
3285
|
const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
|
|
3214
3286
|
if (!cached) {
|
|
3215
3287
|
logger?.debug('Cache miss for context');
|
|
3216
|
-
return
|
|
3288
|
+
return changeSet({ version: 0, type: 'none', updates: [] }, false);
|
|
3217
3289
|
}
|
|
3218
3290
|
const updates = Object.entries(cached.flags).map(([key, flag]) => ({
|
|
3219
3291
|
kind: 'flag-eval',
|
|
@@ -3246,21 +3318,24 @@ async function loadFromCache(config) {
|
|
|
3246
3318
|
* @internal
|
|
3247
3319
|
*/
|
|
3248
3320
|
function createCacheInitializerFactory(config) {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
shutdownResolve
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3321
|
+
return {
|
|
3322
|
+
isCache: true,
|
|
3323
|
+
// The selectorGetter is ignored -- cache data has no selector.
|
|
3324
|
+
create(_selectorGetter) {
|
|
3325
|
+
let shutdownResolve;
|
|
3326
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
3327
|
+
shutdownResolve = resolve;
|
|
3328
|
+
});
|
|
3329
|
+
return {
|
|
3330
|
+
async run() {
|
|
3331
|
+
return Promise.race([shutdownPromise, loadFromCache(config)]);
|
|
3332
|
+
},
|
|
3333
|
+
close() {
|
|
3334
|
+
shutdownResolve?.(shutdown());
|
|
3335
|
+
shutdownResolve = undefined;
|
|
3336
|
+
},
|
|
3337
|
+
};
|
|
3338
|
+
},
|
|
3264
3339
|
};
|
|
3265
3340
|
}
|
|
3266
3341
|
|
|
@@ -3728,7 +3803,7 @@ function createSourceManager(initializerFactories, synchronizerSlots, selectorGe
|
|
|
3728
3803
|
return undefined;
|
|
3729
3804
|
}
|
|
3730
3805
|
closeActiveSource();
|
|
3731
|
-
const initializer = initializerFactories[initializerIndex](selectorGetter);
|
|
3806
|
+
const initializer = initializerFactories[initializerIndex].create(selectorGetter);
|
|
3732
3807
|
activeSource = initializer;
|
|
3733
3808
|
return initializer;
|
|
3734
3809
|
},
|
|
@@ -3748,7 +3823,7 @@ function createSourceManager(initializerFactories, synchronizerSlots, selectorGe
|
|
|
3748
3823
|
const candidate = synchronizerSlots[synchronizerIndex];
|
|
3749
3824
|
if (candidate.state === 'available') {
|
|
3750
3825
|
closeActiveSource();
|
|
3751
|
-
const synchronizer = candidate.factory(selectorGetter);
|
|
3826
|
+
const synchronizer = candidate.factory.create(selectorGetter);
|
|
3752
3827
|
activeSource = synchronizer;
|
|
3753
3828
|
return synchronizer;
|
|
3754
3829
|
}
|
|
@@ -4138,10 +4213,14 @@ function createDefaultSourceFactoryProvider() {
|
|
|
4138
4213
|
switch (entry.type) {
|
|
4139
4214
|
case 'polling': {
|
|
4140
4215
|
const requestor = resolvePollingRequestor(ctx, entry.endpoints);
|
|
4141
|
-
return
|
|
4216
|
+
return {
|
|
4217
|
+
create: (sg) => createPollingInitializer(requestor, ctx.logger, sg),
|
|
4218
|
+
};
|
|
4142
4219
|
}
|
|
4143
4220
|
case 'streaming':
|
|
4144
|
-
return
|
|
4221
|
+
return {
|
|
4222
|
+
create: (sg) => createStreamingInitializer(buildStreamingBase(entry, ctx, sg)),
|
|
4223
|
+
};
|
|
4145
4224
|
case 'cache':
|
|
4146
4225
|
return createCacheInitializerFactory({
|
|
4147
4226
|
storage: ctx.storage,
|
|
@@ -4159,13 +4238,14 @@ function createDefaultSourceFactoryProvider() {
|
|
|
4159
4238
|
case 'polling': {
|
|
4160
4239
|
const intervalMs = (entry.pollInterval ?? ctx.polling.intervalSeconds) * 1000;
|
|
4161
4240
|
const requestor = resolvePollingRequestor(ctx, entry.endpoints);
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
case 'streaming': {
|
|
4166
|
-
const factory = (sg) => createStreamingSynchronizer(buildStreamingBase(entry, ctx, sg));
|
|
4167
|
-
return createSynchronizerSlot(factory);
|
|
4241
|
+
return createSynchronizerSlot({
|
|
4242
|
+
create: (sg) => createPollingSynchronizer(requestor, ctx.logger, sg, intervalMs),
|
|
4243
|
+
});
|
|
4168
4244
|
}
|
|
4245
|
+
case 'streaming':
|
|
4246
|
+
return createSynchronizerSlot({
|
|
4247
|
+
create: (sg) => createStreamingSynchronizer(buildStreamingBase(entry, ctx, sg)),
|
|
4248
|
+
});
|
|
4169
4249
|
default:
|
|
4170
4250
|
return undefined;
|
|
4171
4251
|
}
|
|
@@ -4436,6 +4516,15 @@ function createFDv2DataSource(config) {
|
|
|
4436
4516
|
let dataReceived = false;
|
|
4437
4517
|
let initResolve;
|
|
4438
4518
|
let initReject;
|
|
4519
|
+
// When every initializer is a cache initializer and there are no
|
|
4520
|
+
// synchronizers, the cache is the only possible data source. A cache miss
|
|
4521
|
+
// in that configuration must not fail initialization -- there is nowhere
|
|
4522
|
+
// else for data to come from, and reporting an error would be meaningless.
|
|
4523
|
+
// Mirrors the Android SDK's InitializerFromCache / hasAvailableSources
|
|
4524
|
+
// behavior.
|
|
4525
|
+
const cacheOnlyDataSystem = initializerFactories.length > 0 &&
|
|
4526
|
+
initializerFactories.every((f) => f.isCache === true) &&
|
|
4527
|
+
synchronizerSlots.length === 0;
|
|
4439
4528
|
const sourceManager = createSourceManager(initializerFactories, synchronizerSlots, selectorGetter);
|
|
4440
4529
|
function markInitialized() {
|
|
4441
4530
|
if (!initialized) {
|
|
@@ -4465,6 +4554,10 @@ function createFDv2DataSource(config) {
|
|
|
4465
4554
|
// The orchestration loops intentionally use await-in-loop for sequential
|
|
4466
4555
|
// state machine processing — one result at a time.
|
|
4467
4556
|
async function runInitializers() {
|
|
4557
|
+
// Tracks whether any initializer reported interrupted/terminal_error.
|
|
4558
|
+
// Used below so the cache-only exhaustion branch does not overwrite
|
|
4559
|
+
// that error status with VALID.
|
|
4560
|
+
let errorReportedDuringInit = false;
|
|
4468
4561
|
while (!closed) {
|
|
4469
4562
|
const initializer = sourceManager.getNextInitializerAndSetActive();
|
|
4470
4563
|
if (initializer === undefined) {
|
|
@@ -4474,16 +4567,16 @@ function createFDv2DataSource(config) {
|
|
|
4474
4567
|
if (closed) {
|
|
4475
4568
|
return;
|
|
4476
4569
|
}
|
|
4477
|
-
if (result.type === 'changeSet') {
|
|
4570
|
+
if (result.type === 'changeSet' && result.payload.type !== 'none') {
|
|
4478
4571
|
applyChangeSet(result);
|
|
4479
4572
|
if (handleFdv1Fallback(result)) {
|
|
4480
|
-
// FDv1 fallback triggered during initialization
|
|
4573
|
+
// FDv1 fallback triggered during initialization -- data was received
|
|
4481
4574
|
// but we should move to synchronizers where the FDv1 adapter will run.
|
|
4482
4575
|
dataReceived = true;
|
|
4483
4576
|
break;
|
|
4484
4577
|
}
|
|
4485
4578
|
if (result.payload.state) {
|
|
4486
|
-
// Got basis data with a selector
|
|
4579
|
+
// Got basis data with a selector -- initialization is complete.
|
|
4487
4580
|
markInitialized();
|
|
4488
4581
|
return;
|
|
4489
4582
|
}
|
|
@@ -4497,6 +4590,7 @@ function createFDv2DataSource(config) {
|
|
|
4497
4590
|
case 'terminal_error':
|
|
4498
4591
|
logger?.warn(`Initializer failed: ${result.errorInfo?.message ?? 'unknown error'}`);
|
|
4499
4592
|
reportStatusError(result);
|
|
4593
|
+
errorReportedDuringInit = true;
|
|
4500
4594
|
break;
|
|
4501
4595
|
case 'shutdown':
|
|
4502
4596
|
return;
|
|
@@ -4504,8 +4598,30 @@ function createFDv2DataSource(config) {
|
|
|
4504
4598
|
handleFdv1Fallback(result);
|
|
4505
4599
|
}
|
|
4506
4600
|
}
|
|
4601
|
+
// close() between the last loop iteration and the exhaustion branch.
|
|
4602
|
+
// Exit without marking initialized or emitting a spurious VALID; the
|
|
4603
|
+
// start() promise will be rejected by the post-orchestration handler
|
|
4604
|
+
// with "closed before initialization completed."
|
|
4605
|
+
if (closed) {
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4507
4608
|
// All initializers exhausted.
|
|
4508
|
-
if (
|
|
4609
|
+
if (cacheOnlyDataSystem) {
|
|
4610
|
+
// Cache-only data system with no synchronizer to produce a VALID
|
|
4611
|
+
// status on its own. On a cache miss with no errors, nothing else
|
|
4612
|
+
// has asserted VALID yet, so do it here. Skip the update if:
|
|
4613
|
+
// - dataReceived (cache hit): applyChangeSet already asserted VALID.
|
|
4614
|
+
// - errorReportedDuringInit: reportError set an error status that
|
|
4615
|
+
// must not be silently overwritten.
|
|
4616
|
+
if (!dataReceived && !errorReportedDuringInit) {
|
|
4617
|
+
statusManager.requestStateUpdate('VALID');
|
|
4618
|
+
}
|
|
4619
|
+
markInitialized();
|
|
4620
|
+
}
|
|
4621
|
+
else if (dataReceived) {
|
|
4622
|
+
// At least one initializer delivered data. Do not overwrite any
|
|
4623
|
+
// error status that a subsequent failed initializer may have
|
|
4624
|
+
// reported -- the status will be driven by the synchronizers.
|
|
4509
4625
|
markInitialized();
|
|
4510
4626
|
}
|
|
4511
4627
|
}
|
|
@@ -4736,6 +4852,13 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4736
4852
|
let bootstrapped = false;
|
|
4737
4853
|
let closed = false;
|
|
4738
4854
|
let flushCallback;
|
|
4855
|
+
/**
|
|
4856
|
+
* The minimum data availability required before the identify promise
|
|
4857
|
+
* resolves. Maps from the public `waitForNetworkResults` option:
|
|
4858
|
+
* - `'cached'` -- resolve as soon as any data (e.g. from cache) is delivered
|
|
4859
|
+
* - `'fresh'` -- wait for network data with a selector (REFRESHED state)
|
|
4860
|
+
*/
|
|
4861
|
+
let minimumDataAvailability = 'fresh';
|
|
4739
4862
|
// Explicit connection mode override — bypasses transition table entirely.
|
|
4740
4863
|
let connectionModeOverride;
|
|
4741
4864
|
// Forced/automatic streaming state for browser listener-driven streaming.
|
|
@@ -4838,7 +4961,9 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4838
4961
|
? 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)
|
|
4839
4962
|
: ctx.serviceEndpoints;
|
|
4840
4963
|
const fdv1RequestorFactory = () => makeRequestor(ctx.plainContextString, fallbackServiceEndpoints, fdv1Endpoints.polling(), ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams, config.withReasons, config.useReport);
|
|
4841
|
-
const fdv1SyncFactory =
|
|
4964
|
+
const fdv1SyncFactory = {
|
|
4965
|
+
create: () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger),
|
|
4966
|
+
};
|
|
4842
4967
|
synchronizerSlots.push(createSynchronizerSlot(fdv1SyncFactory, { isFDv1Fallback: true }));
|
|
4843
4968
|
}
|
|
4844
4969
|
return { initializerFactories, synchronizerSlots };
|
|
@@ -4857,11 +4982,21 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4857
4982
|
}
|
|
4858
4983
|
const descriptors = flagEvalPayloadToItemDescriptors(payload.updates ?? []);
|
|
4859
4984
|
// Flag updates and change events happen synchronously inside applyChanges.
|
|
4860
|
-
// The returned promise is only for async cache persistence
|
|
4985
|
+
// The returned promise is only for async cache persistence -- we intentionally
|
|
4861
4986
|
// do not await it so the data source pipeline is not blocked by storage I/O.
|
|
4862
4987
|
flagManager.applyChanges(context, descriptors, payload.type).catch((e) => {
|
|
4863
4988
|
logger.warn(`${logTag} Failed to persist flag cache: ${e}`);
|
|
4864
4989
|
});
|
|
4990
|
+
// When the minimum data availability is 'cached', resolve the identify
|
|
4991
|
+
// promise as soon as any data (e.g. from cache) has been delivered. The
|
|
4992
|
+
// data source continues its initialization pipeline normally --
|
|
4993
|
+
// subsequent changesets update flags and fire change events.
|
|
4994
|
+
if (minimumDataAvailability === 'cached' && !initialized && pendingIdentifyResolve) {
|
|
4995
|
+
initialized = true;
|
|
4996
|
+
pendingIdentifyResolve();
|
|
4997
|
+
pendingIdentifyResolve = undefined;
|
|
4998
|
+
pendingIdentifyReject = undefined;
|
|
4999
|
+
}
|
|
4865
5000
|
}
|
|
4866
5001
|
/**
|
|
4867
5002
|
* Create and start a new FDv2DataSource for the given mode.
|
|
@@ -4968,6 +5103,7 @@ function createFDv2DataManagerBase(baseConfig) {
|
|
|
4968
5103
|
selector = undefined;
|
|
4969
5104
|
initialized = false;
|
|
4970
5105
|
bootstrapped = false;
|
|
5106
|
+
minimumDataAvailability = identifyOptions?.waitForNetworkResults ? 'fresh' : 'cached';
|
|
4971
5107
|
identifiedContext = context;
|
|
4972
5108
|
pendingIdentifyResolve = identifyResolve;
|
|
4973
5109
|
pendingIdentifyReject = identifyReject;
|