@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cjs/LDClientImpl.d.ts +19 -8
  3. package/dist/cjs/LDClientImpl.d.ts.map +1 -1
  4. package/dist/cjs/api/LDStartOptions.d.ts +19 -0
  5. package/dist/cjs/api/LDStartOptions.d.ts.map +1 -0
  6. package/dist/cjs/api/index.d.ts +1 -0
  7. package/dist/cjs/api/index.d.ts.map +1 -1
  8. package/dist/cjs/configuration/Configuration.d.ts +10 -0
  9. package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
  10. package/dist/cjs/context/ensureKey.d.ts.map +1 -1
  11. package/dist/cjs/datasource/FDv2DataManagerBase.d.ts.map +1 -1
  12. package/dist/cjs/datasource/SourceFactoryProvider.d.ts.map +1 -1
  13. package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
  14. package/dist/cjs/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
  15. package/dist/cjs/datasource/fdv2/SourceManager.d.ts +28 -10
  16. package/dist/cjs/datasource/fdv2/SourceManager.d.ts.map +1 -1
  17. package/dist/cjs/index.cjs +231 -95
  18. package/dist/cjs/index.cjs.map +1 -1
  19. package/dist/cjs/index.d.ts +1 -1
  20. package/dist/cjs/index.d.ts.map +1 -1
  21. package/dist/cjs/storage/getOrGenerateKey.d.ts +4 -1
  22. package/dist/cjs/storage/getOrGenerateKey.d.ts.map +1 -1
  23. package/dist/cjs/storage/namespaceUtils.d.ts +3 -6
  24. package/dist/cjs/storage/namespaceUtils.d.ts.map +1 -1
  25. package/dist/esm/LDClientImpl.d.ts +19 -8
  26. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  27. package/dist/esm/api/LDStartOptions.d.ts +19 -0
  28. package/dist/esm/api/LDStartOptions.d.ts.map +1 -0
  29. package/dist/esm/api/index.d.ts +1 -0
  30. package/dist/esm/api/index.d.ts.map +1 -1
  31. package/dist/esm/configuration/Configuration.d.ts +10 -0
  32. package/dist/esm/configuration/Configuration.d.ts.map +1 -1
  33. package/dist/esm/context/ensureKey.d.ts.map +1 -1
  34. package/dist/esm/datasource/FDv2DataManagerBase.d.ts.map +1 -1
  35. package/dist/esm/datasource/SourceFactoryProvider.d.ts.map +1 -1
  36. package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
  37. package/dist/esm/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
  38. package/dist/esm/datasource/fdv2/SourceManager.d.ts +28 -10
  39. package/dist/esm/datasource/fdv2/SourceManager.d.ts.map +1 -1
  40. package/dist/esm/index.d.ts +1 -1
  41. package/dist/esm/index.d.ts.map +1 -1
  42. package/dist/esm/index.mjs +231 -95
  43. package/dist/esm/index.mjs.map +1 -1
  44. package/dist/esm/storage/getOrGenerateKey.d.ts +4 -1
  45. package/dist/esm/storage/getOrGenerateKey.d.ts.map +1 -1
  46. package/dist/esm/storage/namespaceUtils.d.ts +3 -6
  47. package/dist/esm/storage/namespaceUtils.d.ts.map +1 -1
  48. package/package.json +1 -3
@@ -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 (!generatedKey) {
706
- generatedKey = crypto.randomUUID();
707
- await storage?.set(storageKey, generatedKey);
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 prefer {@link namespaceForGeneratedContextKey}. At one time we only generated keys for
731
- * anonymous contexts and they were namespaced in LaunchDarkly_AnonymousKeys. Eventually we started
732
- * generating context keys for non-anonymous contexts such as for the Auto Environment Attributes
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 namespaceForAnonymousGeneratedContextKey(kind);
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.promiseWithTimeout(this.initializedPromise, timeout);
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.promiseWithTimeout(this.initializedPromise, timeout);
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
- promiseWithTimeout(basePromise, timeout) {
2219
- const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout, 'waitForInitialization');
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 interrupted(errorInfoFromUnknown('No storage available'), false);
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 interrupted(errorInfoFromUnknown('Cache miss'), false);
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
- // The selectorGetter is ignored — cache data has no selector.
3250
- return (_selectorGetter) => {
3251
- let shutdownResolve;
3252
- const shutdownPromise = new Promise((resolve) => {
3253
- shutdownResolve = resolve;
3254
- });
3255
- return {
3256
- async run() {
3257
- return Promise.race([shutdownPromise, loadFromCache(config)]);
3258
- },
3259
- close() {
3260
- shutdownResolve?.(shutdown());
3261
- shutdownResolve = undefined;
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 (sg) => createPollingInitializer(requestor, ctx.logger, sg);
4216
+ return {
4217
+ create: (sg) => createPollingInitializer(requestor, ctx.logger, sg),
4218
+ };
4142
4219
  }
4143
4220
  case 'streaming':
4144
- return (sg) => createStreamingInitializer(buildStreamingBase(entry, ctx, sg));
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
- const factory = (sg) => createPollingSynchronizer(requestor, ctx.logger, sg, intervalMs);
4163
- return createSynchronizerSlot(factory);
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 data was received
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 initialization is complete.
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 (dataReceived) {
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 = () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger);
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 we intentionally
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;