@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs/DataManager.d.ts +9 -0
  3. package/dist/cjs/DataManager.d.ts.map +1 -1
  4. package/dist/cjs/LDClientImpl.d.ts +19 -8
  5. package/dist/cjs/LDClientImpl.d.ts.map +1 -1
  6. package/dist/cjs/api/LDOptions.d.ts +17 -5
  7. package/dist/cjs/api/LDOptions.d.ts.map +1 -1
  8. package/dist/cjs/api/LDStartOptions.d.ts +19 -0
  9. package/dist/cjs/api/LDStartOptions.d.ts.map +1 -0
  10. package/dist/cjs/api/datasource/DataSourceEntry.d.ts +46 -0
  11. package/dist/cjs/api/datasource/DataSourceEntry.d.ts.map +1 -1
  12. package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts +5 -0
  13. package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
  14. package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
  15. package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  16. package/dist/cjs/api/datasource/ModeDefinition.d.ts +17 -1
  17. package/dist/cjs/api/datasource/ModeDefinition.d.ts.map +1 -1
  18. package/dist/cjs/api/datasource/index.d.ts +2 -2
  19. package/dist/cjs/api/datasource/index.d.ts.map +1 -1
  20. package/dist/cjs/api/index.d.ts +1 -0
  21. package/dist/cjs/api/index.d.ts.map +1 -1
  22. package/dist/cjs/configuration/Configuration.d.ts +13 -3
  23. package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
  24. package/dist/cjs/configuration/validateOptions.d.ts +18 -2
  25. package/dist/cjs/configuration/validateOptions.d.ts.map +1 -1
  26. package/dist/cjs/configuration/validators.d.ts +1 -1
  27. package/dist/cjs/configuration/validators.d.ts.map +1 -1
  28. package/dist/cjs/datasource/ConnectionModeConfig.d.ts +4 -2
  29. package/dist/cjs/datasource/ConnectionModeConfig.d.ts.map +1 -1
  30. package/dist/cjs/datasource/FDv2DataManagerBase.d.ts +87 -0
  31. package/dist/cjs/datasource/FDv2DataManagerBase.d.ts.map +1 -0
  32. package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts +37 -3
  33. package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  34. package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
  35. package/dist/cjs/index.cjs +1295 -104
  36. package/dist/cjs/index.cjs.map +1 -1
  37. package/dist/cjs/index.d.ts +8 -3
  38. package/dist/cjs/index.d.ts.map +1 -1
  39. package/dist/esm/DataManager.d.ts +9 -0
  40. package/dist/esm/DataManager.d.ts.map +1 -1
  41. package/dist/esm/LDClientImpl.d.ts +19 -8
  42. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  43. package/dist/esm/api/LDOptions.d.ts +17 -5
  44. package/dist/esm/api/LDOptions.d.ts.map +1 -1
  45. package/dist/esm/api/LDStartOptions.d.ts +19 -0
  46. package/dist/esm/api/LDStartOptions.d.ts.map +1 -0
  47. package/dist/esm/api/datasource/DataSourceEntry.d.ts +46 -0
  48. package/dist/esm/api/datasource/DataSourceEntry.d.ts.map +1 -1
  49. package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts +5 -0
  50. package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
  51. package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
  52. package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  53. package/dist/esm/api/datasource/ModeDefinition.d.ts +17 -1
  54. package/dist/esm/api/datasource/ModeDefinition.d.ts.map +1 -1
  55. package/dist/esm/api/datasource/index.d.ts +2 -2
  56. package/dist/esm/api/datasource/index.d.ts.map +1 -1
  57. package/dist/esm/api/index.d.ts +1 -0
  58. package/dist/esm/api/index.d.ts.map +1 -1
  59. package/dist/esm/configuration/Configuration.d.ts +13 -3
  60. package/dist/esm/configuration/Configuration.d.ts.map +1 -1
  61. package/dist/esm/configuration/validateOptions.d.ts +18 -2
  62. package/dist/esm/configuration/validateOptions.d.ts.map +1 -1
  63. package/dist/esm/configuration/validators.d.ts +1 -1
  64. package/dist/esm/configuration/validators.d.ts.map +1 -1
  65. package/dist/esm/datasource/ConnectionModeConfig.d.ts +4 -2
  66. package/dist/esm/datasource/ConnectionModeConfig.d.ts.map +1 -1
  67. package/dist/esm/datasource/FDv2DataManagerBase.d.ts +87 -0
  68. package/dist/esm/datasource/FDv2DataManagerBase.d.ts.map +1 -0
  69. package/dist/esm/datasource/LDClientDataSystemOptions.d.ts +37 -3
  70. package/dist/esm/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  71. package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -1
  72. package/dist/esm/index.d.ts +8 -3
  73. package/dist/esm/index.d.ts.map +1 -1
  74. package/dist/esm/index.mjs +1294 -106
  75. package/dist/esm/index.mjs.map +1 -1
  76. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { getPollingUri, isNullish, TypeValidators, OptionMessages, NumberWithMinimum, createSafeLogger, ServiceEndpoints, ApplicationTags, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, cancelableTimedPromise, LDClientError, base64UrlEncode, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
1
+ import { getPollingUri, isNullish, TypeValidators, OptionMessages, NumberWithMinimum, createSafeLogger, ServiceEndpoints, ApplicationTags, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, cancelableTimedPromise, LDClientError, base64UrlEncode, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError, sleep } from '@launchdarkly/js-sdk-common';
2
2
  export * from '@launchdarkly/js-sdk-common';
3
3
  import * as jsSdkCommon from '@launchdarkly/js-sdk-common';
4
4
  export { jsSdkCommon as platform };
@@ -275,10 +275,18 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
275
275
  * Creates a validator for nested objects. When used in a validator map,
276
276
  * `validateOptions` will recursively validate the nested object's properties.
277
277
  * Defaults for nested fields are passed through from the parent.
278
+ *
279
+ * @param validators - Validator map for the nested object's fields.
280
+ * @param options - Optional configuration.
281
+ * @param options.defaults - Built-in defaults for nested fields.
282
+ * @param options.is - Custom `is` predicate. When provided, replaces the
283
+ * default "is object" check. Use this to discriminate between object shapes
284
+ * in an `anyOf` (e.g., matching on a `type` discriminant field).
278
285
  */
279
- function validatorOf(validators, builtInDefaults) {
286
+ function validatorOf(validators, options) {
287
+ const builtInDefaults = options?.defaults;
280
288
  return {
281
- is: (u) => TypeValidators.Object.is(u),
289
+ is: options?.is ?? ((u) => TypeValidators.Object.is(u)),
282
290
  getType: () => 'object',
283
291
  validate(value, name, logger, defaults) {
284
292
  if (!TypeValidators.Object.is(value)) {
@@ -386,6 +394,10 @@ function anyOf(...validators) {
386
394
  *
387
395
  * @param keyValidator - Validates that each key is an allowed value.
388
396
  * @param valueValidator - Validates each value in the record.
397
+ * @param options - Optional configuration.
398
+ * @param options.defaults - Built-in defaults for the record entries. When
399
+ * provided, takes priority over defaults passed from the parent at
400
+ * validation time (same precedence as {@link validatorOf}).
389
401
  *
390
402
  * @example
391
403
  * ```ts
@@ -397,7 +409,8 @@ function anyOf(...validators) {
397
409
  * )
398
410
  * ```
399
411
  */
400
- function recordOf(keyValidator, valueValidator) {
412
+ function recordOf(keyValidator, valueValidator, options) {
413
+ const builtInDefaults = options?.defaults;
401
414
  return {
402
415
  is: (u) => TypeValidators.Object.is(u),
403
416
  getType: () => 'object',
@@ -422,14 +435,14 @@ function recordOf(keyValidator, valueValidator) {
422
435
  logger?.warn(OptionMessages.wrongOptionType(name, keyValidator.getType(), key));
423
436
  }
424
437
  });
425
- const recordDefaults = TypeValidators.Object.is(defaults)
426
- ? defaults
427
- : {};
438
+ const recordDefaults = builtInDefaults ??
439
+ (TypeValidators.Object.is(defaults) ? defaults : {});
428
440
  return { value: validateOptions(filtered, validatorMap, recordDefaults, logger, name) };
429
441
  },
430
442
  };
431
443
  }
432
444
 
445
+ const DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS = 300;
433
446
  const BACKGROUND_POLL_INTERVAL_SECONDS = 3600;
434
447
  const dataSourceTypeValidator = TypeValidators.oneOf('cache', 'polling', 'streaming');
435
448
  const connectionModeValidator = TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
@@ -459,19 +472,25 @@ const synchronizerEntryArrayValidator = arrayOf('type', {
459
472
  polling: pollingEntryValidators,
460
473
  streaming: streamingEntryValidators,
461
474
  });
475
+ const fdv1FallbackValidators = {
476
+ pollInterval: TypeValidators.numberWithMin(30),
477
+ endpoints: validatorOf(endpointValidators),
478
+ };
462
479
  const modeDefinitionValidators = {
463
480
  initializers: initializerEntryArrayValidator,
464
481
  synchronizers: synchronizerEntryArrayValidator,
482
+ fdv1Fallback: validatorOf(fdv1FallbackValidators),
465
483
  };
466
- const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
467
484
  const MODE_TABLE = {
468
485
  streaming: {
469
486
  initializers: [{ type: 'cache' }, { type: 'polling' }],
470
487
  synchronizers: [{ type: 'streaming' }, { type: 'polling' }],
488
+ fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
471
489
  },
472
490
  polling: {
473
491
  initializers: [{ type: 'cache' }],
474
492
  synchronizers: [{ type: 'polling' }],
493
+ fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
475
494
  },
476
495
  offline: {
477
496
  initializers: [{ type: 'cache' }],
@@ -484,24 +503,33 @@ const MODE_TABLE = {
484
503
  background: {
485
504
  initializers: [{ type: 'cache' }],
486
505
  synchronizers: [{ type: 'polling', pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS }],
506
+ fdv1Fallback: { pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS },
487
507
  },
488
508
  };
509
+ const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
489
510
 
490
- const modeSwitchingValidators = {
511
+ function hasType(u, type) {
512
+ return TypeValidators.Object.is(u) && u.type === type;
513
+ }
514
+ const automaticModeValidators = {
515
+ type: TypeValidators.oneOf('automatic'),
491
516
  lifecycle: TypeValidators.Boolean,
492
517
  network: TypeValidators.Boolean,
493
518
  };
494
- const dataSystemValidators = {
519
+ const manualModeValidators = {
520
+ type: TypeValidators.oneOf('manual'),
495
521
  initialConnectionMode: connectionModeValidator,
522
+ };
523
+ const dataSystemValidators = {
496
524
  backgroundConnectionMode: connectionModeValidator,
497
- automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
525
+ automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(automaticModeValidators, { is: (u) => hasType(u, 'automatic') }), validatorOf(manualModeValidators, { is: (u) => hasType(u, 'manual') })),
498
526
  connectionModes: connectionModesValidator,
499
527
  };
500
528
  /**
501
529
  * Default FDv2 data system configuration for browser SDKs.
502
530
  */
503
531
  const BROWSER_DATA_SYSTEM_DEFAULTS = {
504
- initialConnectionMode: 'one-shot',
532
+ foregroundConnectionMode: 'one-shot',
505
533
  backgroundConnectionMode: undefined,
506
534
  automaticModeSwitching: false,
507
535
  };
@@ -509,7 +537,7 @@ const BROWSER_DATA_SYSTEM_DEFAULTS = {
509
537
  * Default FDv2 data system configuration for mobile (React Native) SDKs.
510
538
  */
511
539
  const MOBILE_DATA_SYSTEM_DEFAULTS = {
512
- initialConnectionMode: 'streaming',
540
+ foregroundConnectionMode: 'streaming',
513
541
  backgroundConnectionMode: 'background',
514
542
  automaticModeSwitching: true,
515
543
  };
@@ -517,10 +545,24 @@ const MOBILE_DATA_SYSTEM_DEFAULTS = {
517
545
  * Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
518
546
  */
519
547
  const DESKTOP_DATA_SYSTEM_DEFAULTS = {
520
- initialConnectionMode: 'streaming',
548
+ foregroundConnectionMode: 'streaming',
521
549
  backgroundConnectionMode: undefined,
522
550
  automaticModeSwitching: false,
523
551
  };
552
+ function isManualModeSwitching(value) {
553
+ return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual';
554
+ }
555
+ /**
556
+ * Resolve the foreground connection mode from a validated data system config
557
+ * and platform defaults. Uses the mode from `ManualModeSwitching` when present,
558
+ * otherwise falls back to the platform default.
559
+ */
560
+ function resolveForegroundMode(dataSystem, defaults) {
561
+ if (isManualModeSwitching(dataSystem.automaticModeSwitching)) {
562
+ return dataSystem.automaticModeSwitching.initialConnectionMode;
563
+ }
564
+ return dataSystem.foregroundConnectionMode ?? defaults.foregroundConnectionMode;
565
+ }
524
566
 
525
567
  function createValidators(options) {
526
568
  return {
@@ -550,7 +592,12 @@ function createValidators(options) {
550
592
  inspectors: TypeValidators.createTypeArray('LDInspection', {}),
551
593
  cleanOldPersistentData: TypeValidators.Boolean,
552
594
  dataSystem: options?.dataSystemDefaults
553
- ? validatorOf(dataSystemValidators, options.dataSystemDefaults)
595
+ ? validatorOf(dataSystemValidators, {
596
+ defaults: {
597
+ ...options.dataSystemDefaults,
598
+ connectionModes: MODE_TABLE,
599
+ },
600
+ })
554
601
  : TypeValidators.Object,
555
602
  };
556
603
  }
@@ -952,6 +999,46 @@ class EventFactory extends internal.EventFactoryBase {
952
999
  }
953
1000
  }
954
1001
 
1002
+ function readFlagsFromBootstrap(logger, data) {
1003
+ // If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
1004
+ // Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
1005
+ // the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
1006
+ const keys = Object.keys(data);
1007
+ const metadataKey = '$flagsState';
1008
+ const validKey = '$valid';
1009
+ const metadata = data[metadataKey];
1010
+ if (!metadata && keys.length) {
1011
+ logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
1012
+ ' metadata. Events may not be sent correctly.');
1013
+ }
1014
+ if (data[validKey] === false) {
1015
+ logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
1016
+ }
1017
+ const ret = {};
1018
+ keys.forEach((key) => {
1019
+ if (key !== metadataKey && key !== validKey) {
1020
+ let flag;
1021
+ if (metadata && metadata[key]) {
1022
+ flag = {
1023
+ value: data[key],
1024
+ ...metadata[key],
1025
+ };
1026
+ }
1027
+ else {
1028
+ flag = {
1029
+ value: data[key],
1030
+ version: 0,
1031
+ };
1032
+ }
1033
+ ret[key] = {
1034
+ version: flag.version,
1035
+ flag,
1036
+ };
1037
+ }
1038
+ });
1039
+ return ret;
1040
+ }
1041
+
955
1042
  /**
956
1043
  * Suffix appended to context storage keys to form the freshness storage key.
957
1044
  */
@@ -1880,6 +1967,10 @@ class LDClientImpl {
1880
1967
  this._eventFactoryWithReasons = new EventFactory(true);
1881
1968
  this._eventSendingEnabled = false;
1882
1969
  this._identifyQueue = createAsyncTaskQueue();
1970
+ // NOTE: this is used to ease the transition to the new initialization pattern.
1971
+ // All client SDKs should set this to true with the exception of React Native which is
1972
+ // still using the deprecated construct + identify pattern.
1973
+ this._requiresStart = false;
1883
1974
  if (!sdkKey) {
1884
1975
  throw new Error('You must configure the client with a client-side SDK key');
1885
1976
  }
@@ -1888,6 +1979,8 @@ class LDClientImpl {
1888
1979
  }
1889
1980
  this._config = new ConfigurationImpl(options, internalOptions);
1890
1981
  this.logger = this._config.logger;
1982
+ this._requiresStart = internalOptions?.requiresStart ?? false;
1983
+ this.initialContext = internalOptions?.initialContext;
1891
1984
  this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
1892
1985
  this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
1893
1986
  this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
@@ -1905,6 +1998,8 @@ class LDClientImpl {
1905
1998
  });
1906
1999
  });
1907
2000
  this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
2001
+ this.isFDv2 = !!this._config.dataSystem;
2002
+ this.dataSystemConfig = this._config.dataSystem;
1908
2003
  const hooks = [...this._config.hooks];
1909
2004
  this.environmentMetadata = createPluginEnvironmentMetadata(this.sdkKey, this.platform, this._config);
1910
2005
  this._config.getImplementationHooks(this.environmentMetadata).forEach((hook) => {
@@ -1982,6 +2077,57 @@ class LDClientImpl {
1982
2077
  presetFlags(newFlags) {
1983
2078
  this._flagManager.presetFlags(newFlags);
1984
2079
  }
2080
+ /**
2081
+ * Starts the client and returns a promise that resolves to the initialization result.
2082
+ *
2083
+ * This method is idempotent - calling it multiple times returns the same promise.
2084
+ *
2085
+ * @param options Optional configuration. See {@link LDStartOptions}.
2086
+ * @returns A promise that resolves to the initialization result.
2087
+ */
2088
+ start(options) {
2089
+ if (this.initializeResult) {
2090
+ return Promise.resolve(this.initializeResult);
2091
+ }
2092
+ if (this.startPromise) {
2093
+ return this.startPromise;
2094
+ }
2095
+ if (!this.initialContext) {
2096
+ this.logger.error('Initial context not set');
2097
+ return Promise.resolve({ status: 'failed', error: new Error('Initial context not set') });
2098
+ }
2099
+ const identifyOptions = {
2100
+ ...(options?.identifyOptions ?? {}),
2101
+ // Initial identify operations are not sheddable.
2102
+ sheddable: false,
2103
+ };
2104
+ // If the bootstrap data is provided in the start options, and the identify options do not
2105
+ // have bootstrap data, then use the bootstrap data from the start options.
2106
+ if (options?.bootstrap && !identifyOptions.bootstrap) {
2107
+ identifyOptions.bootstrap = options.bootstrap;
2108
+ }
2109
+ if (identifyOptions.bootstrap) {
2110
+ try {
2111
+ if (!identifyOptions.bootstrapParsed) {
2112
+ identifyOptions.bootstrapParsed = readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap);
2113
+ }
2114
+ if (identifyOptions.bootstrapParsed) {
2115
+ this.presetFlags(identifyOptions.bootstrapParsed);
2116
+ }
2117
+ }
2118
+ catch (error) {
2119
+ this.logger.error('Failed to bootstrap data', error);
2120
+ }
2121
+ }
2122
+ if (!this.initializedPromise) {
2123
+ this.initializedPromise = new Promise((resolve) => {
2124
+ this.initResolve = resolve;
2125
+ });
2126
+ }
2127
+ this.startPromise = this._promiseWithTimeout(this.initializedPromise, options?.timeout ?? 5, 'start');
2128
+ this.identifyResult(this.initialContext, identifyOptions);
2129
+ return this.startPromise;
2130
+ }
1985
2131
  /**
1986
2132
  * Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
1987
2133
  *
@@ -2019,6 +2165,10 @@ class LDClientImpl {
2019
2165
  // If completed or shed, then we are done.
2020
2166
  }
2021
2167
  async identifyResult(pristineContext, identifyOptions) {
2168
+ if (this._requiresStart && !this.startPromise) {
2169
+ this.logger.error('The client must be started before a context can be identified. Call start() prior to identifying a context.');
2170
+ return { status: 'error', error: new Error('Identify called before start') };
2171
+ }
2022
2172
  const identifyTimeout = identifyOptions?.timeout ?? DEFAULT_IDENTIFY_TIMEOUT_SECONDS;
2023
2173
  const noTimeout = identifyOptions?.timeout === undefined && identifyOptions?.noTimeout === true;
2024
2174
  // When noTimeout is specified, and a timeout is not specified, then this condition cannot
@@ -2128,7 +2278,7 @@ class LDClientImpl {
2128
2278
  // If waitForInitialization was previously called, then return the promise with a timeout.
2129
2279
  // This condition should only be triggered if waitForInitialization was called multiple times.
2130
2280
  if (this.initializedPromise) {
2131
- return this.promiseWithTimeout(this.initializedPromise, timeout);
2281
+ return this._promiseWithTimeout(this.initializedPromise, timeout);
2132
2282
  }
2133
2283
  // Create a new promise for tracking initialization
2134
2284
  if (!this.initializedPromise) {
@@ -2136,22 +2286,18 @@ class LDClientImpl {
2136
2286
  this.initResolve = resolve;
2137
2287
  });
2138
2288
  }
2139
- return this.promiseWithTimeout(this.initializedPromise, timeout);
2289
+ return this._promiseWithTimeout(this.initializedPromise, timeout);
2140
2290
  }
2141
2291
  /**
2142
- * Apply a timeout promise to a base promise. This is for use with waitForInitialization.
2292
+ * Apply a timeout promise to a base promise. This is for use with waitForInitialization
2293
+ * and start.
2143
2294
  *
2144
2295
  * @param basePromise The promise to race against a timeout.
2145
2296
  * @param timeout The timeout in seconds.
2146
2297
  * @returns A promise that resolves to the initialization result or timeout.
2147
- *
2148
- * @privateRemarks
2149
- * This method is protected because it is used by the browser SDK's `start` method.
2150
- * Eventually, the start method will be moved to this common implementation and this method will
2151
- * be made private.
2152
2298
  */
2153
- promiseWithTimeout(basePromise, timeout) {
2154
- const cancelableTimeout = cancelableTimedPromise(timeout, 'waitForInitialization');
2299
+ _promiseWithTimeout(basePromise, timeout, label = 'waitForInitialization') {
2300
+ const cancelableTimeout = cancelableTimedPromise(timeout, label);
2155
2301
  return Promise.race([
2156
2302
  basePromise.then((res) => {
2157
2303
  cancelableTimeout.cancel();
@@ -2381,46 +2527,6 @@ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
2381
2527
  });
2382
2528
  }
2383
2529
 
2384
- function readFlagsFromBootstrap(logger, data) {
2385
- // If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
2386
- // Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
2387
- // the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
2388
- const keys = Object.keys(data);
2389
- const metadataKey = '$flagsState';
2390
- const validKey = '$valid';
2391
- const metadata = data[metadataKey];
2392
- if (!metadata && keys.length) {
2393
- logger.warn('LaunchDarkly client was initialized with bootstrap data that did not include flag' +
2394
- ' metadata. Events may not be sent correctly.');
2395
- }
2396
- if (data[validKey] === false) {
2397
- logger.warn('LaunchDarkly bootstrap data is not available because the back end could not read the flags.');
2398
- }
2399
- const ret = {};
2400
- keys.forEach((key) => {
2401
- if (key !== metadataKey && key !== validKey) {
2402
- let flag;
2403
- if (metadata && metadata[key]) {
2404
- flag = {
2405
- value: data[key],
2406
- ...metadata[key],
2407
- };
2408
- }
2409
- else {
2410
- flag = {
2411
- value: data[key],
2412
- version: 0,
2413
- };
2414
- }
2415
- ret[key] = {
2416
- version: flag.version,
2417
- flag,
2418
- };
2419
- }
2420
- });
2421
- return ret;
2422
- }
2423
-
2424
2530
  /**
2425
2531
  * Creates endpoint paths for browser (client-side ID) FDv1 evaluation.
2426
2532
  *
@@ -3143,12 +3249,12 @@ async function loadFromCache(config) {
3143
3249
  const { storage, crypto, environmentNamespace, context, logger } = config;
3144
3250
  if (!storage) {
3145
3251
  logger?.debug('No storage available for cache initializer');
3146
- return interrupted(errorInfoFromUnknown('No storage available'), false);
3252
+ return changeSet({ version: 0, type: 'none', updates: [] }, false);
3147
3253
  }
3148
3254
  const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
3149
3255
  if (!cached) {
3150
3256
  logger?.debug('Cache miss for context');
3151
- return interrupted(errorInfoFromUnknown('Cache miss'), false);
3257
+ return changeSet({ version: 0, type: 'none', updates: [] }, false);
3152
3258
  }
3153
3259
  const updates = Object.entries(cached.flags).map(([key, flag]) => ({
3154
3260
  kind: 'flag-eval',
@@ -3157,7 +3263,6 @@ async function loadFromCache(config) {
3157
3263
  object: flagToEvaluationResult(flag),
3158
3264
  }));
3159
3265
  const payload = {
3160
- id: 'cache',
3161
3266
  version: 0,
3162
3267
  // No `state` field. The orchestrator sees a changeSet without a selector,
3163
3268
  // records dataReceived=true, and continues to the next initializer.
@@ -3261,6 +3366,51 @@ function makeFDv2Requestor(plainContextString, serviceEndpoints, paths, requests
3261
3366
  function processFlagEval(object) {
3262
3367
  return object;
3263
3368
  }
3369
+ /**
3370
+ * Converts an FDv2 {@link internal.Update} with `kind: 'flag-eval'` into an
3371
+ * {@link ItemDescriptor} suitable for {@link FlagManager}.
3372
+ *
3373
+ * For put updates the envelope `version` is used as the {@link ItemDescriptor.version}
3374
+ * and as {@link Flag.version}. The rest of the fields are spread from the
3375
+ * {@link FlagEvaluationResult} object.
3376
+ *
3377
+ * For delete updates a tombstone descriptor is created with `deleted: true`.
3378
+ */
3379
+ function flagEvalUpdateToItemDescriptor(update) {
3380
+ if (update.deleted) {
3381
+ return {
3382
+ version: update.version,
3383
+ flag: {
3384
+ version: update.version,
3385
+ deleted: true,
3386
+ value: undefined,
3387
+ trackEvents: false,
3388
+ },
3389
+ };
3390
+ }
3391
+ const evalResult = update.object;
3392
+ return {
3393
+ version: update.version,
3394
+ flag: {
3395
+ ...evalResult,
3396
+ version: update.version,
3397
+ },
3398
+ };
3399
+ }
3400
+ /**
3401
+ * Converts an array of FDv2 payload updates into a map of flag key to
3402
+ * {@link ItemDescriptor}. Only `flag-eval` kind updates are processed;
3403
+ * unrecognized kinds are silently ignored.
3404
+ */
3405
+ function flagEvalPayloadToItemDescriptors(updates) {
3406
+ const descriptors = {};
3407
+ updates.forEach((update) => {
3408
+ if (update.kind === 'flag-eval') {
3409
+ descriptors[update.key] = flagEvalUpdateToItemDescriptor(update);
3410
+ }
3411
+ });
3412
+ return descriptors;
3413
+ }
3264
3414
 
3265
3415
  function getFallback(headers) {
3266
3416
  const value = headers.get('x-ld-fd-fallback');
@@ -3277,7 +3427,7 @@ function getEnvironmentId(headers) {
3277
3427
  * it only forwards payloads and actionable errors. For polling results,
3278
3428
  * we need full control over all protocol action types.
3279
3429
  */
3280
- function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
3430
+ function processEvents(events, fdv1Fallback, environmentId, logger) {
3281
3431
  const handler = internal.createProtocolHandler({
3282
3432
  'flag-eval': processFlagEval,
3283
3433
  }, logger);
@@ -3297,9 +3447,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
3297
3447
  case 'serverError': {
3298
3448
  const errorInfo = errorInfoFromUnknown(action.reason);
3299
3449
  logger?.error(`Server error during polling: ${action.reason}`);
3300
- earlyResult = oneShot
3301
- ? terminalError(errorInfo, fdv1Fallback)
3302
- : interrupted(errorInfo, fdv1Fallback);
3450
+ earlyResult = interrupted(errorInfo, fdv1Fallback);
3303
3451
  break;
3304
3452
  }
3305
3453
  case 'error': {
@@ -3307,9 +3455,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
3307
3455
  if (action.kind === 'MISSING_PAYLOAD' || action.kind === 'PROTOCOL_ERROR') {
3308
3456
  const errorInfo = errorInfoFromInvalidData(action.message);
3309
3457
  logger?.warn(`Protocol error during polling: ${action.message}`);
3310
- earlyResult = oneShot
3311
- ? terminalError(errorInfo, fdv1Fallback)
3312
- : interrupted(errorInfo, fdv1Fallback);
3458
+ earlyResult = interrupted(errorInfo, fdv1Fallback);
3313
3459
  }
3314
3460
  else {
3315
3461
  // Non-actionable errors (UNKNOWN_EVENT) are logged but don't stop processing
@@ -3325,19 +3471,18 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
3325
3471
  // Events didn't produce a result
3326
3472
  const errorInfo = errorInfoFromUnknown('Unexpected end of polling response');
3327
3473
  logger?.error('Unexpected end of polling response');
3328
- return oneShot ? terminalError(errorInfo, fdv1Fallback) : interrupted(errorInfo, fdv1Fallback);
3474
+ return interrupted(errorInfo, fdv1Fallback);
3329
3475
  }
3330
3476
  /**
3331
3477
  * Performs a single FDv2 poll request, processes the protocol response, and
3332
3478
  * returns an {@link FDv2SourceResult}.
3333
3479
  *
3334
- * The `oneShot` parameter controls error handling: when true (initializer),
3335
- * all errors are terminal; when false (synchronizer), recoverable errors
3336
- * produce interrupted results.
3480
+ * Recoverable errors produce interrupted results; unrecoverable HTTP errors
3481
+ * produce terminal errors.
3337
3482
  *
3338
3483
  * @internal
3339
3484
  */
3340
- async function poll(requestor, basis, oneShot, logger) {
3485
+ async function poll(requestor, basis, logger) {
3341
3486
  let fdv1Fallback = false;
3342
3487
  let environmentId;
3343
3488
  try {
@@ -3348,7 +3493,6 @@ async function poll(requestor, basis, oneShot, logger) {
3348
3493
  // (Spec Requirement 10.1.2)
3349
3494
  if (response.status === 304) {
3350
3495
  const nonePayload = {
3351
- id: '',
3352
3496
  version: 0,
3353
3497
  type: 'none',
3354
3498
  updates: [],
@@ -3359,9 +3503,6 @@ async function poll(requestor, basis, oneShot, logger) {
3359
3503
  if (response.status < 200 || response.status >= 300) {
3360
3504
  const errorInfo = errorInfoFromHttpError(response.status);
3361
3505
  logger?.error(`Polling request failed with HTTP error: ${response.status}`);
3362
- if (oneShot) {
3363
- return terminalError(errorInfo, fdv1Fallback);
3364
- }
3365
3506
  const recoverable = response.status <= 0 || isHttpRecoverable(response.status);
3366
3507
  return recoverable
3367
3508
  ? interrupted(errorInfo, fdv1Fallback)
@@ -3371,9 +3512,7 @@ async function poll(requestor, basis, oneShot, logger) {
3371
3512
  if (!response.body) {
3372
3513
  const errorInfo = errorInfoFromInvalidData('Empty response body');
3373
3514
  logger?.error('Polling request received empty response body');
3374
- return oneShot
3375
- ? terminalError(errorInfo, fdv1Fallback)
3376
- : interrupted(errorInfo, fdv1Fallback);
3515
+ return interrupted(errorInfo, fdv1Fallback);
3377
3516
  }
3378
3517
  let parsed;
3379
3518
  try {
@@ -3382,34 +3521,36 @@ async function poll(requestor, basis, oneShot, logger) {
3382
3521
  catch {
3383
3522
  const errorInfo = errorInfoFromInvalidData('Malformed JSON data in polling response');
3384
3523
  logger?.error('Polling request received malformed data');
3385
- return oneShot
3386
- ? terminalError(errorInfo, fdv1Fallback)
3387
- : interrupted(errorInfo, fdv1Fallback);
3524
+ return interrupted(errorInfo, fdv1Fallback);
3388
3525
  }
3389
3526
  if (!Array.isArray(parsed.events)) {
3390
3527
  const errorInfo = errorInfoFromInvalidData('Invalid polling response: missing or invalid events array');
3391
3528
  logger?.error('Polling response does not contain a valid events array');
3392
- return oneShot
3393
- ? terminalError(errorInfo, fdv1Fallback)
3394
- : interrupted(errorInfo, fdv1Fallback);
3529
+ return interrupted(errorInfo, fdv1Fallback);
3395
3530
  }
3396
- return processEvents(parsed.events, oneShot, fdv1Fallback, environmentId, logger);
3531
+ return processEvents(parsed.events, fdv1Fallback, environmentId, logger);
3397
3532
  }
3398
3533
  catch (err) {
3399
3534
  // Network or other I/O error from the fetch itself
3400
3535
  const message = err?.message ?? String(err);
3401
3536
  logger?.error(`Polling request failed with network error: ${message}`);
3402
3537
  const errorInfo = errorInfoFromNetworkError(message);
3403
- return oneShot ? terminalError(errorInfo, fdv1Fallback) : interrupted(errorInfo, fdv1Fallback);
3538
+ return interrupted(errorInfo, fdv1Fallback);
3404
3539
  }
3405
3540
  }
3406
3541
 
3542
+ const SHUTDOWN = Symbol('shutdown');
3407
3543
  /**
3408
- * Creates a one-shot polling initializer that performs a single FDv2 poll
3409
- * request and returns the result.
3544
+ * Creates a polling initializer that performs an FDv2 poll request with
3545
+ * retry logic. Retries up to 3 times on recoverable errors with a 1-second
3546
+ * delay between attempts.
3410
3547
  *
3411
- * All errors are treated as terminal (oneShot=true). If `close()` is called
3412
- * before the poll completes, the result will be a shutdown status.
3548
+ * Unrecoverable errors (401, 403, etc.) are returned immediately as terminal
3549
+ * errors. After exhausting retries on recoverable errors, the result is
3550
+ * converted to a terminal error.
3551
+ *
3552
+ * If `close()` is called during a poll or retry delay, the result will be
3553
+ * a shutdown status.
3413
3554
  *
3414
3555
  * @internal
3415
3556
  */
@@ -3420,12 +3561,40 @@ function createPollingInitializer(requestor, logger, selectorGetter) {
3420
3561
  });
3421
3562
  return {
3422
3563
  async run() {
3423
- const pollResult = poll(requestor, selectorGetter(), true, logger);
3424
- // Race the poll against the shutdown signal
3425
- return Promise.race([shutdownPromise, pollResult]);
3564
+ const maxRetries = 3;
3565
+ const retryDelayMs = 1000;
3566
+ const selector = selectorGetter();
3567
+ let lastResult;
3568
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
3569
+ // eslint-disable-next-line no-await-in-loop
3570
+ const result = await Promise.race([shutdownPromise, poll(requestor, selector, logger)]);
3571
+ if (result === SHUTDOWN) {
3572
+ return shutdown();
3573
+ }
3574
+ if (result.type === 'changeSet') {
3575
+ return result;
3576
+ }
3577
+ // Non-retryable status (terminal_error, goodbye) -> return immediately
3578
+ if (result.state !== 'interrupted') {
3579
+ return result;
3580
+ }
3581
+ // Recoverable error — save and potentially retry
3582
+ lastResult = result;
3583
+ if (attempt < maxRetries) {
3584
+ logger?.warn(`Recoverable polling error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${retryDelayMs}ms...`);
3585
+ // eslint-disable-next-line no-await-in-loop
3586
+ const sleepResult = await Promise.race([shutdownPromise, sleep(retryDelayMs)]);
3587
+ if (sleepResult === SHUTDOWN) {
3588
+ return shutdown();
3589
+ }
3590
+ }
3591
+ }
3592
+ // Convert final interrupted -> terminal_error
3593
+ const status = lastResult;
3594
+ return terminalError(status.errorInfo, status.fdv1Fallback);
3426
3595
  },
3427
3596
  close() {
3428
- shutdownResolve?.(shutdown());
3597
+ shutdownResolve?.(SHUTDOWN);
3429
3598
  shutdownResolve = undefined;
3430
3599
  },
3431
3600
  };
@@ -3488,7 +3657,7 @@ function createPollingSynchronizer(requestor, logger, selectorGetter, pollInterv
3488
3657
  }
3489
3658
  const startTime = Date.now();
3490
3659
  try {
3491
- const result = await poll(requestor, selectorGetter(), false, logger);
3660
+ const result = await poll(requestor, selectorGetter(), logger);
3492
3661
  if (stopped) {
3493
3662
  return;
3494
3663
  }
@@ -3565,6 +3734,100 @@ function createSynchronizerSlot(factory, options) {
3565
3734
  const state = options?.initialState ?? (isFDv1Fallback ? 'blocked' : 'available');
3566
3735
  return { factory, isFDv1Fallback, state };
3567
3736
  }
3737
+ /**
3738
+ * Creates a {@link SourceManager} that coordinates initializer and
3739
+ * synchronizer lifecycle.
3740
+ *
3741
+ * @param initializerFactories Ordered list of initializer factories.
3742
+ * @param synchronizerSlots Ordered list of synchronizer slots with state.
3743
+ * @param selectorGetter Closure that returns the current selector string.
3744
+ */
3745
+ function createSourceManager(initializerFactories, synchronizerSlots, selectorGetter) {
3746
+ let activeSource;
3747
+ let initializerIndex = -1;
3748
+ let synchronizerIndex = -1;
3749
+ let isShutdown = false;
3750
+ function closeActiveSource() {
3751
+ if (activeSource) {
3752
+ activeSource.close();
3753
+ activeSource = undefined;
3754
+ }
3755
+ }
3756
+ function findFirstAvailableIndex() {
3757
+ return synchronizerSlots.findIndex((slot) => slot.state === 'available');
3758
+ }
3759
+ return {
3760
+ get isShutdown() {
3761
+ return isShutdown;
3762
+ },
3763
+ getNextInitializerAndSetActive() {
3764
+ if (isShutdown) {
3765
+ return undefined;
3766
+ }
3767
+ initializerIndex += 1;
3768
+ if (initializerIndex >= initializerFactories.length) {
3769
+ return undefined;
3770
+ }
3771
+ closeActiveSource();
3772
+ const initializer = initializerFactories[initializerIndex](selectorGetter);
3773
+ activeSource = initializer;
3774
+ return initializer;
3775
+ },
3776
+ getNextAvailableSynchronizerAndSetActive() {
3777
+ if (isShutdown || synchronizerSlots.length === 0) {
3778
+ return undefined;
3779
+ }
3780
+ // Scan all slots starting from the position after the current one,
3781
+ // wrapping around to the beginning if needed. This matches the Java
3782
+ // SourceManager behavior where synchronizers cycle rather than exhausting.
3783
+ let visited = 0;
3784
+ while (visited < synchronizerSlots.length) {
3785
+ synchronizerIndex += 1;
3786
+ if (synchronizerIndex >= synchronizerSlots.length) {
3787
+ synchronizerIndex = 0;
3788
+ }
3789
+ const candidate = synchronizerSlots[synchronizerIndex];
3790
+ if (candidate.state === 'available') {
3791
+ closeActiveSource();
3792
+ const synchronizer = candidate.factory(selectorGetter);
3793
+ activeSource = synchronizer;
3794
+ return synchronizer;
3795
+ }
3796
+ visited += 1;
3797
+ }
3798
+ return undefined;
3799
+ },
3800
+ blockCurrentSynchronizer() {
3801
+ if (synchronizerIndex >= 0 && synchronizerIndex < synchronizerSlots.length) {
3802
+ // eslint-disable-next-line no-param-reassign
3803
+ synchronizerSlots[synchronizerIndex].state = 'blocked';
3804
+ }
3805
+ },
3806
+ resetSourceIndex() {
3807
+ synchronizerIndex = -1;
3808
+ },
3809
+ fdv1Fallback() {
3810
+ synchronizerSlots.forEach((slot) => {
3811
+ // eslint-disable-next-line no-param-reassign
3812
+ slot.state = slot.isFDv1Fallback ? 'available' : 'blocked';
3813
+ });
3814
+ synchronizerIndex = -1;
3815
+ },
3816
+ isPrimeSynchronizer() {
3817
+ return synchronizerIndex === findFirstAvailableIndex();
3818
+ },
3819
+ getAvailableSynchronizerCount() {
3820
+ return synchronizerSlots.filter((slot) => slot.state === 'available').length;
3821
+ },
3822
+ hasFDv1Fallback() {
3823
+ return synchronizerSlots.some((slot) => slot.isFDv1Fallback);
3824
+ },
3825
+ close() {
3826
+ isShutdown = true;
3827
+ closeActiveSource();
3828
+ },
3829
+ };
3830
+ }
3568
3831
 
3569
3832
  /**
3570
3833
  * FDv2 event names to listen for on the EventSource. This must stay in sync
@@ -3728,6 +3991,7 @@ function createStreamingBase(config) {
3728
3991
  initialRetryDelayMillis: config.initialRetryDelayMillis,
3729
3992
  readTimeoutMillis: 5 * 60 * 1000,
3730
3993
  retryResetIntervalMillis: 60 * 1000,
3994
+ urlBuilder: buildStreamUri,
3731
3995
  });
3732
3996
  eventSource = es;
3733
3997
  attachFDv2Listeners(es);
@@ -3859,7 +4123,7 @@ function createStreamingSynchronizer(base) {
3859
4123
 
3860
4124
  function createPingHandler(requestor, selectorGetter, logger) {
3861
4125
  return {
3862
- handlePing: () => poll(requestor, selectorGetter(), false, logger),
4126
+ handlePing: () => poll(requestor, selectorGetter(), logger),
3863
4127
  };
3864
4128
  }
3865
4129
  /**
@@ -3950,5 +4214,929 @@ function createDefaultSourceFactoryProvider() {
3950
4214
  };
3951
4215
  }
3952
4216
 
3953
- export { BROWSER_DATA_SYSTEM_DEFAULTS, BROWSER_TRANSITION_TABLE, BaseDataManager, DESKTOP_DATA_SYSTEM_DEFAULTS, DESKTOP_TRANSITION_TABLE, DataSourceState, LDClientImpl, MOBILE_DATA_SYSTEM_DEFAULTS, MOBILE_TRANSITION_TABLE, MODE_TABLE, browserFdv1Endpoints, createDataSourceStatusManager, createDefaultSourceFactoryProvider, dataSystemValidators, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, resolveConnectionMode, safeRegisterDebugOverridePlugins, validateOptions };
4217
+ function flagsToPayload(flags) {
4218
+ const updates = Object.entries(flags).map(([key, flag]) => ({
4219
+ kind: 'flag',
4220
+ key,
4221
+ version: flag.version ?? 1,
4222
+ object: flag,
4223
+ }));
4224
+ return {
4225
+ version: 1,
4226
+ type: 'full',
4227
+ updates,
4228
+ };
4229
+ }
4230
+ /**
4231
+ * Creates a polling synchronizer that polls an FDv1 endpoint and produces
4232
+ * results in the FDv2 {@link Synchronizer} pull model.
4233
+ *
4234
+ * This is a standalone implementation (not a wrapper around the existing FDv1
4235
+ * `PollingProcessor`) because the FDv1 fallback is temporary and will be
4236
+ * removed in the next major version. A focused implementation is simpler than
4237
+ * an adapter layer.
4238
+ *
4239
+ * Polling starts lazily on the first call to `next()`. Each poll returns the
4240
+ * complete flag set as a `changeSet` result with `type: 'full'` and an empty
4241
+ * selector.
4242
+ *
4243
+ * @param requestor FDv1 requestor configured with the appropriate endpoint.
4244
+ * @param pollIntervalMs Interval between polls in milliseconds.
4245
+ * @param logger Optional logger.
4246
+ * @internal
4247
+ */
4248
+ function createFDv1PollingSynchronizer(requestor, pollIntervalMs, logger) {
4249
+ const resultQueue = createAsyncQueue();
4250
+ let shutdownResolve;
4251
+ const shutdownPromise = new Promise((resolve) => {
4252
+ shutdownResolve = resolve;
4253
+ });
4254
+ let timeoutHandle;
4255
+ let stopped = false;
4256
+ let started = false;
4257
+ function scheduleNextPoll(startTime) {
4258
+ if (!stopped) {
4259
+ const elapsed = Date.now() - startTime;
4260
+ const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
4261
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
4262
+ timeoutHandle = setTimeout(doPoll, sleepFor);
4263
+ }
4264
+ }
4265
+ async function doPoll() {
4266
+ if (stopped) {
4267
+ return;
4268
+ }
4269
+ logger?.debug('Polling FDv1 endpoint for feature flag updates');
4270
+ const startTime = Date.now();
4271
+ try {
4272
+ const body = await requestor.requestPayload();
4273
+ if (stopped) {
4274
+ return;
4275
+ }
4276
+ let payload;
4277
+ try {
4278
+ const flags = JSON.parse(body);
4279
+ payload = flagsToPayload(flags);
4280
+ }
4281
+ catch {
4282
+ logger?.error('FDv1 polling received malformed data');
4283
+ resultQueue.put({
4284
+ type: 'status',
4285
+ state: 'interrupted',
4286
+ errorInfo: errorInfoFromInvalidData('Malformed data in FDv1 polling response'),
4287
+ fdv1Fallback: false,
4288
+ });
4289
+ scheduleNextPoll(startTime);
4290
+ return;
4291
+ }
4292
+ resultQueue.put(changeSet(payload, false));
4293
+ }
4294
+ catch (err) {
4295
+ if (stopped) {
4296
+ return;
4297
+ }
4298
+ const requestError = err;
4299
+ if (requestError.status !== undefined) {
4300
+ if (!isHttpRecoverable(requestError.status)) {
4301
+ logger?.error(httpErrorMessage(err, 'FDv1 polling request'));
4302
+ stopped = true;
4303
+ shutdownResolve?.(terminalError(errorInfoFromHttpError(requestError.status), false));
4304
+ shutdownResolve = undefined;
4305
+ return;
4306
+ }
4307
+ }
4308
+ logger?.warn(httpErrorMessage(err, 'FDv1 polling request', 'will retry'));
4309
+ resultQueue.put({
4310
+ type: 'status',
4311
+ state: 'interrupted',
4312
+ errorInfo: requestError.status
4313
+ ? errorInfoFromHttpError(requestError.status)
4314
+ : errorInfoFromNetworkError(requestError.message),
4315
+ fdv1Fallback: false,
4316
+ });
4317
+ }
4318
+ scheduleNextPoll(startTime);
4319
+ }
4320
+ return {
4321
+ next() {
4322
+ if (!started) {
4323
+ started = true;
4324
+ doPoll();
4325
+ }
4326
+ return Promise.race([shutdownPromise, resultQueue.take()]);
4327
+ },
4328
+ close() {
4329
+ stopped = true;
4330
+ if (timeoutHandle !== undefined) {
4331
+ clearTimeout(timeoutHandle);
4332
+ timeoutHandle = undefined;
4333
+ }
4334
+ shutdownResolve?.(shutdown());
4335
+ shutdownResolve = undefined;
4336
+ },
4337
+ };
4338
+ }
4339
+
4340
+ const DEFAULT_FALLBACK_TIMEOUT_MS = 2 * 60 * 1000; // 120 seconds
4341
+ const DEFAULT_RECOVERY_TIMEOUT_MS = 5 * 60 * 1000; // 300 seconds
4342
+ /**
4343
+ * Creates a cancelable timer that resolves with the given {@link ConditionType}
4344
+ * when it fires. Wraps {@link cancelableTimedPromise} to convert its
4345
+ * reject-on-timeout semantics into resolve-with-type semantics.
4346
+ */
4347
+ function conditionTimer(timeoutMs, type, taskName) {
4348
+ const timed = cancelableTimedPromise(timeoutMs / 1000, taskName);
4349
+ return {
4350
+ promise: timed.promise.then(() => new Promise(() => { }), // cancelled — never settle
4351
+ () => type),
4352
+ cancel: timed.cancel,
4353
+ };
4354
+ }
4355
+ /**
4356
+ * Creates a timed {@link Condition}.
4357
+ *
4358
+ * @param timeoutMs Time in milliseconds before the condition fires.
4359
+ * @param type The {@link ConditionType} to resolve with when the timer fires.
4360
+ * @param informHandler Optional callback invoked on each `inform()` call.
4361
+ * When omitted, the timer starts immediately and `inform()` is a no-op.
4362
+ * When provided, the timer must be started explicitly via `controls.start()`.
4363
+ */
4364
+ function createCondition(timeoutMs, type, informHandler) {
4365
+ let resolve;
4366
+ let timer;
4367
+ let closed = false;
4368
+ const promise = new Promise((res) => {
4369
+ resolve = res;
4370
+ });
4371
+ function startTimer() {
4372
+ if (!timer && !closed) {
4373
+ timer = conditionTimer(timeoutMs, type, `${type} condition`);
4374
+ timer.promise.then((t) => {
4375
+ timer = undefined;
4376
+ resolve?.(t);
4377
+ });
4378
+ }
4379
+ }
4380
+ function cancelTimer() {
4381
+ timer?.cancel();
4382
+ timer = undefined;
4383
+ }
4384
+ // No inform handler — start immediately (recovery behavior).
4385
+ if (!informHandler) {
4386
+ startTimer();
4387
+ }
4388
+ return {
4389
+ promise,
4390
+ inform(result) {
4391
+ if (closed) {
4392
+ return;
4393
+ }
4394
+ informHandler?.(result, { start: startTimer, cancel: cancelTimer });
4395
+ },
4396
+ close() {
4397
+ closed = true;
4398
+ cancelTimer();
4399
+ },
4400
+ };
4401
+ }
4402
+ /**
4403
+ * Creates a fallback condition. The condition starts a timer when an
4404
+ * `interrupted` status is received and cancels it when a `changeSet` is
4405
+ * received. If the timer fires, the condition resolves with `'fallback'`.
4406
+ */
4407
+ function createFallbackCondition(timeoutMs) {
4408
+ return createCondition(timeoutMs, 'fallback', (result, { start, cancel }) => {
4409
+ if (result.type === 'changeSet') {
4410
+ cancel();
4411
+ }
4412
+ else if (result.type === 'status' && result.state === 'interrupted') {
4413
+ start();
4414
+ }
4415
+ });
4416
+ }
4417
+ /**
4418
+ * Creates a recovery condition. The condition starts a timer immediately
4419
+ * and resolves with `'recovery'` when it fires. It ignores all `inform()`
4420
+ * calls.
4421
+ */
4422
+ function createRecoveryCondition(timeoutMs) {
4423
+ return createCondition(timeoutMs, 'recovery');
4424
+ }
4425
+ /**
4426
+ * Creates a group of conditions that are managed together.
4427
+ *
4428
+ * @param conditions The conditions to group.
4429
+ */
4430
+ function createConditionGroup(conditions) {
4431
+ return {
4432
+ promise: conditions.length === 0 ? undefined : Promise.race(conditions.map((c) => c.promise)),
4433
+ inform(result) {
4434
+ conditions.forEach((condition) => condition.inform(result));
4435
+ },
4436
+ close() {
4437
+ conditions.forEach((condition) => condition.close());
4438
+ },
4439
+ };
4440
+ }
4441
+ /**
4442
+ * Determines which conditions to create based on the synchronizer's position
4443
+ * and availability.
4444
+ *
4445
+ * - If there is only one available synchronizer, no conditions are needed
4446
+ * (there is nowhere to fall back to).
4447
+ * - If the current synchronizer is the primary (first available), only a
4448
+ * fallback condition is created.
4449
+ * - If the current synchronizer is non-primary, both fallback and recovery
4450
+ * conditions are created.
4451
+ *
4452
+ * @param availableSyncCount Number of available (non-blocked) synchronizers.
4453
+ * @param isPrime Whether the current synchronizer is the primary.
4454
+ * @param fallbackTimeoutMs Fallback condition timeout.
4455
+ * @param recoveryTimeoutMs Recovery condition timeout.
4456
+ */
4457
+ function getConditions(availableSyncCount, isPrime, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS) {
4458
+ if (availableSyncCount <= 1) {
4459
+ return createConditionGroup([]);
4460
+ }
4461
+ if (isPrime) {
4462
+ return createConditionGroup([createFallbackCondition(fallbackTimeoutMs)]);
4463
+ }
4464
+ return createConditionGroup([
4465
+ createFallbackCondition(fallbackTimeoutMs),
4466
+ createRecoveryCondition(recoveryTimeoutMs),
4467
+ ]);
4468
+ }
4469
+
4470
+ /**
4471
+ * Creates an {@link FDv2DataSource} orchestrator.
4472
+ */
4473
+ function createFDv2DataSource(config) {
4474
+ const { initializerFactories, synchronizerSlots, dataCallback, statusManager, selectorGetter, logger, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS, } = config;
4475
+ let initialized = false;
4476
+ let closed = false;
4477
+ let dataReceived = false;
4478
+ let initResolve;
4479
+ let initReject;
4480
+ const sourceManager = createSourceManager(initializerFactories, synchronizerSlots, selectorGetter);
4481
+ function markInitialized() {
4482
+ if (!initialized) {
4483
+ initialized = true;
4484
+ initResolve?.();
4485
+ initResolve = undefined;
4486
+ initReject = undefined;
4487
+ }
4488
+ }
4489
+ function applyChangeSet(result) {
4490
+ dataCallback(result.payload);
4491
+ statusManager.requestStateUpdate('VALID');
4492
+ }
4493
+ function reportStatusError(result) {
4494
+ if (result.errorInfo) {
4495
+ statusManager.reportError(result.errorInfo.kind, result.errorInfo.message, result.errorInfo.statusCode, result.state === 'interrupted');
4496
+ }
4497
+ }
4498
+ function handleFdv1Fallback(result) {
4499
+ if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) {
4500
+ sourceManager.fdv1Fallback();
4501
+ return true;
4502
+ }
4503
+ return false;
4504
+ }
4505
+ /* eslint-disable no-await-in-loop */
4506
+ // The orchestration loops intentionally use await-in-loop for sequential
4507
+ // state machine processing — one result at a time.
4508
+ async function runInitializers() {
4509
+ while (!closed) {
4510
+ const initializer = sourceManager.getNextInitializerAndSetActive();
4511
+ if (initializer === undefined) {
4512
+ break;
4513
+ }
4514
+ const result = await initializer.run();
4515
+ if (closed) {
4516
+ return;
4517
+ }
4518
+ if (result.type === 'changeSet' && result.payload.type !== 'none') {
4519
+ applyChangeSet(result);
4520
+ if (handleFdv1Fallback(result)) {
4521
+ // FDv1 fallback triggered during initialization -- data was received
4522
+ // but we should move to synchronizers where the FDv1 adapter will run.
4523
+ dataReceived = true;
4524
+ break;
4525
+ }
4526
+ if (result.payload.state) {
4527
+ // Got basis data with a selector -- initialization is complete.
4528
+ markInitialized();
4529
+ return;
4530
+ }
4531
+ // Got data but no selector (e.g., cache). Record that data was
4532
+ // received and continue to the next initializer.
4533
+ dataReceived = true;
4534
+ }
4535
+ else if (result.type === 'status') {
4536
+ switch (result.state) {
4537
+ case 'interrupted':
4538
+ case 'terminal_error':
4539
+ logger?.warn(`Initializer failed: ${result.errorInfo?.message ?? 'unknown error'}`);
4540
+ reportStatusError(result);
4541
+ break;
4542
+ case 'shutdown':
4543
+ return;
4544
+ }
4545
+ handleFdv1Fallback(result);
4546
+ }
4547
+ }
4548
+ // All initializers exhausted.
4549
+ if (dataReceived) {
4550
+ markInitialized();
4551
+ }
4552
+ }
4553
+ async function runSynchronizers() {
4554
+ while (!closed) {
4555
+ const synchronizer = sourceManager.getNextAvailableSynchronizerAndSetActive();
4556
+ if (synchronizer === undefined) {
4557
+ if (!initialized) {
4558
+ initReject?.(new Error('All data sources exhausted without receiving data.'));
4559
+ initResolve = undefined;
4560
+ initReject = undefined;
4561
+ }
4562
+ return;
4563
+ }
4564
+ const conditions = getConditions(sourceManager.getAvailableSynchronizerCount(), sourceManager.isPrimeSynchronizer(), fallbackTimeoutMs, recoveryTimeoutMs);
4565
+ if (conditions.promise) {
4566
+ logger?.debug('Fallback condition active for current synchronizer.');
4567
+ }
4568
+ // try/finally ensures conditions are closed on all code paths.
4569
+ let synchronizerRunning = true;
4570
+ try {
4571
+ while (!closed && synchronizerRunning) {
4572
+ const syncPromise = synchronizer
4573
+ .next()
4574
+ .then((value) => ({ source: 'sync', value }));
4575
+ const racers = [syncPromise];
4576
+ if (conditions.promise !== undefined) {
4577
+ racers.push(conditions.promise.then((value) => ({ source: 'condition', value })));
4578
+ }
4579
+ const winner = await Promise.race(racers);
4580
+ if (closed) {
4581
+ return;
4582
+ }
4583
+ if (winner.source === 'condition') {
4584
+ const conditionType = winner.value;
4585
+ if (conditionType === 'fallback') {
4586
+ logger?.warn('Fallback condition fired, moving to next synchronizer.');
4587
+ }
4588
+ else if (conditionType === 'recovery') {
4589
+ logger?.info('Recovery condition fired, resetting to primary synchronizer.');
4590
+ sourceManager.resetSourceIndex();
4591
+ }
4592
+ synchronizerRunning = false;
4593
+ }
4594
+ else {
4595
+ // Synchronizer produced a result.
4596
+ const syncResult = winner.value;
4597
+ conditions.inform(syncResult);
4598
+ if (syncResult.type === 'changeSet') {
4599
+ applyChangeSet(syncResult);
4600
+ if (!initialized) {
4601
+ markInitialized();
4602
+ }
4603
+ }
4604
+ else if (syncResult.type === 'status') {
4605
+ switch (syncResult.state) {
4606
+ case 'interrupted':
4607
+ logger?.warn(`Synchronizer interrupted: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
4608
+ reportStatusError(syncResult);
4609
+ break;
4610
+ case 'terminal_error':
4611
+ logger?.error(`Synchronizer terminal error: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
4612
+ reportStatusError(syncResult);
4613
+ sourceManager.blockCurrentSynchronizer();
4614
+ synchronizerRunning = false;
4615
+ break;
4616
+ case 'shutdown':
4617
+ return;
4618
+ case 'goodbye':
4619
+ // The synchronizer will handle reconnection internally.
4620
+ break;
4621
+ default:
4622
+ break;
4623
+ }
4624
+ }
4625
+ // Check for FDv1 fallback after all result handling — single location.
4626
+ if (handleFdv1Fallback(syncResult)) {
4627
+ synchronizerRunning = false;
4628
+ }
4629
+ }
4630
+ }
4631
+ }
4632
+ finally {
4633
+ conditions.close();
4634
+ }
4635
+ }
4636
+ }
4637
+ /* eslint-enable no-await-in-loop */
4638
+ async function runOrchestration() {
4639
+ // No sources configured at all — nothing to wait for, immediately valid.
4640
+ if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
4641
+ statusManager.requestStateUpdate('VALID');
4642
+ markInitialized();
4643
+ return;
4644
+ }
4645
+ await runInitializers();
4646
+ if (!closed) {
4647
+ await runSynchronizers();
4648
+ }
4649
+ }
4650
+ return {
4651
+ start() {
4652
+ return new Promise((resolve, reject) => {
4653
+ initResolve = resolve;
4654
+ initReject = reject;
4655
+ statusManager.requestStateUpdate('INITIALIZING');
4656
+ runOrchestration()
4657
+ .then(() => {
4658
+ // Orchestration completed without error. If the init promise was
4659
+ // never resolved (e.g., close() called during init, or all sources
4660
+ // exhausted without data and no synchronizers), resolve it now.
4661
+ // This prevents the start() promise from hanging forever.
4662
+ if (!initialized) {
4663
+ initReject?.(new Error('Data source closed before initialization completed.'));
4664
+ initResolve = undefined;
4665
+ initReject = undefined;
4666
+ }
4667
+ })
4668
+ .catch((err) => {
4669
+ if (!initialized) {
4670
+ initReject?.(err instanceof Error ? err : new Error(String(err)));
4671
+ initResolve = undefined;
4672
+ initReject = undefined;
4673
+ }
4674
+ else {
4675
+ logger?.error(`Orchestration error: ${err}`);
4676
+ }
4677
+ });
4678
+ });
4679
+ },
4680
+ close() {
4681
+ closed = true;
4682
+ sourceManager.close();
4683
+ },
4684
+ };
4685
+ }
4686
+
4687
+ /** Default debounce window duration in milliseconds. */
4688
+ const DEFAULT_DEBOUNCE_MS = 1000;
4689
+ /**
4690
+ * Creates a {@link StateDebounceManager}.
4691
+ *
4692
+ * The manager accumulates state changes from network, lifecycle, and
4693
+ * connection mode events. Each event updates the relevant component
4694
+ * of the pending state and resets the debounce timer. When the timer
4695
+ * fires, the reconciliation callback receives the final combined state.
4696
+ *
4697
+ * @param config Configuration for the debounce manager.
4698
+ */
4699
+ function createStateDebounceManager(config) {
4700
+ const { initialState, onReconcile, debounceMs = DEFAULT_DEBOUNCE_MS } = config;
4701
+ let { networkState, lifecycleState, requestedMode } = initialState;
4702
+ let timer;
4703
+ let closed = false;
4704
+ function getPendingState() {
4705
+ return { networkState, lifecycleState, requestedMode };
4706
+ }
4707
+ function resetTimer() {
4708
+ if (closed) {
4709
+ return;
4710
+ }
4711
+ if (timer !== undefined) {
4712
+ clearTimeout(timer);
4713
+ }
4714
+ timer = setTimeout(() => {
4715
+ timer = undefined;
4716
+ if (!closed) {
4717
+ onReconcile(getPendingState());
4718
+ }
4719
+ }, debounceMs);
4720
+ }
4721
+ return {
4722
+ setNetworkState(state) {
4723
+ if (networkState === state) {
4724
+ return;
4725
+ }
4726
+ networkState = state;
4727
+ resetTimer();
4728
+ },
4729
+ setLifecycleState(state) {
4730
+ if (lifecycleState === state) {
4731
+ return;
4732
+ }
4733
+ lifecycleState = state;
4734
+ resetTimer();
4735
+ },
4736
+ setRequestedMode(mode) {
4737
+ if (requestedMode === mode) {
4738
+ return;
4739
+ }
4740
+ requestedMode = mode;
4741
+ resetTimer();
4742
+ },
4743
+ close() {
4744
+ closed = true;
4745
+ if (timer !== undefined) {
4746
+ clearTimeout(timer);
4747
+ timer = undefined;
4748
+ }
4749
+ },
4750
+ };
4751
+ }
4752
+
4753
+ const logTag = '[FDv2DataManagerBase]';
4754
+ /**
4755
+ * Creates a shared FDv2 data manager that owns mode resolution, debouncing,
4756
+ * selector state, and FDv2DataSource lifecycle. Platform SDKs (browser, RN)
4757
+ * wrap this with platform-specific config and event wiring.
4758
+ */
4759
+ function createFDv2DataManagerBase(baseConfig) {
4760
+ const { platform, flagManager, config, baseHeaders, emitter, transitionTable, foregroundMode: configuredForegroundMode, backgroundMode, modeTable, sourceFactoryProvider, buildQueryParams, fdv1Endpoints, fallbackTimeoutMs, recoveryTimeoutMs, } = baseConfig;
4761
+ const { logger } = config;
4762
+ const statusManager = createDataSourceStatusManager(emitter);
4763
+ const endpoints = fdv2Endpoints();
4764
+ // Merge user-provided connection mode overrides into the mode table.
4765
+ const effectiveModeTable = config.dataSystem?.connectionModes
4766
+ ? { ...modeTable, ...config.dataSystem.connectionModes }
4767
+ : modeTable;
4768
+ // --- Mutable state ---
4769
+ let selector;
4770
+ let currentResolvedMode = configuredForegroundMode;
4771
+ let foregroundMode = configuredForegroundMode;
4772
+ let dataSource;
4773
+ let debounceManager;
4774
+ let identifiedContext;
4775
+ let factoryContext;
4776
+ let initialized = false;
4777
+ let bootstrapped = false;
4778
+ let closed = false;
4779
+ let flushCallback;
4780
+ // Explicit connection mode override — bypasses transition table entirely.
4781
+ let connectionModeOverride;
4782
+ // Forced/automatic streaming state for browser listener-driven streaming.
4783
+ let forcedStreaming;
4784
+ let automaticStreamingState = false;
4785
+ // Outstanding identify promise callbacks — needed so that mode switches
4786
+ // during identify can wire the new data source's completion to the
4787
+ // original identify promise.
4788
+ let pendingIdentifyResolve;
4789
+ let pendingIdentifyReject;
4790
+ // Current debounce input state.
4791
+ let networkState = 'available';
4792
+ let lifecycleState = 'foreground';
4793
+ // --- Helpers ---
4794
+ function getModeDefinition(mode) {
4795
+ return effectiveModeTable[mode];
4796
+ }
4797
+ function buildModeState() {
4798
+ return {
4799
+ lifecycle: lifecycleState,
4800
+ networkAvailable: networkState === 'available',
4801
+ foregroundMode,
4802
+ backgroundMode: backgroundMode ?? 'offline',
4803
+ };
4804
+ }
4805
+ /**
4806
+ * Resolve the current effective connection mode.
4807
+ *
4808
+ * Priority:
4809
+ * 1. connectionModeOverride (set via setConnectionMode) — bypasses everything
4810
+ * 2. Transition table (network/lifecycle state + foreground/background modes)
4811
+ */
4812
+ function resolveMode() {
4813
+ if (connectionModeOverride !== undefined) {
4814
+ return connectionModeOverride;
4815
+ }
4816
+ return resolveConnectionMode(transitionTable, buildModeState());
4817
+ }
4818
+ /**
4819
+ * Resolve the foreground mode input for the transition table based on
4820
+ * forced/automatic streaming state.
4821
+ *
4822
+ * Priority: forcedStreaming > automaticStreaming > configuredForegroundMode
4823
+ */
4824
+ function resolveStreamingForeground() {
4825
+ if (forcedStreaming === true) {
4826
+ return 'streaming';
4827
+ }
4828
+ if (forcedStreaming === false) {
4829
+ return configuredForegroundMode === 'streaming' ? 'one-shot' : configuredForegroundMode;
4830
+ }
4831
+ return automaticStreamingState ? 'streaming' : configuredForegroundMode;
4832
+ }
4833
+ /**
4834
+ * Compute the effective foreground mode from streaming state and push it
4835
+ * through the debounce manager. Used by setForcedStreaming and
4836
+ * setAutomaticStreamingState.
4837
+ */
4838
+ function pushForegroundMode() {
4839
+ foregroundMode = resolveStreamingForeground();
4840
+ debounceManager?.setRequestedMode(foregroundMode);
4841
+ }
4842
+ /**
4843
+ * Convert a ModeDefinition's entries into concrete InitializerFactory[]
4844
+ * and SynchronizerSlot[] using the source factory provider.
4845
+ */
4846
+ function buildFactories(modeDef, ctx, includeInitializers) {
4847
+ const initializerFactories = [];
4848
+ if (includeInitializers) {
4849
+ modeDef.initializers
4850
+ // Skip cache when bootstrapped — bootstrap data was applied to the
4851
+ // flag store before identify, so the cache would only load older data.
4852
+ .filter((entry) => !(bootstrapped && entry.type === 'cache'))
4853
+ .forEach((entry) => {
4854
+ const factory = sourceFactoryProvider.createInitializerFactory(entry, ctx);
4855
+ if (factory) {
4856
+ initializerFactories.push(factory);
4857
+ }
4858
+ else {
4859
+ logger.warn(`${logTag} Unsupported initializer type '${entry.type}'. It will be skipped.`);
4860
+ }
4861
+ });
4862
+ }
4863
+ const synchronizerSlots = [];
4864
+ modeDef.synchronizers.forEach((entry) => {
4865
+ const slot = sourceFactoryProvider.createSynchronizerSlot(entry, ctx);
4866
+ if (slot) {
4867
+ synchronizerSlots.push(slot);
4868
+ }
4869
+ else {
4870
+ logger.warn(`${logTag} Unsupported synchronizer type '${entry.type}'. It will be skipped.`);
4871
+ }
4872
+ });
4873
+ // Append a blocked FDv1 fallback synchronizer when configured and
4874
+ // when there are FDv2 synchronizers to fall back from.
4875
+ if (fdv1Endpoints && synchronizerSlots.length > 0) {
4876
+ const fallbackConfig = modeDef.fdv1Fallback;
4877
+ const fallbackPollIntervalMs = (fallbackConfig?.pollInterval ?? config.pollInterval) * 1000;
4878
+ const fallbackServiceEndpoints = fallbackConfig?.endpoints?.pollingBaseUri || fallbackConfig?.endpoints?.streamingBaseUri
4879
+ ? 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)
4880
+ : ctx.serviceEndpoints;
4881
+ const fdv1RequestorFactory = () => makeRequestor(ctx.plainContextString, fallbackServiceEndpoints, fdv1Endpoints.polling(), ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams, config.withReasons, config.useReport);
4882
+ const fdv1SyncFactory = () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger);
4883
+ synchronizerSlots.push(createSynchronizerSlot(fdv1SyncFactory, { isFDv1Fallback: true }));
4884
+ }
4885
+ return { initializerFactories, synchronizerSlots };
4886
+ }
4887
+ /**
4888
+ * The data callback shared across all FDv2DataSource instances for
4889
+ * the current identify. Handles selector tracking and flag updates.
4890
+ */
4891
+ function dataCallback(payload) {
4892
+ logger.debug(`${logTag} dataCallback: type=${payload.type}, updates=${payload.updates.length}, state=${payload.state}`);
4893
+ selector = payload.state;
4894
+ const context = identifiedContext;
4895
+ if (!context) {
4896
+ logger.warn(`${logTag} dataCallback called without an identified context.`);
4897
+ return;
4898
+ }
4899
+ const descriptors = flagEvalPayloadToItemDescriptors(payload.updates ?? []);
4900
+ // Flag updates and change events happen synchronously inside applyChanges.
4901
+ // The returned promise is only for async cache persistence — we intentionally
4902
+ // do not await it so the data source pipeline is not blocked by storage I/O.
4903
+ flagManager.applyChanges(context, descriptors, payload.type).catch((e) => {
4904
+ logger.warn(`${logTag} Failed to persist flag cache: ${e}`);
4905
+ });
4906
+ }
4907
+ /**
4908
+ * Create and start a new FDv2DataSource for the given mode.
4909
+ *
4910
+ * @param mode The connection mode to use.
4911
+ * @param includeInitializers Whether to include initializers (true on
4912
+ * first identify, false on mode switch after initialization).
4913
+ */
4914
+ function createAndStartDataSource(mode, includeInitializers) {
4915
+ if (!factoryContext) {
4916
+ logger.warn(`${logTag} Cannot create data source without factory context.`);
4917
+ return;
4918
+ }
4919
+ const modeDef = getModeDefinition(mode);
4920
+ const { initializerFactories, synchronizerSlots } = buildFactories(modeDef, factoryContext, includeInitializers);
4921
+ currentResolvedMode = mode;
4922
+ // If there are no sources at all (e.g., offline or one-shot mode
4923
+ // post-initialization), don't create a data source.
4924
+ if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
4925
+ logger.debug(`${logTag} Mode '${mode}' has no sources. No data source created.`);
4926
+ if (!initialized && pendingIdentifyResolve) {
4927
+ // Offline mode during initial identify — resolve immediately.
4928
+ // The SDK will use cached data if any.
4929
+ initialized = true;
4930
+ pendingIdentifyResolve();
4931
+ pendingIdentifyResolve = undefined;
4932
+ pendingIdentifyReject = undefined;
4933
+ }
4934
+ return;
4935
+ }
4936
+ const selectorGetter = () => selector;
4937
+ dataSource = createFDv2DataSource({
4938
+ initializerFactories,
4939
+ synchronizerSlots,
4940
+ dataCallback,
4941
+ statusManager,
4942
+ selectorGetter,
4943
+ logger,
4944
+ fallbackTimeoutMs,
4945
+ recoveryTimeoutMs,
4946
+ });
4947
+ dataSource
4948
+ .start()
4949
+ .then(() => {
4950
+ initialized = true;
4951
+ if (pendingIdentifyResolve) {
4952
+ pendingIdentifyResolve();
4953
+ pendingIdentifyResolve = undefined;
4954
+ pendingIdentifyReject = undefined;
4955
+ }
4956
+ })
4957
+ .catch((err) => {
4958
+ if (pendingIdentifyReject) {
4959
+ pendingIdentifyReject(err instanceof Error ? err : new Error(String(err)));
4960
+ pendingIdentifyResolve = undefined;
4961
+ pendingIdentifyReject = undefined;
4962
+ }
4963
+ });
4964
+ }
4965
+ /**
4966
+ * Reconciliation callback invoked when the debounce timer fires.
4967
+ * Resolves the new mode and switches data sources if needed.
4968
+ */
4969
+ function onReconcile(pendingState) {
4970
+ if (closed || !factoryContext) {
4971
+ return;
4972
+ }
4973
+ // Update local state from the debounced pending state.
4974
+ networkState = pendingState.networkState;
4975
+ lifecycleState = pendingState.lifecycleState;
4976
+ foregroundMode = pendingState.requestedMode;
4977
+ const newMode = resolveMode();
4978
+ if (newMode === currentResolvedMode) {
4979
+ logger.debug(`${logTag} Reconcile: mode unchanged (${newMode}). No action.`);
4980
+ return;
4981
+ }
4982
+ logger.debug(`${logTag} Reconcile: mode switching from '${currentResolvedMode}' to '${newMode}'.`);
4983
+ // Close the current data source.
4984
+ dataSource?.close();
4985
+ dataSource = undefined;
4986
+ // Include initializers if we don't have a selector yet. This covers:
4987
+ // - Not yet initialized (normal case)
4988
+ // - Initialized from bootstrap (no selector) — need initializers to
4989
+ // get a full payload via poll before starting synchronizers
4990
+ // When we have a selector, only synchronizers change (spec 5.3.8).
4991
+ const includeInitializers = !selector;
4992
+ createAndStartDataSource(newMode, includeInitializers);
4993
+ }
4994
+ // --- Public interface ---
4995
+ return {
4996
+ get configuredForegroundMode() {
4997
+ return configuredForegroundMode;
4998
+ },
4999
+ async identify(identifyResolve, identifyReject, context, identifyOptions) {
5000
+ if (closed) {
5001
+ logger.debug(`${logTag} Identify called after close.`);
5002
+ return;
5003
+ }
5004
+ // Tear down previous state.
5005
+ dataSource?.close();
5006
+ dataSource = undefined;
5007
+ debounceManager?.close();
5008
+ debounceManager = undefined;
5009
+ selector = undefined;
5010
+ initialized = false;
5011
+ bootstrapped = false;
5012
+ identifiedContext = context;
5013
+ pendingIdentifyResolve = identifyResolve;
5014
+ pendingIdentifyReject = identifyReject;
5015
+ const plainContextString = JSON.stringify(Context.toLDContext(context));
5016
+ const queryParams = buildQueryParams(identifyOptions);
5017
+ if (config.withReasons) {
5018
+ queryParams.push({ key: 'withReasons', value: 'true' });
5019
+ }
5020
+ const streamingEndpoints = endpoints.streaming();
5021
+ const pollingEndpoints = endpoints.polling();
5022
+ const requestor = makeFDv2Requestor(plainContextString, config.serviceEndpoints, pollingEndpoints, platform.requests, platform.encoding, baseHeaders, queryParams);
5023
+ const environmentNamespace = await namespaceForEnvironment(platform.crypto, baseConfig.credential);
5024
+ // Re-check after the await — close() may have been called while
5025
+ // namespaceForEnvironment was pending.
5026
+ if (closed) {
5027
+ logger.debug(`${logTag} Identify aborted: closed during async setup.`);
5028
+ return;
5029
+ }
5030
+ factoryContext = {
5031
+ requestor,
5032
+ requests: platform.requests,
5033
+ encoding: platform.encoding,
5034
+ serviceEndpoints: config.serviceEndpoints,
5035
+ baseHeaders,
5036
+ queryParams,
5037
+ plainContextString,
5038
+ logger,
5039
+ polling: {
5040
+ paths: pollingEndpoints,
5041
+ intervalSeconds: config.pollInterval,
5042
+ },
5043
+ streaming: {
5044
+ paths: streamingEndpoints,
5045
+ initialReconnectDelaySeconds: config.streamInitialReconnectDelay,
5046
+ },
5047
+ storage: platform.storage,
5048
+ crypto: platform.crypto,
5049
+ environmentNamespace,
5050
+ context,
5051
+ };
5052
+ // Ensure foreground mode reflects current streaming state before resolving.
5053
+ foregroundMode = resolveStreamingForeground();
5054
+ // Resolve the initial mode.
5055
+ const mode = resolveMode();
5056
+ logger.debug(`${logTag} Identify: initial mode resolved to '${mode}'.`);
5057
+ bootstrapped = identifyOptions?.bootstrap !== undefined;
5058
+ if (bootstrapped) {
5059
+ // Bootstrap data was already applied to the flag store by the
5060
+ // caller (BrowserClient.start → presetFlags) before identify
5061
+ // was called. Resolve immediately — flag evaluations will use
5062
+ // the bootstrap data synchronously.
5063
+ initialized = true;
5064
+ statusManager.requestStateUpdate('VALID');
5065
+ // selector remains undefined — bootstrap data has no selector.
5066
+ pendingIdentifyResolve?.();
5067
+ pendingIdentifyResolve = undefined;
5068
+ pendingIdentifyReject = undefined;
5069
+ // Only create a data source if the mode has synchronizers.
5070
+ // For one-shot (no synchronizers), there's nothing more to do.
5071
+ const modeDef = getModeDefinition(mode);
5072
+ if (modeDef.synchronizers.length > 0) {
5073
+ // Start synchronizers without initializers — we already have
5074
+ // data from bootstrap. Initializers will run on mode switches
5075
+ // if selector is still undefined (see onReconcile).
5076
+ createAndStartDataSource(mode, false);
5077
+ }
5078
+ }
5079
+ else {
5080
+ // Normal identify — create and start the data source with full pipeline.
5081
+ createAndStartDataSource(mode, true);
5082
+ }
5083
+ // Set up debouncing for subsequent state changes.
5084
+ debounceManager = createStateDebounceManager({
5085
+ initialState: {
5086
+ networkState,
5087
+ lifecycleState,
5088
+ requestedMode: foregroundMode,
5089
+ },
5090
+ onReconcile,
5091
+ });
5092
+ },
5093
+ close() {
5094
+ closed = true;
5095
+ dataSource?.close();
5096
+ dataSource = undefined;
5097
+ debounceManager?.close();
5098
+ debounceManager = undefined;
5099
+ pendingIdentifyResolve = undefined;
5100
+ pendingIdentifyReject = undefined;
5101
+ },
5102
+ setNetworkState(state) {
5103
+ networkState = state;
5104
+ debounceManager?.setNetworkState(state);
5105
+ },
5106
+ setLifecycleState(state) {
5107
+ // Flush immediately when going to background — the app may be
5108
+ // about to close. This is not debounced (CONNMODE spec 3.3.1).
5109
+ if (state === 'background' && lifecycleState !== 'background') {
5110
+ flushCallback?.();
5111
+ }
5112
+ lifecycleState = state;
5113
+ debounceManager?.setLifecycleState(state);
5114
+ },
5115
+ setConnectionMode(mode) {
5116
+ connectionModeOverride = mode;
5117
+ if (mode !== undefined) {
5118
+ debounceManager?.setRequestedMode(mode);
5119
+ }
5120
+ else {
5121
+ pushForegroundMode();
5122
+ }
5123
+ },
5124
+ getCurrentMode() {
5125
+ return currentResolvedMode;
5126
+ },
5127
+ setFlushCallback(callback) {
5128
+ flushCallback = callback;
5129
+ },
5130
+ setForcedStreaming(streaming) {
5131
+ forcedStreaming = streaming;
5132
+ pushForegroundMode();
5133
+ },
5134
+ setAutomaticStreamingState(streaming) {
5135
+ automaticStreamingState = streaming;
5136
+ pushForegroundMode();
5137
+ },
5138
+ };
5139
+ }
5140
+
5141
+ export { BROWSER_DATA_SYSTEM_DEFAULTS, BROWSER_TRANSITION_TABLE, BaseDataManager, DESKTOP_DATA_SYSTEM_DEFAULTS, DESKTOP_TRANSITION_TABLE, DataSourceState, LDClientImpl, MOBILE_DATA_SYSTEM_DEFAULTS, MOBILE_TRANSITION_TABLE, MODE_TABLE, browserFdv1Endpoints, createDataSourceStatusManager, createDefaultSourceFactoryProvider, createFDv2DataManagerBase, createStateDebounceManager, dataSystemValidators, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, resolveConnectionMode, resolveForegroundMode, safeRegisterDebugOverridePlugins, validateOptions };
3954
5142
  //# sourceMappingURL=index.mjs.map