@launchdarkly/js-client-sdk-common 1.22.0 → 1.23.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 (60) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/DataManager.d.ts +24 -0
  3. package/dist/cjs/DataManager.d.ts.map +1 -1
  4. package/dist/cjs/api/datasource/DataSourceEntry.d.ts +11 -0
  5. package/dist/cjs/api/datasource/DataSourceEntry.d.ts.map +1 -1
  6. package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts +22 -0
  7. package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  8. package/dist/cjs/api/datasource/ModeDefinition.d.ts +3 -3
  9. package/dist/cjs/api/datasource/ModeDefinition.d.ts.map +1 -1
  10. package/dist/cjs/api/datasource/index.d.ts +1 -1
  11. package/dist/cjs/api/datasource/index.d.ts.map +1 -1
  12. package/dist/cjs/datasource/ConnectionModeConfig.d.ts.map +1 -1
  13. package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts +1 -0
  14. package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  15. package/dist/cjs/datasource/SourceFactoryProvider.d.ts +77 -0
  16. package/dist/cjs/datasource/SourceFactoryProvider.d.ts.map +1 -0
  17. package/dist/cjs/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
  18. package/dist/cjs/flag-manager/FlagManager.d.ts +16 -1
  19. package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
  20. package/dist/cjs/flag-manager/FlagPersistence.d.ts +13 -1
  21. package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
  22. package/dist/cjs/flag-manager/FlagStore.d.ts +10 -0
  23. package/dist/cjs/flag-manager/FlagStore.d.ts.map +1 -1
  24. package/dist/cjs/flag-manager/FlagUpdater.d.ts +13 -1
  25. package/dist/cjs/flag-manager/FlagUpdater.d.ts.map +1 -1
  26. package/dist/cjs/index.cjs +1130 -3
  27. package/dist/cjs/index.cjs.map +1 -1
  28. package/dist/cjs/index.d.ts +6 -1
  29. package/dist/cjs/index.d.ts.map +1 -1
  30. package/dist/cjs/types/index.d.ts +2 -2
  31. package/dist/esm/DataManager.d.ts +24 -0
  32. package/dist/esm/DataManager.d.ts.map +1 -1
  33. package/dist/esm/api/datasource/DataSourceEntry.d.ts +11 -0
  34. package/dist/esm/api/datasource/DataSourceEntry.d.ts.map +1 -1
  35. package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts +22 -0
  36. package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  37. package/dist/esm/api/datasource/ModeDefinition.d.ts +3 -3
  38. package/dist/esm/api/datasource/ModeDefinition.d.ts.map +1 -1
  39. package/dist/esm/api/datasource/index.d.ts +1 -1
  40. package/dist/esm/api/datasource/index.d.ts.map +1 -1
  41. package/dist/esm/datasource/ConnectionModeConfig.d.ts.map +1 -1
  42. package/dist/esm/datasource/LDClientDataSystemOptions.d.ts +1 -0
  43. package/dist/esm/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
  44. package/dist/esm/datasource/SourceFactoryProvider.d.ts +77 -0
  45. package/dist/esm/datasource/SourceFactoryProvider.d.ts.map +1 -0
  46. package/dist/esm/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
  47. package/dist/esm/flag-manager/FlagManager.d.ts +16 -1
  48. package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
  49. package/dist/esm/flag-manager/FlagPersistence.d.ts +13 -1
  50. package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
  51. package/dist/esm/flag-manager/FlagStore.d.ts +10 -0
  52. package/dist/esm/flag-manager/FlagStore.d.ts.map +1 -1
  53. package/dist/esm/flag-manager/FlagUpdater.d.ts +13 -1
  54. package/dist/esm/flag-manager/FlagUpdater.d.ts.map +1 -1
  55. package/dist/esm/index.d.ts +6 -1
  56. package/dist/esm/index.d.ts.map +1 -1
  57. package/dist/esm/index.mjs +1128 -4
  58. package/dist/esm/index.mjs.map +1 -1
  59. package/dist/esm/types/index.d.ts +2 -2
  60. package/package.json +2 -2
@@ -310,6 +310,62 @@ function validatorOf(validators, builtInDefaults) {
310
310
  },
311
311
  };
312
312
  }
313
+ /**
314
+ * Creates a validator for arrays of discriminated objects. Each item in the
315
+ * array must be an object containing a `discriminant` field whose value
316
+ * selects which validator map to apply. The valid discriminant values are
317
+ * the keys of `validatorsByType`. Items that are not objects, or whose
318
+ * discriminant value is missing or unrecognized, are filtered out with a
319
+ * warning.
320
+ *
321
+ * @param discriminant - The field name used to determine each item's type.
322
+ * @param validatorsByType - A mapping from discriminant values to the
323
+ * validator maps used to validate items of that type. Each validator map
324
+ * should include a validator for the discriminant field itself.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * // Validates an array like:
329
+ * // [{ type: 'polling', pollInterval: 60 }, { type: 'cache' }]
330
+ *
331
+ * const validator = arrayOf('type', {
332
+ * cache: { type: TypeValidators.String },
333
+ * polling: { type: TypeValidators.String, pollInterval: TypeValidators.numberWithMin(30) },
334
+ * streaming: { type: TypeValidators.String, initialReconnectDelay: TypeValidators.numberWithMin(1) },
335
+ * });
336
+ * ```
337
+ */
338
+ function arrayOf(discriminant, validatorsByType) {
339
+ return {
340
+ is: (u) => Array.isArray(u),
341
+ getType: () => 'array',
342
+ validate(value, name, logger) {
343
+ if (!Array.isArray(value)) {
344
+ logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, 'array', typeof value));
345
+ return undefined;
346
+ }
347
+ const results = [];
348
+ value.forEach((item, i) => {
349
+ const itemPath = `${name}[${i}]`;
350
+ if (jsSdkCommon.isNullish(item) || !jsSdkCommon.TypeValidators.Object.is(item)) {
351
+ logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(itemPath, 'object', typeof item));
352
+ return;
353
+ }
354
+ const obj = item;
355
+ const typeValue = obj[discriminant];
356
+ const validators = typeof typeValue === 'string' ? validatorsByType[typeValue] : undefined;
357
+ if (!validators) {
358
+ const expected = Object.keys(validatorsByType).join(' | ');
359
+ const received = typeof typeValue === 'string' ? typeValue : typeof typeValue;
360
+ logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(`${itemPath}.${discriminant}`, expected, received));
361
+ return;
362
+ }
363
+ results.push(validateOptions(obj, validators, {}, logger, itemPath));
364
+ });
365
+ return { value: results };
366
+ },
367
+ };
368
+ }
313
369
  /**
314
370
  * Creates a validator that tries each provided validator in order and uses the
315
371
  * first one whose `is()` check passes. For compound validators the value is
@@ -339,23 +395,115 @@ function anyOf(...validators) {
339
395
  },
340
396
  };
341
397
  }
398
+ /**
399
+ * Creates a validator for objects with dynamic keys. Each key in the input
400
+ * object is checked against `keyValidator`; unrecognized keys produce a
401
+ * warning. Each value is validated by `valueValidator`. Defaults for
402
+ * individual entries are passed through from the parent so partial overrides
403
+ * preserve non-overridden entries.
404
+ *
405
+ * @param keyValidator - Validates that each key is an allowed value.
406
+ * @param valueValidator - Validates each value in the record.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * // Validates a record like { streaming: { ... }, polling: { ... } }
411
+ * // where keys must be valid connection modes:
412
+ * recordOf(
413
+ * TypeValidators.oneOf('streaming', 'polling', 'offline'),
414
+ * validatorOf({ initializers: arrayValidator, synchronizers: arrayValidator }),
415
+ * )
416
+ * ```
417
+ */
418
+ function recordOf(keyValidator, valueValidator) {
419
+ return {
420
+ is: (u) => jsSdkCommon.TypeValidators.Object.is(u),
421
+ getType: () => 'object',
422
+ validate(value, name, logger, defaults) {
423
+ if (jsSdkCommon.isNullish(value)) {
424
+ return undefined;
425
+ }
426
+ if (!jsSdkCommon.TypeValidators.Object.is(value)) {
427
+ logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, 'object', typeof value));
428
+ return undefined;
429
+ }
430
+ // Filter invalid keys, then delegate to validateOptions.
431
+ const obj = value;
432
+ const filtered = {};
433
+ const validatorMap = {};
434
+ Object.keys(obj).forEach((key) => {
435
+ if (keyValidator.is(key)) {
436
+ filtered[key] = obj[key];
437
+ validatorMap[key] = valueValidator;
438
+ }
439
+ else {
440
+ logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, keyValidator.getType(), key));
441
+ }
442
+ });
443
+ const recordDefaults = jsSdkCommon.TypeValidators.Object.is(defaults)
444
+ ? defaults
445
+ : {};
446
+ return { value: validateOptions(filtered, validatorMap, recordDefaults, logger, name) };
447
+ },
448
+ };
449
+ }
342
450
 
451
+ const BACKGROUND_POLL_INTERVAL_SECONDS = 3600;
343
452
  const dataSourceTypeValidator = jsSdkCommon.TypeValidators.oneOf('cache', 'polling', 'streaming');
344
453
  const connectionModeValidator = jsSdkCommon.TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
345
454
  const endpointValidators = {
346
455
  pollingBaseUri: jsSdkCommon.TypeValidators.String,
347
456
  streamingBaseUri: jsSdkCommon.TypeValidators.String,
348
457
  };
349
- ({
458
+ const cacheEntryValidators = {
459
+ type: dataSourceTypeValidator,
460
+ };
461
+ const pollingEntryValidators = {
350
462
  type: dataSourceTypeValidator,
351
463
  pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
352
464
  endpoints: validatorOf(endpointValidators),
353
- });
354
- ({
465
+ };
466
+ const streamingEntryValidators = {
355
467
  type: dataSourceTypeValidator,
356
468
  initialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(1),
357
469
  endpoints: validatorOf(endpointValidators),
470
+ };
471
+ const initializerEntryArrayValidator = arrayOf('type', {
472
+ cache: cacheEntryValidators,
473
+ polling: pollingEntryValidators,
474
+ streaming: streamingEntryValidators,
475
+ });
476
+ const synchronizerEntryArrayValidator = arrayOf('type', {
477
+ polling: pollingEntryValidators,
478
+ streaming: streamingEntryValidators,
358
479
  });
480
+ const modeDefinitionValidators = {
481
+ initializers: initializerEntryArrayValidator,
482
+ synchronizers: synchronizerEntryArrayValidator,
483
+ };
484
+ const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
485
+ const MODE_TABLE = {
486
+ streaming: {
487
+ initializers: [{ type: 'cache' }, { type: 'polling' }],
488
+ synchronizers: [{ type: 'streaming' }, { type: 'polling' }],
489
+ },
490
+ polling: {
491
+ initializers: [{ type: 'cache' }],
492
+ synchronizers: [{ type: 'polling' }],
493
+ },
494
+ offline: {
495
+ initializers: [{ type: 'cache' }],
496
+ synchronizers: [],
497
+ },
498
+ 'one-shot': {
499
+ initializers: [{ type: 'cache' }, { type: 'polling' }, { type: 'streaming' }],
500
+ synchronizers: [],
501
+ },
502
+ background: {
503
+ initializers: [{ type: 'cache' }],
504
+ synchronizers: [{ type: 'polling', pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS }],
505
+ },
506
+ };
359
507
 
360
508
  const modeSwitchingValidators = {
361
509
  lifecycle: jsSdkCommon.TypeValidators.Boolean,
@@ -365,6 +513,7 @@ const dataSystemValidators = {
365
513
  initialConnectionMode: connectionModeValidator,
366
514
  backgroundConnectionMode: connectionModeValidator,
367
515
  automaticModeSwitching: anyOf(jsSdkCommon.TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
516
+ connectionModes: connectionModesValidator,
368
517
  };
369
518
  /**
370
519
  * Default FDv2 data system configuration for browser SDKs.
@@ -836,6 +985,33 @@ async function hashContext(crypto, context) {
836
985
  }
837
986
  return digest(crypto.createHash('sha256').update(json), 'base64');
838
987
  }
988
+ /**
989
+ * Reads the freshness timestamp from storage for the given context.
990
+ *
991
+ * Returns `undefined` if no freshness record exists, the data is corrupt,
992
+ * or the context attributes have changed since the freshness was recorded.
993
+ */
994
+ async function readFreshness(storage, crypto, environmentNamespace, context, logger) {
995
+ const contextStorageKey = await namespaceForContextData(crypto, environmentNamespace, context);
996
+ const json = await storage.get(`${contextStorageKey}${FRESHNESS_SUFFIX}`);
997
+ if (json === null || json === undefined) {
998
+ return undefined;
999
+ }
1000
+ try {
1001
+ const record = JSON.parse(json);
1002
+ const currentHash = await hashContext(crypto, context);
1003
+ if (currentHash === undefined || record.contextHash !== currentHash) {
1004
+ return undefined;
1005
+ }
1006
+ return typeof record.timestamp === 'number' && !Number.isNaN(record.timestamp)
1007
+ ? record.timestamp
1008
+ : undefined;
1009
+ }
1010
+ catch (e) {
1011
+ logger?.warn(`Could not read freshness data from persistent storage: ${e.message}`);
1012
+ return undefined;
1013
+ }
1014
+ }
839
1015
 
840
1016
  function isValidFlag(value) {
841
1017
  return value !== null && typeof value === 'object' && typeof value.version === 'number';
@@ -989,6 +1165,19 @@ class FlagPersistence {
989
1165
  }
990
1166
  return false;
991
1167
  }
1168
+ /**
1169
+ * Applies a changeset to the flag store.
1170
+ * - `'full'`: replaces all flags via {@link FlagUpdater.init}.
1171
+ * - `'partial'`: upserts individual flags via {@link FlagUpdater.upsert}.
1172
+ * - `'none'`: no flag changes, only persists cache to update freshness.
1173
+ *
1174
+ * Always persists to cache afterwards, which updates the freshness timestamp
1175
+ * even when no flags change (e.g., transfer-none).
1176
+ */
1177
+ async applyChanges(context, updates, type) {
1178
+ this._flagUpdater.applyChanges(context, updates, type);
1179
+ await this._storeCache(context);
1180
+ }
992
1181
  /**
993
1182
  * Loads the flags from persistence for the provided context and gives those to the
994
1183
  * {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
@@ -1112,6 +1301,16 @@ function createDefaultFlagStore() {
1112
1301
  getAll() {
1113
1302
  return flags;
1114
1303
  },
1304
+ applyChanges(updates, type) {
1305
+ if (type === 'full') {
1306
+ this.init(updates);
1307
+ }
1308
+ else if (type === 'partial') {
1309
+ Object.entries(updates).forEach(([key, descriptor]) => {
1310
+ flags[key] = descriptor;
1311
+ });
1312
+ }
1313
+ },
1115
1314
  };
1116
1315
  }
1117
1316
 
@@ -1169,6 +1368,24 @@ function createFlagUpdater(_flagStore, _logger) {
1169
1368
  }
1170
1369
  this.init(context, newFlags);
1171
1370
  },
1371
+ applyChanges(context, updates, type) {
1372
+ activeContext = context;
1373
+ const oldFlags = flagStore.getAll();
1374
+ flagStore.applyChanges(updates, type);
1375
+ if (type === 'full') {
1376
+ const changed = calculateChangedKeys(oldFlags, updates);
1377
+ if (changed.length > 0) {
1378
+ this.handleFlagChanges(changed, 'init');
1379
+ }
1380
+ }
1381
+ else if (type === 'partial') {
1382
+ const keys = Object.keys(updates);
1383
+ if (keys.length > 0) {
1384
+ this.handleFlagChanges(keys, 'patch');
1385
+ }
1386
+ }
1387
+ // 'none' — no flag changes, caller handles freshness.
1388
+ },
1172
1389
  upsert(context, key, item) {
1173
1390
  if (activeContext?.canonicalKey !== context.canonicalKey) {
1174
1391
  logger.warn('Received an update for an inactive context.');
@@ -1248,6 +1465,9 @@ class DefaultFlagManager {
1248
1465
  async loadCached(context) {
1249
1466
  return (await this._flagPersistencePromise).loadCached(context);
1250
1467
  }
1468
+ async applyChanges(context, updates, type) {
1469
+ return (await this._flagPersistencePromise).applyChanges(context, updates, type);
1470
+ }
1251
1471
  on(callback) {
1252
1472
  this._flagUpdater.on(callback);
1253
1473
  }
@@ -2844,6 +3064,910 @@ const DESKTOP_TRANSITION_TABLE = [
2844
3064
  { conditions: {}, mode: { configured: 'foreground' } },
2845
3065
  ];
2846
3066
 
3067
+ /**
3068
+ * Creates a change set result containing processed flag data.
3069
+ */
3070
+ function changeSet(payload, fdv1Fallback, environmentId, freshness) {
3071
+ return { type: 'changeSet', payload, fdv1Fallback, environmentId, freshness };
3072
+ }
3073
+ /**
3074
+ * Creates an interrupted status result. Indicates a transient error; the
3075
+ * synchronizer will attempt to recover automatically.
3076
+ *
3077
+ * When used with an initializer, this is still a terminal state.
3078
+ */
3079
+ function interrupted(errorInfo, fdv1Fallback) {
3080
+ return { type: 'status', state: 'interrupted', errorInfo, fdv1Fallback };
3081
+ }
3082
+ /**
3083
+ * Creates a shutdown status result. Indicates the data source was closed
3084
+ * gracefully and will not produce any further results.
3085
+ */
3086
+ function shutdown() {
3087
+ return { type: 'status', state: 'shutdown', fdv1Fallback: false };
3088
+ }
3089
+ /**
3090
+ * Creates a terminal error status result. Indicates an unrecoverable error;
3091
+ * the data source will not produce any further results.
3092
+ */
3093
+ function terminalError(errorInfo, fdv1Fallback) {
3094
+ return { type: 'status', state: 'terminal_error', errorInfo, fdv1Fallback };
3095
+ }
3096
+ /**
3097
+ * Creates a goodbye status result. Indicates the server has instructed the
3098
+ * client to disconnect.
3099
+ */
3100
+ function goodbye(reason, fdv1Fallback) {
3101
+ return { type: 'status', state: 'goodbye', reason, fdv1Fallback };
3102
+ }
3103
+ /**
3104
+ * Helper to create a {@link DataSourceStatusErrorInfo} from an HTTP status code.
3105
+ */
3106
+ function errorInfoFromHttpError(statusCode) {
3107
+ return {
3108
+ kind: jsSdkCommon.DataSourceErrorKind.ErrorResponse,
3109
+ message: `Unexpected status code: ${statusCode}`,
3110
+ statusCode,
3111
+ time: Date.now(),
3112
+ };
3113
+ }
3114
+ /**
3115
+ * Helper to create a {@link DataSourceStatusErrorInfo} from a network error.
3116
+ */
3117
+ function errorInfoFromNetworkError(message) {
3118
+ return {
3119
+ kind: jsSdkCommon.DataSourceErrorKind.NetworkError,
3120
+ message,
3121
+ time: Date.now(),
3122
+ };
3123
+ }
3124
+ /**
3125
+ * Helper to create a {@link DataSourceStatusErrorInfo} from invalid data.
3126
+ */
3127
+ function errorInfoFromInvalidData(message) {
3128
+ return {
3129
+ kind: jsSdkCommon.DataSourceErrorKind.InvalidData,
3130
+ message,
3131
+ time: Date.now(),
3132
+ };
3133
+ }
3134
+ /**
3135
+ * Helper to create a {@link DataSourceStatusErrorInfo} for unknown errors.
3136
+ */
3137
+ function errorInfoFromUnknown(message) {
3138
+ return {
3139
+ kind: jsSdkCommon.DataSourceErrorKind.Unknown,
3140
+ message,
3141
+ time: Date.now(),
3142
+ };
3143
+ }
3144
+
3145
+ /**
3146
+ * Strips the `version` field from a stored {@link Flag} to produce the
3147
+ * `FlagEvaluationResult` shape expected in an FDv2 `Update.object`.
3148
+ *
3149
+ * The version is carried on the `Update` envelope, not on the object itself.
3150
+ */
3151
+ function flagToEvaluationResult(flag) {
3152
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3153
+ const { version, ...evalResult } = flag;
3154
+ return evalResult;
3155
+ }
3156
+ /**
3157
+ * Reads cached flag data and freshness from platform storage and returns
3158
+ * them as an {@link FDv2SourceResult}.
3159
+ */
3160
+ async function loadFromCache(config) {
3161
+ const { storage, crypto, environmentNamespace, context, logger } = config;
3162
+ if (!storage) {
3163
+ logger?.debug('No storage available for cache initializer');
3164
+ return interrupted(errorInfoFromUnknown('No storage available'), false);
3165
+ }
3166
+ const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
3167
+ if (!cached) {
3168
+ logger?.debug('Cache miss for context');
3169
+ return interrupted(errorInfoFromUnknown('Cache miss'), false);
3170
+ }
3171
+ const updates = Object.entries(cached.flags).map(([key, flag]) => ({
3172
+ kind: 'flag-eval',
3173
+ key,
3174
+ version: flag.version,
3175
+ object: flagToEvaluationResult(flag),
3176
+ }));
3177
+ const payload = {
3178
+ id: 'cache',
3179
+ version: 0,
3180
+ // No `state` field. The orchestrator sees a changeSet without a selector,
3181
+ // records dataReceived=true, and continues to the next initializer.
3182
+ type: 'full',
3183
+ updates,
3184
+ };
3185
+ const freshness = await readFreshness(storage, crypto, environmentNamespace, context, logger);
3186
+ logger?.debug('Loaded cached flag evaluations via cache initializer');
3187
+ return changeSet(payload, false, undefined, freshness);
3188
+ }
3189
+ /**
3190
+ * Creates an {@link InitializerFactory} that produces cache initializers.
3191
+ *
3192
+ * The cache initializer reads flag data and freshness from persistent storage
3193
+ * for the given context and returns them as a changeSet without a selector.
3194
+ * This allows the orchestrator to provide cached data immediately while
3195
+ * continuing to the next initializer for network-verified data.
3196
+ *
3197
+ * Per spec Requirement 4.1.2, the payload has `persist=false` semantics
3198
+ * (no selector) so the consuming layer should not re-persist it.
3199
+ *
3200
+ * @internal
3201
+ */
3202
+ function createCacheInitializerFactory(config) {
3203
+ // The selectorGetter is ignored — cache data has no selector.
3204
+ return (_selectorGetter) => {
3205
+ let shutdownResolve;
3206
+ const shutdownPromise = new Promise((resolve) => {
3207
+ shutdownResolve = resolve;
3208
+ });
3209
+ return {
3210
+ async run() {
3211
+ return Promise.race([shutdownPromise, loadFromCache(config)]);
3212
+ },
3213
+ close() {
3214
+ shutdownResolve?.(shutdown());
3215
+ shutdownResolve = undefined;
3216
+ },
3217
+ };
3218
+ };
3219
+ }
3220
+
3221
+ /**
3222
+ * Creates an {@link FDv2Requestor} for client-side FDv2 polling.
3223
+ *
3224
+ * @param plainContextString The JSON-serialized evaluation context.
3225
+ * @param serviceEndpoints Service endpoint configuration.
3226
+ * @param paths FDv2 polling endpoint paths.
3227
+ * @param requests Platform HTTP abstraction.
3228
+ * @param encoding Platform encoding abstraction.
3229
+ * @param baseHeaders Default HTTP headers (e.g. authorization).
3230
+ * @param baseQueryParams Additional query parameters to include on every request.
3231
+ * @param usePost If true, use POST with context in body instead of GET with
3232
+ * context in URL path.
3233
+ */
3234
+ function makeFDv2Requestor(plainContextString, serviceEndpoints, paths, requests, encoding, baseHeaders, baseQueryParams, usePost) {
3235
+ const headers = { ...baseHeaders };
3236
+ let body;
3237
+ let method = 'GET';
3238
+ let path;
3239
+ if (usePost) {
3240
+ method = 'POST';
3241
+ headers['content-type'] = 'application/json';
3242
+ body = plainContextString;
3243
+ path = paths.pathPost(encoding, plainContextString);
3244
+ }
3245
+ else {
3246
+ path = paths.pathGet(encoding, plainContextString);
3247
+ }
3248
+ return {
3249
+ async poll(basis) {
3250
+ const parameters = [...(baseQueryParams ?? [])];
3251
+ // Intentionally falsy check: an empty string basis must not be sent as
3252
+ // a query parameter, since it does not represent a valid selector.
3253
+ if (basis) {
3254
+ parameters.push({ key: 'basis', value: basis });
3255
+ }
3256
+ const uri = jsSdkCommon.getPollingUri(serviceEndpoints, path, parameters);
3257
+ const res = await requests.fetch(uri, {
3258
+ method,
3259
+ headers,
3260
+ body,
3261
+ });
3262
+ const responseBody = res.status === 304 ? null : await res.text();
3263
+ return {
3264
+ status: res.status,
3265
+ headers: res.headers,
3266
+ body: responseBody,
3267
+ };
3268
+ },
3269
+ };
3270
+ }
3271
+
3272
+ /**
3273
+ * ObjProcessor for the `flag-eval` object kind. Used by the protocol handler to
3274
+ * process objects received in `put-object` events.
3275
+ *
3276
+ * Client-side evaluation results are already in their final form (pre-evaluated
3277
+ * by the server), so no transformation is needed — this is a passthrough.
3278
+ */
3279
+ function processFlagEval(object) {
3280
+ return object;
3281
+ }
3282
+
3283
+ function getFallback(headers) {
3284
+ const value = headers.get('x-ld-fd-fallback');
3285
+ return value !== null && value.toLowerCase() === 'true';
3286
+ }
3287
+ function getEnvironmentId(headers) {
3288
+ return headers.get('x-ld-envid') ?? undefined;
3289
+ }
3290
+ /**
3291
+ * Process FDv2 events using the protocol handler directly.
3292
+ *
3293
+ * We use `createProtocolHandler` rather than `PayloadProcessor` because
3294
+ * the PayloadProcessor does not surface goodbye/serverError actions —
3295
+ * it only forwards payloads and actionable errors. For polling results,
3296
+ * we need full control over all protocol action types.
3297
+ */
3298
+ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
3299
+ const handler = jsSdkCommon.internal.createProtocolHandler({
3300
+ 'flag-eval': processFlagEval,
3301
+ }, logger);
3302
+ let earlyResult;
3303
+ events.forEach((event) => {
3304
+ if (earlyResult) {
3305
+ return;
3306
+ }
3307
+ const action = handler.processEvent(event);
3308
+ switch (action.type) {
3309
+ case 'payload':
3310
+ earlyResult = changeSet(action.payload, fdv1Fallback, environmentId);
3311
+ break;
3312
+ case 'goodbye':
3313
+ earlyResult = goodbye(action.reason, fdv1Fallback);
3314
+ break;
3315
+ case 'serverError': {
3316
+ const errorInfo = errorInfoFromUnknown(action.reason);
3317
+ logger?.error(`Server error during polling: ${action.reason}`);
3318
+ earlyResult = oneShot
3319
+ ? terminalError(errorInfo, fdv1Fallback)
3320
+ : interrupted(errorInfo, fdv1Fallback);
3321
+ break;
3322
+ }
3323
+ case 'error': {
3324
+ // Actionable protocol errors (MISSING_PAYLOAD, PROTOCOL_ERROR)
3325
+ if (action.kind === 'MISSING_PAYLOAD' || action.kind === 'PROTOCOL_ERROR') {
3326
+ const errorInfo = errorInfoFromInvalidData(action.message);
3327
+ logger?.warn(`Protocol error during polling: ${action.message}`);
3328
+ earlyResult = oneShot
3329
+ ? terminalError(errorInfo, fdv1Fallback)
3330
+ : interrupted(errorInfo, fdv1Fallback);
3331
+ }
3332
+ else {
3333
+ // Non-actionable errors (UNKNOWN_EVENT) are logged but don't stop processing
3334
+ logger?.warn(action.message);
3335
+ }
3336
+ break;
3337
+ }
3338
+ }
3339
+ });
3340
+ if (earlyResult) {
3341
+ return earlyResult;
3342
+ }
3343
+ // Events didn't produce a result
3344
+ const errorInfo = errorInfoFromUnknown('Unexpected end of polling response');
3345
+ logger?.error('Unexpected end of polling response');
3346
+ return oneShot ? terminalError(errorInfo, fdv1Fallback) : interrupted(errorInfo, fdv1Fallback);
3347
+ }
3348
+ /**
3349
+ * Performs a single FDv2 poll request, processes the protocol response, and
3350
+ * returns an {@link FDv2SourceResult}.
3351
+ *
3352
+ * The `oneShot` parameter controls error handling: when true (initializer),
3353
+ * all errors are terminal; when false (synchronizer), recoverable errors
3354
+ * produce interrupted results.
3355
+ *
3356
+ * @internal
3357
+ */
3358
+ async function poll(requestor, basis, oneShot, logger) {
3359
+ let fdv1Fallback = false;
3360
+ let environmentId;
3361
+ try {
3362
+ const response = await requestor.poll(basis);
3363
+ fdv1Fallback = getFallback(response.headers);
3364
+ environmentId = getEnvironmentId(response.headers);
3365
+ // 304 Not Modified: treat as server-intent with intentCode 'none'
3366
+ // (Spec Requirement 10.1.2)
3367
+ if (response.status === 304) {
3368
+ const nonePayload = {
3369
+ id: '',
3370
+ version: 0,
3371
+ type: 'none',
3372
+ updates: [],
3373
+ };
3374
+ return changeSet(nonePayload, fdv1Fallback, environmentId);
3375
+ }
3376
+ // Non-success HTTP status
3377
+ if (response.status < 200 || response.status >= 300) {
3378
+ const errorInfo = errorInfoFromHttpError(response.status);
3379
+ logger?.error(`Polling request failed with HTTP error: ${response.status}`);
3380
+ if (oneShot) {
3381
+ return terminalError(errorInfo, fdv1Fallback);
3382
+ }
3383
+ const recoverable = response.status <= 0 || jsSdkCommon.isHttpRecoverable(response.status);
3384
+ return recoverable
3385
+ ? interrupted(errorInfo, fdv1Fallback)
3386
+ : terminalError(errorInfo, fdv1Fallback);
3387
+ }
3388
+ // Successful response — process FDv2 events
3389
+ if (!response.body) {
3390
+ const errorInfo = errorInfoFromInvalidData('Empty response body');
3391
+ logger?.error('Polling request received empty response body');
3392
+ return oneShot
3393
+ ? terminalError(errorInfo, fdv1Fallback)
3394
+ : interrupted(errorInfo, fdv1Fallback);
3395
+ }
3396
+ let parsed;
3397
+ try {
3398
+ parsed = JSON.parse(response.body);
3399
+ }
3400
+ catch {
3401
+ const errorInfo = errorInfoFromInvalidData('Malformed JSON data in polling response');
3402
+ logger?.error('Polling request received malformed data');
3403
+ return oneShot
3404
+ ? terminalError(errorInfo, fdv1Fallback)
3405
+ : interrupted(errorInfo, fdv1Fallback);
3406
+ }
3407
+ if (!Array.isArray(parsed.events)) {
3408
+ const errorInfo = errorInfoFromInvalidData('Invalid polling response: missing or invalid events array');
3409
+ logger?.error('Polling response does not contain a valid events array');
3410
+ return oneShot
3411
+ ? terminalError(errorInfo, fdv1Fallback)
3412
+ : interrupted(errorInfo, fdv1Fallback);
3413
+ }
3414
+ return processEvents(parsed.events, oneShot, fdv1Fallback, environmentId, logger);
3415
+ }
3416
+ catch (err) {
3417
+ // Network or other I/O error from the fetch itself
3418
+ const message = err?.message ?? String(err);
3419
+ logger?.error(`Polling request failed with network error: ${message}`);
3420
+ const errorInfo = errorInfoFromNetworkError(message);
3421
+ return oneShot ? terminalError(errorInfo, fdv1Fallback) : interrupted(errorInfo, fdv1Fallback);
3422
+ }
3423
+ }
3424
+
3425
+ /**
3426
+ * Creates a one-shot polling initializer that performs a single FDv2 poll
3427
+ * request and returns the result.
3428
+ *
3429
+ * All errors are treated as terminal (oneShot=true). If `close()` is called
3430
+ * before the poll completes, the result will be a shutdown status.
3431
+ *
3432
+ * @internal
3433
+ */
3434
+ function createPollingInitializer(requestor, logger, selectorGetter) {
3435
+ let shutdownResolve;
3436
+ const shutdownPromise = new Promise((resolve) => {
3437
+ shutdownResolve = resolve;
3438
+ });
3439
+ return {
3440
+ async run() {
3441
+ const pollResult = poll(requestor, selectorGetter(), true, logger);
3442
+ // Race the poll against the shutdown signal
3443
+ return Promise.race([shutdownPromise, pollResult]);
3444
+ },
3445
+ close() {
3446
+ shutdownResolve?.(shutdown());
3447
+ shutdownResolve = undefined;
3448
+ },
3449
+ };
3450
+ }
3451
+
3452
+ /**
3453
+ * Creates a new {@link AsyncQueue}.
3454
+ *
3455
+ * @internal
3456
+ */
3457
+ function createAsyncQueue() {
3458
+ const items = [];
3459
+ const waiters = [];
3460
+ return {
3461
+ put(item) {
3462
+ const waiter = waiters.shift();
3463
+ if (waiter) {
3464
+ waiter(item);
3465
+ }
3466
+ else {
3467
+ items.push(item);
3468
+ }
3469
+ },
3470
+ take() {
3471
+ if (items.length > 0) {
3472
+ return Promise.resolve(items.shift());
3473
+ }
3474
+ return new Promise((resolve) => {
3475
+ waiters.push(resolve);
3476
+ });
3477
+ },
3478
+ };
3479
+ }
3480
+
3481
+ /**
3482
+ * Creates a continuous polling synchronizer that periodically polls for FDv2
3483
+ * data and yields results via successive calls to `next()`.
3484
+ *
3485
+ * The first poll fires immediately. Subsequent polls are scheduled using
3486
+ * `setTimeout` after each poll completes, ensuring sequential execution and
3487
+ * preventing overlapping requests on slow networks.
3488
+ *
3489
+ * Results are buffered in an async queue. On terminal errors, polling stops
3490
+ * and the shutdown future is resolved. On `close()`, polling stops and the
3491
+ * next `next()` call returns a shutdown result.
3492
+ *
3493
+ * @internal
3494
+ */
3495
+ function createPollingSynchronizer(requestor, logger, selectorGetter, pollIntervalMs) {
3496
+ const resultQueue = createAsyncQueue();
3497
+ let shutdownResolve;
3498
+ const shutdownPromise = new Promise((resolve) => {
3499
+ shutdownResolve = resolve;
3500
+ });
3501
+ let timeoutHandle;
3502
+ let stopped = false;
3503
+ async function doPoll() {
3504
+ if (stopped) {
3505
+ return;
3506
+ }
3507
+ const startTime = Date.now();
3508
+ try {
3509
+ const result = await poll(requestor, selectorGetter(), false, logger);
3510
+ if (stopped) {
3511
+ return;
3512
+ }
3513
+ let shouldShutdown = false;
3514
+ if (result.type === 'status') {
3515
+ switch (result.state) {
3516
+ case 'terminal_error':
3517
+ stopped = true;
3518
+ shouldShutdown = true;
3519
+ break;
3520
+ case 'interrupted':
3521
+ case 'goodbye':
3522
+ // Continue polling on transient errors and goodbyes
3523
+ break;
3524
+ case 'shutdown':
3525
+ // The base poll function doesn't emit shutdown; we handle it
3526
+ // at this level via close().
3527
+ break;
3528
+ default:
3529
+ break;
3530
+ }
3531
+ }
3532
+ if (shouldShutdown) {
3533
+ shutdownResolve?.(result);
3534
+ shutdownResolve = undefined;
3535
+ }
3536
+ else {
3537
+ resultQueue.put(result);
3538
+ }
3539
+ }
3540
+ catch (err) {
3541
+ logger?.debug(`Polling error: ${err}`);
3542
+ }
3543
+ // Schedule next poll after completion, accounting for elapsed time.
3544
+ // This ensures sequential execution — no overlapping requests.
3545
+ if (!stopped) {
3546
+ const elapsed = Date.now() - startTime;
3547
+ const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
3548
+ timeoutHandle = setTimeout(() => {
3549
+ doPoll();
3550
+ }, sleepFor);
3551
+ }
3552
+ }
3553
+ // Start polling immediately
3554
+ doPoll();
3555
+ return {
3556
+ async next() {
3557
+ return Promise.race([shutdownPromise, resultQueue.take()]);
3558
+ },
3559
+ close() {
3560
+ stopped = true;
3561
+ if (timeoutHandle !== undefined) {
3562
+ clearTimeout(timeoutHandle);
3563
+ timeoutHandle = undefined;
3564
+ }
3565
+ shutdownResolve?.(shutdown());
3566
+ shutdownResolve = undefined;
3567
+ },
3568
+ };
3569
+ }
3570
+
3571
+ /**
3572
+ * Creates a {@link SynchronizerSlot}.
3573
+ *
3574
+ * @param factory The synchronizer factory function.
3575
+ * @param options Optional configuration.
3576
+ * @param options.isFDv1Fallback Whether this slot is the FDv1 fallback adapter.
3577
+ * FDv1 slots start `'blocked'` by default.
3578
+ * @param options.initialState Override the initial state (defaults to
3579
+ * `'blocked'` for FDv1 slots, `'available'` otherwise).
3580
+ */
3581
+ function createSynchronizerSlot(factory, options) {
3582
+ const isFDv1Fallback = options?.isFDv1Fallback ?? false;
3583
+ const state = options?.initialState ?? (isFDv1Fallback ? 'blocked' : 'available');
3584
+ return { factory, isFDv1Fallback, state };
3585
+ }
3586
+
3587
+ /**
3588
+ * FDv2 event names to listen for on the EventSource. This must stay in sync
3589
+ * with the `EventType` union defined in `@launchdarkly/js-sdk-common`'s
3590
+ * `internal/fdv2/proto.ts`.
3591
+ */
3592
+ const FDV2_EVENT_NAMES = [
3593
+ 'server-intent',
3594
+ 'put-object',
3595
+ 'delete-object',
3596
+ 'payload-transferred',
3597
+ 'goodbye',
3598
+ 'error',
3599
+ 'heart-beat',
3600
+ ];
3601
+ /**
3602
+ * Creates the core streaming base for FDv2 client-side data sources.
3603
+ *
3604
+ * Manages an EventSource connection, processes FDv2 protocol events using
3605
+ * a protocol handler from the common package, detects FDv1 fallback signals,
3606
+ * handles legacy ping events, and queues results for consumption by
3607
+ * {@link createStreamingInitializer} or {@link createStreamingSynchronizer}.
3608
+ *
3609
+ * @internal
3610
+ */
3611
+ function createStreamingBase(config) {
3612
+ const resultQueue = createAsyncQueue();
3613
+ const protocolHandler = jsSdkCommon.internal.createProtocolHandler({ 'flag-eval': processFlagEval }, config.logger);
3614
+ const headers = { ...config.headers };
3615
+ function buildStreamUri() {
3616
+ const params = [...config.parameters];
3617
+ const basis = config.selectorGetter?.();
3618
+ if (basis) {
3619
+ params.push({ key: 'basis', value: encodeURIComponent(basis) });
3620
+ }
3621
+ return jsSdkCommon.getStreamingUri(config.serviceEndpoints, config.streamUriPath, params);
3622
+ }
3623
+ let eventSource;
3624
+ let connectionAttemptStartTime;
3625
+ let fdv1Fallback = false;
3626
+ let started = false;
3627
+ let stopped = false;
3628
+ function logConnectionAttempt() {
3629
+ connectionAttemptStartTime = Date.now();
3630
+ }
3631
+ function logConnectionResult(success) {
3632
+ if (connectionAttemptStartTime && config.diagnosticsManager) {
3633
+ config.diagnosticsManager.recordStreamInit(connectionAttemptStartTime, !success, Date.now() - connectionAttemptStartTime);
3634
+ }
3635
+ connectionAttemptStartTime = undefined;
3636
+ }
3637
+ function handleAction(action) {
3638
+ switch (action.type) {
3639
+ case 'payload':
3640
+ logConnectionResult(true);
3641
+ resultQueue.put(changeSet(action.payload, fdv1Fallback));
3642
+ break;
3643
+ case 'goodbye':
3644
+ resultQueue.put(goodbye(action.reason, fdv1Fallback));
3645
+ break;
3646
+ case 'serverError':
3647
+ resultQueue.put(interrupted(errorInfoFromUnknown(action.reason), fdv1Fallback));
3648
+ break;
3649
+ case 'error':
3650
+ // Only actionable errors are queued; informational ones (UNKNOWN_EVENT)
3651
+ // are logged by the protocol handler.
3652
+ if (action.kind === 'MISSING_PAYLOAD' || action.kind === 'PROTOCOL_ERROR') {
3653
+ resultQueue.put(interrupted(errorInfoFromInvalidData(action.message), fdv1Fallback));
3654
+ }
3655
+ break;
3656
+ }
3657
+ }
3658
+ function handleError(err) {
3659
+ // Check for FDv1 fallback header.
3660
+ if (err.headers?.['x-ld-fd-fallback'] === 'true') {
3661
+ fdv1Fallback = true;
3662
+ logConnectionResult(false);
3663
+ resultQueue.put(terminalError(errorInfoFromHttpError(err.status ?? 0), true));
3664
+ return false;
3665
+ }
3666
+ if (!jsSdkCommon.shouldRetry(err)) {
3667
+ config.logger?.error(jsSdkCommon.httpErrorMessage(err, 'streaming request'));
3668
+ logConnectionResult(false);
3669
+ resultQueue.put(terminalError(errorInfoFromHttpError(err.status ?? 0), fdv1Fallback));
3670
+ return false;
3671
+ }
3672
+ config.logger?.warn(jsSdkCommon.httpErrorMessage(err, 'streaming request', 'will retry'));
3673
+ logConnectionResult(false);
3674
+ logConnectionAttempt();
3675
+ resultQueue.put(interrupted(errorInfoFromHttpError(err.status ?? 0), fdv1Fallback));
3676
+ return true;
3677
+ }
3678
+ function attachFDv2Listeners(es) {
3679
+ FDV2_EVENT_NAMES.forEach((eventName) => {
3680
+ es.addEventListener(eventName, (event) => {
3681
+ if (stopped) {
3682
+ config.logger?.debug(`Received ${eventName} event after processor was closed. Skipping.`);
3683
+ return;
3684
+ }
3685
+ if (!event?.data) {
3686
+ // Some events (e.g. 'error') may legitimately arrive without a body.
3687
+ if (eventName !== 'error') {
3688
+ config.logger?.warn(`Event from EventStream missing data for "${eventName}".`);
3689
+ }
3690
+ return;
3691
+ }
3692
+ config.logger?.debug(`Received ${eventName} event`);
3693
+ let parsed;
3694
+ try {
3695
+ parsed = JSON.parse(event.data);
3696
+ }
3697
+ catch {
3698
+ config.logger?.error(`Stream received data that was unable to be parsed in "${eventName}" message`);
3699
+ config.logger?.debug(`Data follows: ${event.data}`);
3700
+ resultQueue.put(interrupted(errorInfoFromInvalidData('Malformed JSON in EventStream'), fdv1Fallback));
3701
+ return;
3702
+ }
3703
+ const action = protocolHandler.processEvent({ event: eventName, data: parsed });
3704
+ handleAction(action);
3705
+ });
3706
+ });
3707
+ }
3708
+ function attachPingListener(es) {
3709
+ es.addEventListener('ping', async () => {
3710
+ if (stopped) {
3711
+ config.logger?.debug('Ping received after processor was closed. Skipping.');
3712
+ return;
3713
+ }
3714
+ config.logger?.debug('Got PING, going to poll LaunchDarkly for feature flag updates');
3715
+ if (!config.pingHandler) {
3716
+ config.logger?.warn('Ping event received but no ping handler configured.');
3717
+ return;
3718
+ }
3719
+ try {
3720
+ const result = await config.pingHandler.handlePing();
3721
+ if (stopped) {
3722
+ config.logger?.debug('Ping completed after processor was closed. Skipping processing.');
3723
+ return;
3724
+ }
3725
+ resultQueue.put(result);
3726
+ }
3727
+ catch (err) {
3728
+ if (stopped) {
3729
+ return;
3730
+ }
3731
+ config.logger?.error(`Error handling ping: ${err?.message ?? err}`);
3732
+ resultQueue.put(interrupted(errorInfoFromNetworkError(err?.message ?? 'Error during ping poll'), fdv1Fallback));
3733
+ }
3734
+ });
3735
+ }
3736
+ return {
3737
+ start() {
3738
+ if (started || stopped) {
3739
+ return;
3740
+ }
3741
+ started = true;
3742
+ logConnectionAttempt();
3743
+ const es = config.requests.createEventSource(buildStreamUri(), {
3744
+ headers,
3745
+ errorFilter: (error) => handleError(error),
3746
+ initialRetryDelayMillis: config.initialRetryDelayMillis,
3747
+ readTimeoutMillis: 5 * 60 * 1000,
3748
+ retryResetIntervalMillis: 60 * 1000,
3749
+ });
3750
+ eventSource = es;
3751
+ attachFDv2Listeners(es);
3752
+ attachPingListener(es);
3753
+ es.onclose = () => {
3754
+ config.logger?.info('Closed LaunchDarkly stream connection');
3755
+ };
3756
+ es.onerror = (err) => {
3757
+ if (stopped) {
3758
+ return;
3759
+ }
3760
+ if (err && typeof err.status === 'number') {
3761
+ // This condition will be handled by the error filter.
3762
+ return;
3763
+ }
3764
+ resultQueue.put(interrupted(errorInfoFromNetworkError(err?.message ?? 'IO Error'), fdv1Fallback));
3765
+ };
3766
+ es.onopen = () => {
3767
+ config.logger?.info('Opened LaunchDarkly stream connection');
3768
+ protocolHandler.reset();
3769
+ };
3770
+ es.onretrying = (e) => {
3771
+ config.logger?.info(`Will retry stream connection in ${e.delayMillis} milliseconds`);
3772
+ };
3773
+ },
3774
+ close() {
3775
+ if (stopped) {
3776
+ return;
3777
+ }
3778
+ stopped = true;
3779
+ eventSource?.close();
3780
+ eventSource = undefined;
3781
+ resultQueue.put(shutdown());
3782
+ },
3783
+ takeResult() {
3784
+ return resultQueue.take();
3785
+ },
3786
+ };
3787
+ }
3788
+
3789
+ /**
3790
+ * Creates a one-shot streaming initializer for FDv2.
3791
+ *
3792
+ * Connects to the FDv2 streaming endpoint, waits for the first result
3793
+ * (a change set or an error status), then disconnects. Used in browser
3794
+ * `one-shot` mode where a persistent connection is not desired.
3795
+ *
3796
+ * If `close()` is called before a result arrives, the returned promise
3797
+ * resolves with a shutdown result.
3798
+ *
3799
+ * @param base - The streaming base that manages the EventSource connection.
3800
+ * @internal
3801
+ */
3802
+ function createStreamingInitializer(base) {
3803
+ let closed = false;
3804
+ let shutdownResolve;
3805
+ const shutdownPromise = new Promise((resolve) => {
3806
+ shutdownResolve = resolve;
3807
+ });
3808
+ return {
3809
+ run() {
3810
+ if (closed) {
3811
+ return Promise.resolve(shutdown());
3812
+ }
3813
+ base.start();
3814
+ return Promise.race([
3815
+ base.takeResult().then((result) => {
3816
+ // Got our first result — close the connection.
3817
+ base.close();
3818
+ return result;
3819
+ }),
3820
+ shutdownPromise,
3821
+ ]);
3822
+ },
3823
+ close() {
3824
+ if (closed) {
3825
+ return;
3826
+ }
3827
+ closed = true;
3828
+ base.close();
3829
+ shutdownResolve?.(shutdown());
3830
+ shutdownResolve = undefined;
3831
+ },
3832
+ };
3833
+ }
3834
+
3835
+ /**
3836
+ * Creates a long-lived streaming synchronizer for FDv2.
3837
+ *
3838
+ * Maintains a persistent EventSource connection to the FDv2 streaming
3839
+ * endpoint and produces a stream of results via the pull-based `next()`
3840
+ * method. Used in streaming connection mode.
3841
+ *
3842
+ * The connection is started lazily on the first call to `next()`.
3843
+ * On `close()`, the next call to `next()` returns a shutdown result.
3844
+ *
3845
+ * @param base - The streaming base that manages the EventSource connection.
3846
+ * @internal
3847
+ */
3848
+ function createStreamingSynchronizer(base) {
3849
+ let started = false;
3850
+ let closed = false;
3851
+ let shutdownResolve;
3852
+ const shutdownPromise = new Promise((resolve) => {
3853
+ shutdownResolve = resolve;
3854
+ });
3855
+ return {
3856
+ next() {
3857
+ if (closed) {
3858
+ return Promise.resolve(shutdown());
3859
+ }
3860
+ if (!started) {
3861
+ started = true;
3862
+ base.start();
3863
+ }
3864
+ return Promise.race([base.takeResult(), shutdownPromise]);
3865
+ },
3866
+ close() {
3867
+ if (closed) {
3868
+ return;
3869
+ }
3870
+ closed = true;
3871
+ base.close();
3872
+ shutdownResolve?.(shutdown());
3873
+ shutdownResolve = undefined;
3874
+ },
3875
+ };
3876
+ }
3877
+
3878
+ function createPingHandler(requestor, selectorGetter, logger) {
3879
+ return {
3880
+ handlePing: () => poll(requestor, selectorGetter(), false, logger),
3881
+ };
3882
+ }
3883
+ /**
3884
+ * Create a {@link ServiceEndpoints} with per-entry endpoint overrides applied.
3885
+ * Returns the original endpoints if no overrides are specified.
3886
+ */
3887
+ function resolveEndpoints(ctx, endpoints) {
3888
+ if (!endpoints?.pollingBaseUri && !endpoints?.streamingBaseUri) {
3889
+ return ctx.serviceEndpoints;
3890
+ }
3891
+ return new jsSdkCommon.ServiceEndpoints(endpoints.streamingBaseUri ?? ctx.serviceEndpoints.streaming, endpoints.pollingBaseUri ?? ctx.serviceEndpoints.polling, ctx.serviceEndpoints.events, ctx.serviceEndpoints.analyticsEventPath, ctx.serviceEndpoints.diagnosticEventPath, ctx.serviceEndpoints.includeAuthorizationHeader, ctx.serviceEndpoints.payloadFilterKey);
3892
+ }
3893
+ /**
3894
+ * Get the FDv2 requestor for a polling entry. If the entry has custom
3895
+ * endpoints, creates a new requestor targeting those endpoints. Otherwise
3896
+ * returns the shared requestor from the context.
3897
+ */
3898
+ function resolvePollingRequestor(ctx, endpoints) {
3899
+ if (!endpoints?.pollingBaseUri) {
3900
+ return ctx.requestor;
3901
+ }
3902
+ const overriddenEndpoints = resolveEndpoints(ctx, endpoints);
3903
+ return makeFDv2Requestor(ctx.plainContextString, overriddenEndpoints, ctx.polling.paths, ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams);
3904
+ }
3905
+ /**
3906
+ * Build a streaming base instance using per-entry config with context defaults
3907
+ * as fallbacks. The `sg` selector getter is the canonical source of truth for
3908
+ * the current selector — both the stream and its ping handler use it.
3909
+ */
3910
+ function buildStreamingBase(entry, ctx, sg) {
3911
+ const entryEndpoints = resolveEndpoints(ctx, entry.endpoints);
3912
+ const requestor = resolvePollingRequestor(ctx, entry.endpoints);
3913
+ const streamUriPath = ctx.streaming.paths.pathGet(ctx.encoding, ctx.plainContextString);
3914
+ return createStreamingBase({
3915
+ requests: ctx.requests,
3916
+ serviceEndpoints: entryEndpoints,
3917
+ streamUriPath,
3918
+ parameters: ctx.queryParams,
3919
+ selectorGetter: sg,
3920
+ headers: ctx.baseHeaders,
3921
+ initialRetryDelayMillis: (entry.initialReconnectDelay ?? ctx.streaming.initialReconnectDelaySeconds) * 1000,
3922
+ logger: ctx.logger,
3923
+ pingHandler: createPingHandler(requestor, sg, ctx.logger),
3924
+ });
3925
+ }
3926
+ /**
3927
+ * Creates a {@link SourceFactoryProvider} that handles `cache`, `polling`,
3928
+ * and `streaming` data source entries.
3929
+ */
3930
+ function createDefaultSourceFactoryProvider() {
3931
+ return {
3932
+ createInitializerFactory(entry, ctx) {
3933
+ switch (entry.type) {
3934
+ case 'polling': {
3935
+ const requestor = resolvePollingRequestor(ctx, entry.endpoints);
3936
+ return (sg) => createPollingInitializer(requestor, ctx.logger, sg);
3937
+ }
3938
+ case 'streaming':
3939
+ return (sg) => createStreamingInitializer(buildStreamingBase(entry, ctx, sg));
3940
+ case 'cache':
3941
+ return createCacheInitializerFactory({
3942
+ storage: ctx.storage,
3943
+ crypto: ctx.crypto,
3944
+ environmentNamespace: ctx.environmentNamespace,
3945
+ context: ctx.context,
3946
+ logger: ctx.logger,
3947
+ });
3948
+ default:
3949
+ return undefined;
3950
+ }
3951
+ },
3952
+ createSynchronizerSlot(entry, ctx) {
3953
+ switch (entry.type) {
3954
+ case 'polling': {
3955
+ const intervalMs = (entry.pollInterval ?? ctx.polling.intervalSeconds) * 1000;
3956
+ const requestor = resolvePollingRequestor(ctx, entry.endpoints);
3957
+ const factory = (sg) => createPollingSynchronizer(requestor, ctx.logger, sg, intervalMs);
3958
+ return createSynchronizerSlot(factory);
3959
+ }
3960
+ case 'streaming': {
3961
+ const factory = (sg) => createStreamingSynchronizer(buildStreamingBase(entry, ctx, sg));
3962
+ return createSynchronizerSlot(factory);
3963
+ }
3964
+ default:
3965
+ return undefined;
3966
+ }
3967
+ },
3968
+ };
3969
+ }
3970
+
2847
3971
  exports.platform = jsSdkCommon__namespace;
2848
3972
  exports.BROWSER_DATA_SYSTEM_DEFAULTS = BROWSER_DATA_SYSTEM_DEFAULTS;
2849
3973
  exports.BROWSER_TRANSITION_TABLE = BROWSER_TRANSITION_TABLE;
@@ -2854,7 +3978,10 @@ exports.DataSourceState = DataSourceState;
2854
3978
  exports.LDClientImpl = LDClientImpl;
2855
3979
  exports.MOBILE_DATA_SYSTEM_DEFAULTS = MOBILE_DATA_SYSTEM_DEFAULTS;
2856
3980
  exports.MOBILE_TRANSITION_TABLE = MOBILE_TRANSITION_TABLE;
3981
+ exports.MODE_TABLE = MODE_TABLE;
2857
3982
  exports.browserFdv1Endpoints = browserFdv1Endpoints;
3983
+ exports.createDataSourceStatusManager = createDataSourceStatusManager;
3984
+ exports.createDefaultSourceFactoryProvider = createDefaultSourceFactoryProvider;
2858
3985
  exports.dataSystemValidators = dataSystemValidators;
2859
3986
  exports.fdv2Endpoints = fdv2Endpoints;
2860
3987
  exports.makeRequestor = makeRequestor;