@launchdarkly/js-client-sdk-common 1.21.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.
- package/CHANGELOG.md +36 -0
- package/dist/cjs/DataManager.d.ts +24 -0
- package/dist/cjs/DataManager.d.ts.map +1 -1
- package/dist/cjs/LDClientImpl.d.ts.map +1 -1
- package/dist/cjs/api/LDOptions.d.ts +8 -0
- package/dist/cjs/api/LDOptions.d.ts.map +1 -1
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts +11 -0
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts +22 -0
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/api/datasource/ModeDefinition.d.ts +3 -3
- package/dist/cjs/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/cjs/api/datasource/ModeResolution.d.ts +75 -0
- package/dist/cjs/api/datasource/ModeResolution.d.ts.map +1 -0
- package/dist/cjs/api/datasource/index.d.ts +2 -1
- package/dist/cjs/api/datasource/index.d.ts.map +1 -1
- package/dist/cjs/configuration/Configuration.d.ts +6 -0
- package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
- package/dist/cjs/configuration/validateOptions.d.ts +1 -1
- package/dist/cjs/configuration/validateOptions.d.ts.map +1 -1
- package/dist/cjs/configuration/validators.d.ts +5 -2
- package/dist/cjs/configuration/validators.d.ts.map +1 -1
- package/dist/cjs/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts +1 -0
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/datasource/ModeResolver.d.ts +30 -0
- package/dist/cjs/datasource/ModeResolver.d.ts.map +1 -0
- package/dist/cjs/datasource/SourceFactoryProvider.d.ts +77 -0
- package/dist/cjs/datasource/SourceFactoryProvider.d.ts.map +1 -0
- package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts +17 -0
- package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -0
- package/dist/cjs/datasource/fdv2/FDv1PollingSynchronizer.d.ts +2 -0
- package/dist/cjs/datasource/fdv2/FDv1PollingSynchronizer.d.ts.map +1 -0
- package/dist/cjs/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/FDv2SourceResult.d.ts +3 -1
- package/dist/cjs/datasource/fdv2/FDv2SourceResult.d.ts.map +1 -1
- package/dist/cjs/datasource/fdv2/calculatePollDelay.d.ts +2 -0
- package/dist/cjs/datasource/fdv2/calculatePollDelay.d.ts.map +1 -0
- package/dist/cjs/datasource/fdv2/index.d.ts +4 -0
- package/dist/cjs/datasource/fdv2/index.d.ts.map +1 -1
- package/dist/cjs/datasource/flagEvalMapper.d.ts +3 -3
- package/dist/cjs/flag-manager/FlagManager.d.ts +18 -2
- package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/cjs/flag-manager/FlagPersistence.d.ts +21 -2
- package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/cjs/flag-manager/FlagStore.d.ts +10 -0
- package/dist/cjs/flag-manager/FlagStore.d.ts.map +1 -1
- package/dist/cjs/flag-manager/FlagUpdater.d.ts +13 -1
- package/dist/cjs/flag-manager/FlagUpdater.d.ts.map +1 -1
- package/dist/cjs/index.cjs +1463 -60
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +8 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/storage/freshness.d.ts +27 -0
- package/dist/cjs/storage/freshness.d.ts.map +1 -0
- package/dist/cjs/storage/loadCachedFlags.d.ts +25 -0
- package/dist/cjs/storage/loadCachedFlags.d.ts.map +1 -0
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/esm/DataManager.d.ts +24 -0
- package/dist/esm/DataManager.d.ts.map +1 -1
- package/dist/esm/LDClientImpl.d.ts.map +1 -1
- package/dist/esm/api/LDOptions.d.ts +8 -0
- package/dist/esm/api/LDOptions.d.ts.map +1 -1
- package/dist/esm/api/datasource/DataSourceEntry.d.ts +11 -0
- package/dist/esm/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts +22 -0
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/api/datasource/ModeDefinition.d.ts +3 -3
- package/dist/esm/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/esm/api/datasource/ModeResolution.d.ts +75 -0
- package/dist/esm/api/datasource/ModeResolution.d.ts.map +1 -0
- package/dist/esm/api/datasource/index.d.ts +2 -1
- package/dist/esm/api/datasource/index.d.ts.map +1 -1
- package/dist/esm/configuration/Configuration.d.ts +6 -0
- package/dist/esm/configuration/Configuration.d.ts.map +1 -1
- package/dist/esm/configuration/validateOptions.d.ts +1 -1
- package/dist/esm/configuration/validateOptions.d.ts.map +1 -1
- package/dist/esm/configuration/validators.d.ts +5 -2
- package/dist/esm/configuration/validators.d.ts.map +1 -1
- package/dist/esm/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts +1 -0
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/datasource/ModeResolver.d.ts +30 -0
- package/dist/esm/datasource/ModeResolver.d.ts.map +1 -0
- package/dist/esm/datasource/SourceFactoryProvider.d.ts +77 -0
- package/dist/esm/datasource/SourceFactoryProvider.d.ts.map +1 -0
- package/dist/esm/datasource/fdv2/CacheInitializer.d.ts +17 -0
- package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -0
- package/dist/esm/datasource/fdv2/FDv1PollingSynchronizer.d.ts +2 -0
- package/dist/esm/datasource/fdv2/FDv1PollingSynchronizer.d.ts.map +1 -0
- package/dist/esm/datasource/fdv2/FDv2DataSource.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/FDv2SourceResult.d.ts +3 -1
- package/dist/esm/datasource/fdv2/FDv2SourceResult.d.ts.map +1 -1
- package/dist/esm/datasource/fdv2/calculatePollDelay.d.ts +2 -0
- package/dist/esm/datasource/fdv2/calculatePollDelay.d.ts.map +1 -0
- package/dist/esm/datasource/fdv2/index.d.ts +4 -0
- package/dist/esm/datasource/fdv2/index.d.ts.map +1 -1
- package/dist/esm/datasource/flagEvalMapper.d.ts +3 -3
- package/dist/esm/flag-manager/FlagManager.d.ts +18 -2
- package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/esm/flag-manager/FlagPersistence.d.ts +21 -2
- package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/esm/flag-manager/FlagStore.d.ts +10 -0
- package/dist/esm/flag-manager/FlagStore.d.ts.map +1 -1
- package/dist/esm/flag-manager/FlagUpdater.d.ts +13 -1
- package/dist/esm/flag-manager/FlagUpdater.d.ts.map +1 -1
- package/dist/esm/index.d.ts +8 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.mjs +1453 -61
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/storage/freshness.d.ts +27 -0
- package/dist/esm/storage/freshness.d.ts.map +1 -0
- package/dist/esm/storage/loadCachedFlags.d.ts +25 -0
- package/dist/esm/storage/loadCachedFlags.d.ts.map +1 -0
- package/dist/esm/types/index.d.ts +2 -2
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -289,35 +289,290 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
|
|
|
289
289
|
});
|
|
290
290
|
return result;
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Creates a validator for nested objects. When used in a validator map,
|
|
294
|
+
* `validateOptions` will recursively validate the nested object's properties.
|
|
295
|
+
* Defaults for nested fields are passed through from the parent.
|
|
296
|
+
*/
|
|
297
|
+
function validatorOf(validators, builtInDefaults) {
|
|
298
|
+
return {
|
|
299
|
+
is: (u) => jsSdkCommon.TypeValidators.Object.is(u),
|
|
300
|
+
getType: () => 'object',
|
|
301
|
+
validate(value, name, logger, defaults) {
|
|
302
|
+
if (!jsSdkCommon.TypeValidators.Object.is(value)) {
|
|
303
|
+
logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, 'object', typeof value));
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const nestedDefaults = builtInDefaults ??
|
|
307
|
+
(jsSdkCommon.TypeValidators.Object.is(defaults) ? defaults : {});
|
|
308
|
+
const nested = validateOptions(value, validators, nestedDefaults, logger, name);
|
|
309
|
+
return Object.keys(nested).length > 0 ? { value: nested } : undefined;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
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
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Creates a validator that tries each provided validator in order and uses the
|
|
371
|
+
* first one whose `is()` check passes. For compound validators the value is
|
|
372
|
+
* processed through `validate()`; for simple validators the value is accepted
|
|
373
|
+
* as-is. If no validator matches, a warning is logged and the default is
|
|
374
|
+
* preserved.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* // Accepts either a boolean or a nested object with specific fields:
|
|
379
|
+
* anyOf(TypeValidators.Boolean, validatorOf({ lifecycle: TypeValidators.Boolean }))
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
function anyOf(...validators) {
|
|
383
|
+
return {
|
|
384
|
+
is: (u) => validators.some((v) => v.is(u)),
|
|
385
|
+
getType: () => validators.map((v) => v.getType()).join(' | '),
|
|
386
|
+
validate(value, name, logger, defaults) {
|
|
387
|
+
const match = validators.find((v) => v.is(value));
|
|
388
|
+
if (match) {
|
|
389
|
+
return isCompoundValidator(match)
|
|
390
|
+
? match.validate(value, name, logger, defaults)
|
|
391
|
+
: { value };
|
|
392
|
+
}
|
|
393
|
+
logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, this.getType(), typeof value));
|
|
394
|
+
return undefined;
|
|
395
|
+
},
|
|
396
|
+
};
|
|
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
|
+
}
|
|
292
450
|
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
debug: jsSdkCommon.TypeValidators.Boolean,
|
|
306
|
-
diagnosticOptOut: jsSdkCommon.TypeValidators.Boolean,
|
|
307
|
-
withReasons: jsSdkCommon.TypeValidators.Boolean,
|
|
308
|
-
sendEvents: jsSdkCommon.TypeValidators.Boolean,
|
|
451
|
+
const BACKGROUND_POLL_INTERVAL_SECONDS = 3600;
|
|
452
|
+
const dataSourceTypeValidator = jsSdkCommon.TypeValidators.oneOf('cache', 'polling', 'streaming');
|
|
453
|
+
const connectionModeValidator = jsSdkCommon.TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
|
|
454
|
+
const endpointValidators = {
|
|
455
|
+
pollingBaseUri: jsSdkCommon.TypeValidators.String,
|
|
456
|
+
streamingBaseUri: jsSdkCommon.TypeValidators.String,
|
|
457
|
+
};
|
|
458
|
+
const cacheEntryValidators = {
|
|
459
|
+
type: dataSourceTypeValidator,
|
|
460
|
+
};
|
|
461
|
+
const pollingEntryValidators = {
|
|
462
|
+
type: dataSourceTypeValidator,
|
|
309
463
|
pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
464
|
+
endpoints: validatorOf(endpointValidators),
|
|
465
|
+
};
|
|
466
|
+
const streamingEntryValidators = {
|
|
467
|
+
type: dataSourceTypeValidator,
|
|
468
|
+
initialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(1),
|
|
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,
|
|
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
|
+
},
|
|
319
506
|
};
|
|
320
507
|
|
|
508
|
+
const modeSwitchingValidators = {
|
|
509
|
+
lifecycle: jsSdkCommon.TypeValidators.Boolean,
|
|
510
|
+
network: jsSdkCommon.TypeValidators.Boolean,
|
|
511
|
+
};
|
|
512
|
+
const dataSystemValidators = {
|
|
513
|
+
initialConnectionMode: connectionModeValidator,
|
|
514
|
+
backgroundConnectionMode: connectionModeValidator,
|
|
515
|
+
automaticModeSwitching: anyOf(jsSdkCommon.TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
|
|
516
|
+
connectionModes: connectionModesValidator,
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Default FDv2 data system configuration for browser SDKs.
|
|
520
|
+
*/
|
|
521
|
+
const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
522
|
+
initialConnectionMode: 'one-shot',
|
|
523
|
+
backgroundConnectionMode: undefined,
|
|
524
|
+
automaticModeSwitching: false,
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* Default FDv2 data system configuration for mobile (React Native) SDKs.
|
|
528
|
+
*/
|
|
529
|
+
const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
530
|
+
initialConnectionMode: 'streaming',
|
|
531
|
+
backgroundConnectionMode: 'background',
|
|
532
|
+
automaticModeSwitching: true,
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
|
|
536
|
+
*/
|
|
537
|
+
const DESKTOP_DATA_SYSTEM_DEFAULTS = {
|
|
538
|
+
initialConnectionMode: 'streaming',
|
|
539
|
+
backgroundConnectionMode: undefined,
|
|
540
|
+
automaticModeSwitching: false,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
function createValidators(options) {
|
|
544
|
+
return {
|
|
545
|
+
logger: jsSdkCommon.TypeValidators.Object,
|
|
546
|
+
maxCachedContexts: jsSdkCommon.TypeValidators.numberWithMin(0),
|
|
547
|
+
baseUri: jsSdkCommon.TypeValidators.String,
|
|
548
|
+
streamUri: jsSdkCommon.TypeValidators.String,
|
|
549
|
+
eventsUri: jsSdkCommon.TypeValidators.String,
|
|
550
|
+
capacity: jsSdkCommon.TypeValidators.numberWithMin(1),
|
|
551
|
+
diagnosticRecordingInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
552
|
+
flushInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
553
|
+
streamInitialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(0),
|
|
554
|
+
allAttributesPrivate: jsSdkCommon.TypeValidators.Boolean,
|
|
555
|
+
debug: jsSdkCommon.TypeValidators.Boolean,
|
|
556
|
+
diagnosticOptOut: jsSdkCommon.TypeValidators.Boolean,
|
|
557
|
+
withReasons: jsSdkCommon.TypeValidators.Boolean,
|
|
558
|
+
sendEvents: jsSdkCommon.TypeValidators.Boolean,
|
|
559
|
+
pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
|
|
560
|
+
useReport: jsSdkCommon.TypeValidators.Boolean,
|
|
561
|
+
privateAttributes: jsSdkCommon.TypeValidators.StringArray,
|
|
562
|
+
disableCache: jsSdkCommon.TypeValidators.Boolean,
|
|
563
|
+
applicationInfo: jsSdkCommon.TypeValidators.Object,
|
|
564
|
+
wrapperName: jsSdkCommon.TypeValidators.String,
|
|
565
|
+
wrapperVersion: jsSdkCommon.TypeValidators.String,
|
|
566
|
+
payloadFilterKey: jsSdkCommon.TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
|
|
567
|
+
hooks: jsSdkCommon.TypeValidators.createTypeArray('Hook[]', {}),
|
|
568
|
+
inspectors: jsSdkCommon.TypeValidators.createTypeArray('LDInspection', {}),
|
|
569
|
+
cleanOldPersistentData: jsSdkCommon.TypeValidators.Boolean,
|
|
570
|
+
dataSystem: options?.dataSystemDefaults
|
|
571
|
+
? validatorOf(dataSystemValidators, options.dataSystemDefaults)
|
|
572
|
+
: jsSdkCommon.TypeValidators.Object,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
321
576
|
const DEFAULT_POLLING_INTERVAL = 60 * 5;
|
|
322
577
|
const DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com';
|
|
323
578
|
const DEFAULT_STREAM = 'https://clientstream.launchdarkly.com';
|
|
@@ -343,6 +598,7 @@ class ConfigurationImpl {
|
|
|
343
598
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
344
599
|
this.streamUri = DEFAULT_STREAM;
|
|
345
600
|
this.maxCachedContexts = 5;
|
|
601
|
+
this.disableCache = false;
|
|
346
602
|
this.capacity = 100;
|
|
347
603
|
this.diagnosticRecordingInterval = 900;
|
|
348
604
|
this.flushInterval = 30;
|
|
@@ -359,6 +615,9 @@ class ConfigurationImpl {
|
|
|
359
615
|
this.hooks = [];
|
|
360
616
|
this.inspectors = [];
|
|
361
617
|
this.logger = ensureSafeLogger(pristineOptions.logger);
|
|
618
|
+
const validators = createValidators({
|
|
619
|
+
dataSystemDefaults: internalOptions.dataSystemDefaults,
|
|
620
|
+
});
|
|
362
621
|
const validated = validateOptions(pristineOptions, validators, {}, this.logger);
|
|
363
622
|
Object.entries(validated).forEach(([k, v]) => {
|
|
364
623
|
if (k !== 'logger') {
|
|
@@ -711,6 +970,98 @@ class EventFactory extends jsSdkCommon.internal.EventFactoryBase {
|
|
|
711
970
|
}
|
|
712
971
|
}
|
|
713
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Suffix appended to context storage keys to form the freshness storage key.
|
|
975
|
+
*/
|
|
976
|
+
const FRESHNESS_SUFFIX = '_freshness';
|
|
977
|
+
/**
|
|
978
|
+
* Computes a SHA-256 hash of the context's full canonical JSON.
|
|
979
|
+
* Returns `undefined` if the context cannot be serialized.
|
|
980
|
+
*/
|
|
981
|
+
async function hashContext(crypto, context) {
|
|
982
|
+
const json = context.canonicalUnfilteredJson();
|
|
983
|
+
if (!json) {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
return digest(crypto.createHash('sha256').update(json), 'base64');
|
|
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
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function isValidFlag(value) {
|
|
1017
|
+
return value !== null && typeof value === 'object' && typeof value.version === 'number';
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Loads cached flag data from storage for the given context.
|
|
1021
|
+
*
|
|
1022
|
+
* Checks the current storage key first, then falls back to the legacy
|
|
1023
|
+
* canonical key location (pre-10.3.1). Does NOT perform migration — the
|
|
1024
|
+
* caller is responsible for migrating data if {@link CachedFlagData.fromLegacyKey}
|
|
1025
|
+
* is true.
|
|
1026
|
+
*
|
|
1027
|
+
* @returns The cached flag data, or `undefined` on cache miss or parse error.
|
|
1028
|
+
*/
|
|
1029
|
+
async function loadCachedFlags(storage, crypto, environmentNamespace, context, logger) {
|
|
1030
|
+
const storageKey = await namespaceForContextData(crypto, environmentNamespace, context);
|
|
1031
|
+
let flagsJson = await storage.get(storageKey);
|
|
1032
|
+
let fromLegacyKey = false;
|
|
1033
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
1034
|
+
// Fallback: in version <10.3.1 flag data was stored under the canonical key.
|
|
1035
|
+
flagsJson = await storage.get(context.canonicalKey);
|
|
1036
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
fromLegacyKey = true;
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const parsed = JSON.parse(flagsJson);
|
|
1043
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
1044
|
+
logger?.warn('Cached flag data is not a valid object');
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1047
|
+
const entries = Object.entries(parsed);
|
|
1048
|
+
const invalidKey = entries.find(([, value]) => !isValidFlag(value));
|
|
1049
|
+
if (invalidKey) {
|
|
1050
|
+
logger?.warn(`Discarding cached flags due to invalid entry: ${invalidKey[0]}`);
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
const flags = entries.reduce((acc, [key, value]) => {
|
|
1054
|
+
acc[key] = value;
|
|
1055
|
+
return acc;
|
|
1056
|
+
}, {});
|
|
1057
|
+
return { flags, storageKey, fromLegacyKey };
|
|
1058
|
+
}
|
|
1059
|
+
catch (e) {
|
|
1060
|
+
logger?.warn(`Could not parse cached flag evaluations from persistent storage: ${e.message}`);
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
714
1065
|
/**
|
|
715
1066
|
* An index for tracking the most recently used contexts by timestamp with the ability to
|
|
716
1067
|
* update entry timestamps and prune out least used contexts above a max capacity provided.
|
|
@@ -776,12 +1127,18 @@ class ContextIndex {
|
|
|
776
1127
|
* This class handles persisting and loading flag values from a persistent
|
|
777
1128
|
* store. It intercepts updates and forwards them to the flag updater and
|
|
778
1129
|
* then persists changes after the updater has completed.
|
|
1130
|
+
*
|
|
1131
|
+
* Freshness metadata (timestamp + context attribute hash) is stored in a
|
|
1132
|
+
* separate storage key (`{contextKey}_freshness`) alongside the flag data.
|
|
1133
|
+
* Both keys are managed together — when a context is evicted, both the flag
|
|
1134
|
+
* data and freshness record are cleared.
|
|
779
1135
|
*/
|
|
780
1136
|
class FlagPersistence {
|
|
781
|
-
constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
1137
|
+
constructor(_platform, _environmentNamespace, _maxCachedContexts, _disableCache, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
782
1138
|
this._platform = _platform;
|
|
783
1139
|
this._environmentNamespace = _environmentNamespace;
|
|
784
1140
|
this._maxCachedContexts = _maxCachedContexts;
|
|
1141
|
+
this._disableCache = _disableCache;
|
|
785
1142
|
this._flagStore = _flagStore;
|
|
786
1143
|
this._flagUpdater = _flagUpdater;
|
|
787
1144
|
this._logger = _logger;
|
|
@@ -808,40 +1165,56 @@ class FlagPersistence {
|
|
|
808
1165
|
}
|
|
809
1166
|
return false;
|
|
810
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
|
+
}
|
|
811
1181
|
/**
|
|
812
1182
|
* Loads the flags from persistence for the provided context and gives those to the
|
|
813
1183
|
* {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
|
|
814
1184
|
*/
|
|
815
1185
|
async loadCached(context) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (flagsJson === null || flagsJson === undefined) {
|
|
819
|
-
// Fallback: in version <10.3.1 flag data was stored under the canonical key, check
|
|
820
|
-
// to see if data is present and migrate the data if present.
|
|
821
|
-
flagsJson = await this._platform.storage?.get(context.canonicalKey);
|
|
822
|
-
if (flagsJson === null || flagsJson === undefined) {
|
|
823
|
-
// return false indicating cache did not load if flag json is still absent
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
// migrate data from version <10.3.1 and cleanup data that was under canonical key
|
|
827
|
-
await this._platform.storage?.set(storageKey, flagsJson);
|
|
828
|
-
await this._platform.storage?.clear(context.canonicalKey);
|
|
1186
|
+
if (this._disableCache || this._maxCachedContexts <= 0) {
|
|
1187
|
+
return false;
|
|
829
1188
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// mapping flags to item descriptors
|
|
833
|
-
const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
|
|
834
|
-
acc[key] = { version: flag.version, flag };
|
|
835
|
-
return acc;
|
|
836
|
-
}, {});
|
|
837
|
-
this._flagUpdater.initCached(context, descriptors);
|
|
838
|
-
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
839
|
-
return true;
|
|
1189
|
+
if (!this._platform.storage) {
|
|
1190
|
+
return false;
|
|
840
1191
|
}
|
|
841
|
-
|
|
842
|
-
|
|
1192
|
+
const cached = await loadCachedFlags(this._platform.storage, this._platform.crypto, this._environmentNamespace, context, this._logger);
|
|
1193
|
+
if (!cached) {
|
|
843
1194
|
return false;
|
|
844
1195
|
}
|
|
1196
|
+
// Migrate data from version <10.3.1 stored under the canonical key
|
|
1197
|
+
if (cached.fromLegacyKey) {
|
|
1198
|
+
await this._platform.storage.set(cached.storageKey, JSON.stringify(cached.flags));
|
|
1199
|
+
await this._platform.storage.clear(context.canonicalKey);
|
|
1200
|
+
}
|
|
1201
|
+
// mapping flags to item descriptors
|
|
1202
|
+
const descriptors = Object.entries(cached.flags).reduce((acc, [key, flag]) => {
|
|
1203
|
+
acc[key] = { version: flag.version, flag };
|
|
1204
|
+
return acc;
|
|
1205
|
+
}, {});
|
|
1206
|
+
this._flagUpdater.initCached(context, descriptors);
|
|
1207
|
+
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
async _storeFreshness(contextStorageKey, context, timestamp) {
|
|
1211
|
+
const contextHash = await hashContext(this._platform.crypto, context);
|
|
1212
|
+
if (contextHash === undefined) {
|
|
1213
|
+
this._logger.error('Could not serialize context for freshness tracking');
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const record = { timestamp, contextHash };
|
|
1217
|
+
await this._platform.storage?.set(`${contextStorageKey}${FRESHNESS_SUFFIX}`, JSON.stringify(record));
|
|
845
1218
|
}
|
|
846
1219
|
async _loadIndex() {
|
|
847
1220
|
if (this._contextIndex !== undefined) {
|
|
@@ -863,13 +1236,26 @@ class FlagPersistence {
|
|
|
863
1236
|
return this._contextIndex;
|
|
864
1237
|
}
|
|
865
1238
|
async _storeCache(context) {
|
|
1239
|
+
if (this._disableCache) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const now = this._timeStamper();
|
|
866
1243
|
const index = await this._loadIndex();
|
|
867
1244
|
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
868
|
-
|
|
1245
|
+
if (this._maxCachedContexts > 0) {
|
|
1246
|
+
index.notice(storageKey, now);
|
|
1247
|
+
}
|
|
869
1248
|
const pruned = index.prune(this._maxCachedContexts);
|
|
870
|
-
|
|
871
|
-
|
|
1249
|
+
// If maxCachedContexts <= 0, current context was never added, so always skip flag write
|
|
1250
|
+
const currentContextWasPruned = this._maxCachedContexts <= 0 || pruned.some((it) => it.id === storageKey);
|
|
1251
|
+
await Promise.all(pruned.flatMap((it) => [
|
|
1252
|
+
this._platform.storage?.clear(it.id),
|
|
1253
|
+
this._platform.storage?.clear(`${it.id}${FRESHNESS_SUFFIX}`),
|
|
1254
|
+
]));
|
|
872
1255
|
await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
|
|
1256
|
+
if (currentContextWasPruned) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
873
1259
|
const allFlags = this._flagStore.getAll();
|
|
874
1260
|
// mapping item descriptors to flags
|
|
875
1261
|
const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
|
|
@@ -879,8 +1265,15 @@ class FlagPersistence {
|
|
|
879
1265
|
return acc;
|
|
880
1266
|
}, {});
|
|
881
1267
|
const jsonAll = JSON.stringify(flags);
|
|
882
|
-
// store flag data
|
|
1268
|
+
// store flag data first, so freshness is never newer than the flags it describes
|
|
883
1269
|
await this._platform.storage?.set(storageKey, jsonAll);
|
|
1270
|
+
// store freshness — best-effort, must not block flag persistence
|
|
1271
|
+
try {
|
|
1272
|
+
await this._storeFreshness(storageKey, context, now);
|
|
1273
|
+
}
|
|
1274
|
+
catch (e) {
|
|
1275
|
+
this._logger.warn(`Failed to store freshness data: ${e.message}`);
|
|
1276
|
+
}
|
|
884
1277
|
}
|
|
885
1278
|
}
|
|
886
1279
|
|
|
@@ -908,6 +1301,16 @@ function createDefaultFlagStore() {
|
|
|
908
1301
|
getAll() {
|
|
909
1302
|
return flags;
|
|
910
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
|
+
},
|
|
911
1314
|
};
|
|
912
1315
|
}
|
|
913
1316
|
|
|
@@ -965,6 +1368,24 @@ function createFlagUpdater(_flagStore, _logger) {
|
|
|
965
1368
|
}
|
|
966
1369
|
this.init(context, newFlags);
|
|
967
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
|
+
},
|
|
968
1389
|
upsert(context, key, item) {
|
|
969
1390
|
if (activeContext?.canonicalKey !== context.canonicalKey) {
|
|
970
1391
|
logger.warn('Received an update for an inactive context.');
|
|
@@ -996,17 +1417,18 @@ class DefaultFlagManager {
|
|
|
996
1417
|
* @param platform implementation of various platform provided functionality
|
|
997
1418
|
* @param sdkKey that will be used to distinguish different environments
|
|
998
1419
|
* @param maxCachedContexts that specifies the max number of contexts that will be cached in persistence
|
|
1420
|
+
* @param disableCache set to true to completely disable the persistent flag cache
|
|
999
1421
|
* @param logger used for logging various messages
|
|
1000
1422
|
* @param timeStamper exists for testing purposes
|
|
1001
1423
|
*/
|
|
1002
|
-
constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1424
|
+
constructor(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
1003
1425
|
this._flagStore = createDefaultFlagStore();
|
|
1004
1426
|
this._flagUpdater = createFlagUpdater(this._flagStore, logger);
|
|
1005
|
-
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
1427
|
+
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper);
|
|
1006
1428
|
}
|
|
1007
|
-
async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1429
|
+
async _initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
1008
1430
|
const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
|
|
1009
|
-
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
1431
|
+
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, disableCache, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
1010
1432
|
}
|
|
1011
1433
|
get(key) {
|
|
1012
1434
|
if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
|
|
@@ -1043,6 +1465,9 @@ class DefaultFlagManager {
|
|
|
1043
1465
|
async loadCached(context) {
|
|
1044
1466
|
return (await this._flagPersistencePromise).loadCached(context);
|
|
1045
1467
|
}
|
|
1468
|
+
async applyChanges(context, updates, type) {
|
|
1469
|
+
return (await this._flagPersistencePromise).applyChanges(context, updates, type);
|
|
1470
|
+
}
|
|
1046
1471
|
on(callback) {
|
|
1047
1472
|
this._flagUpdater.on(callback);
|
|
1048
1473
|
}
|
|
@@ -1482,7 +1907,7 @@ class LDClientImpl {
|
|
|
1482
1907
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1483
1908
|
this.logger = this._config.logger;
|
|
1484
1909
|
this._baseHeaders = jsSdkCommon.defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1485
|
-
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.logger);
|
|
1910
|
+
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1486
1911
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
1487
1912
|
this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
|
|
1488
1913
|
this.emitter = new LDEmitter();
|
|
@@ -2576,15 +3001,993 @@ class BaseDataManager {
|
|
|
2576
3001
|
}
|
|
2577
3002
|
}
|
|
2578
3003
|
|
|
3004
|
+
function allConditionsMatch(conditions, input) {
|
|
3005
|
+
return Object.entries(conditions).every(([key, value]) => value === undefined || input[key] === value);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Given a mode resolution table and the current input state, returns the
|
|
3009
|
+
* resolved FDv2 connection mode.
|
|
3010
|
+
*
|
|
3011
|
+
* Iterates entries in order. The first entry whose conditions all match the
|
|
3012
|
+
* input wins. If no entry matches (should not happen when the table ends with
|
|
3013
|
+
* a catch-all), falls back to `input.foregroundMode`.
|
|
3014
|
+
*/
|
|
3015
|
+
const CONFIGURED_MODE_MAP = {
|
|
3016
|
+
foreground: 'foregroundMode',
|
|
3017
|
+
background: 'backgroundMode',
|
|
3018
|
+
};
|
|
3019
|
+
function resolveConnectionMode(table, input) {
|
|
3020
|
+
const match = table.find((entry) => allConditionsMatch(entry.conditions, input));
|
|
3021
|
+
if (match) {
|
|
3022
|
+
const { mode } = match;
|
|
3023
|
+
if (typeof mode === 'object') {
|
|
3024
|
+
return input[CONFIGURED_MODE_MAP[mode.configured]];
|
|
3025
|
+
}
|
|
3026
|
+
return mode;
|
|
3027
|
+
}
|
|
3028
|
+
return input.foregroundMode;
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Mode resolution table for mobile platforms (React Native, etc.).
|
|
3032
|
+
*
|
|
3033
|
+
* - No network → offline.
|
|
3034
|
+
* - Background → configured background mode.
|
|
3035
|
+
* - Foreground → configured foreground mode.
|
|
3036
|
+
*/
|
|
3037
|
+
const MOBILE_TRANSITION_TABLE = [
|
|
3038
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
3039
|
+
{ conditions: { lifecycle: 'background' }, mode: { configured: 'background' } },
|
|
3040
|
+
{ conditions: { lifecycle: 'foreground' }, mode: { configured: 'foreground' } },
|
|
3041
|
+
];
|
|
3042
|
+
/**
|
|
3043
|
+
* Mode resolution table for browser platforms.
|
|
3044
|
+
*
|
|
3045
|
+
* - No network → offline.
|
|
3046
|
+
* - Otherwise → configured foreground mode.
|
|
3047
|
+
*
|
|
3048
|
+
* Browser listener-driven streaming (auto-promotion to streaming when change
|
|
3049
|
+
* listeners are registered) is handled externally by the caller modifying
|
|
3050
|
+
* `foregroundMode` before consulting this table.
|
|
3051
|
+
*/
|
|
3052
|
+
const BROWSER_TRANSITION_TABLE = [
|
|
3053
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
3054
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
3055
|
+
];
|
|
3056
|
+
/**
|
|
3057
|
+
* Mode resolution table for desktop platforms (Electron, etc.).
|
|
3058
|
+
*
|
|
3059
|
+
* - No network → offline.
|
|
3060
|
+
* - Otherwise → configured foreground mode.
|
|
3061
|
+
*/
|
|
3062
|
+
const DESKTOP_TRANSITION_TABLE = [
|
|
3063
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
3064
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
3065
|
+
];
|
|
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
|
+
|
|
2579
3971
|
exports.platform = jsSdkCommon__namespace;
|
|
3972
|
+
exports.BROWSER_DATA_SYSTEM_DEFAULTS = BROWSER_DATA_SYSTEM_DEFAULTS;
|
|
3973
|
+
exports.BROWSER_TRANSITION_TABLE = BROWSER_TRANSITION_TABLE;
|
|
2580
3974
|
exports.BaseDataManager = BaseDataManager;
|
|
3975
|
+
exports.DESKTOP_DATA_SYSTEM_DEFAULTS = DESKTOP_DATA_SYSTEM_DEFAULTS;
|
|
3976
|
+
exports.DESKTOP_TRANSITION_TABLE = DESKTOP_TRANSITION_TABLE;
|
|
2581
3977
|
exports.DataSourceState = DataSourceState;
|
|
2582
3978
|
exports.LDClientImpl = LDClientImpl;
|
|
3979
|
+
exports.MOBILE_DATA_SYSTEM_DEFAULTS = MOBILE_DATA_SYSTEM_DEFAULTS;
|
|
3980
|
+
exports.MOBILE_TRANSITION_TABLE = MOBILE_TRANSITION_TABLE;
|
|
3981
|
+
exports.MODE_TABLE = MODE_TABLE;
|
|
2583
3982
|
exports.browserFdv1Endpoints = browserFdv1Endpoints;
|
|
3983
|
+
exports.createDataSourceStatusManager = createDataSourceStatusManager;
|
|
3984
|
+
exports.createDefaultSourceFactoryProvider = createDefaultSourceFactoryProvider;
|
|
3985
|
+
exports.dataSystemValidators = dataSystemValidators;
|
|
2584
3986
|
exports.fdv2Endpoints = fdv2Endpoints;
|
|
2585
3987
|
exports.makeRequestor = makeRequestor;
|
|
2586
3988
|
exports.mobileFdv1Endpoints = mobileFdv1Endpoints;
|
|
2587
3989
|
exports.readFlagsFromBootstrap = readFlagsFromBootstrap;
|
|
3990
|
+
exports.resolveConnectionMode = resolveConnectionMode;
|
|
2588
3991
|
exports.safeRegisterDebugOverridePlugins = safeRegisterDebugOverridePlugins;
|
|
2589
3992
|
exports.validateOptions = validateOptions;
|
|
2590
3993
|
Object.keys(jsSdkCommon).forEach(function (k) {
|