@launchdarkly/js-client-sdk-common 1.23.0 → 1.24.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 +24 -0
- package/dist/cjs/DataManager.d.ts +9 -0
- package/dist/cjs/DataManager.d.ts.map +1 -1
- package/dist/cjs/api/LDOptions.d.ts +17 -5
- package/dist/cjs/api/LDOptions.d.ts.map +1 -1
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts +46 -0
- package/dist/cjs/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts +5 -0
- package/dist/cjs/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
- package/dist/cjs/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/api/datasource/ModeDefinition.d.ts +17 -1
- package/dist/cjs/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/cjs/api/datasource/index.d.ts +2 -2
- package/dist/cjs/api/datasource/index.d.ts.map +1 -1
- package/dist/cjs/configuration/Configuration.d.ts +3 -3
- package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
- package/dist/cjs/configuration/validateOptions.d.ts +18 -2
- package/dist/cjs/configuration/validateOptions.d.ts.map +1 -1
- package/dist/cjs/configuration/validators.d.ts +1 -1
- package/dist/cjs/configuration/validators.d.ts.map +1 -1
- package/dist/cjs/datasource/ConnectionModeConfig.d.ts +4 -2
- package/dist/cjs/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/cjs/datasource/FDv2DataManagerBase.d.ts +87 -0
- package/dist/cjs/datasource/FDv2DataManagerBase.d.ts.map +1 -0
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts +37 -3
- package/dist/cjs/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/cjs/index.cjs +1184 -52
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +7 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/esm/DataManager.d.ts +9 -0
- package/dist/esm/DataManager.d.ts.map +1 -1
- package/dist/esm/api/LDOptions.d.ts +17 -5
- package/dist/esm/api/LDOptions.d.ts.map +1 -1
- package/dist/esm/api/datasource/DataSourceEntry.d.ts +46 -0
- package/dist/esm/api/datasource/DataSourceEntry.d.ts.map +1 -1
- package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts +5 -0
- package/dist/esm/api/datasource/FDv2ConnectionMode.d.ts.map +1 -1
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts +37 -41
- package/dist/esm/api/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/api/datasource/ModeDefinition.d.ts +17 -1
- package/dist/esm/api/datasource/ModeDefinition.d.ts.map +1 -1
- package/dist/esm/api/datasource/index.d.ts +2 -2
- package/dist/esm/api/datasource/index.d.ts.map +1 -1
- package/dist/esm/configuration/Configuration.d.ts +3 -3
- package/dist/esm/configuration/Configuration.d.ts.map +1 -1
- package/dist/esm/configuration/validateOptions.d.ts +18 -2
- package/dist/esm/configuration/validateOptions.d.ts.map +1 -1
- package/dist/esm/configuration/validators.d.ts +1 -1
- package/dist/esm/configuration/validators.d.ts.map +1 -1
- package/dist/esm/datasource/ConnectionModeConfig.d.ts +4 -2
- package/dist/esm/datasource/ConnectionModeConfig.d.ts.map +1 -1
- package/dist/esm/datasource/FDv2DataManagerBase.d.ts +87 -0
- package/dist/esm/datasource/FDv2DataManagerBase.d.ts.map +1 -0
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts +37 -3
- package/dist/esm/datasource/LDClientDataSystemOptions.d.ts.map +1 -1
- package/dist/esm/index.d.ts +7 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.mjs +1183 -54
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPollingUri, isNullish, TypeValidators, OptionMessages, NumberWithMinimum, createSafeLogger, ServiceEndpoints, ApplicationTags, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, cancelableTimedPromise, LDClientError, base64UrlEncode, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
|
|
1
|
+
import { getPollingUri, isNullish, TypeValidators, OptionMessages, NumberWithMinimum, createSafeLogger, ServiceEndpoints, ApplicationTags, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, cancelableTimedPromise, LDClientError, base64UrlEncode, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError, sleep } from '@launchdarkly/js-sdk-common';
|
|
2
2
|
export * from '@launchdarkly/js-sdk-common';
|
|
3
3
|
import * as jsSdkCommon from '@launchdarkly/js-sdk-common';
|
|
4
4
|
export { jsSdkCommon as platform };
|
|
@@ -275,10 +275,18 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
|
|
|
275
275
|
* Creates a validator for nested objects. When used in a validator map,
|
|
276
276
|
* `validateOptions` will recursively validate the nested object's properties.
|
|
277
277
|
* Defaults for nested fields are passed through from the parent.
|
|
278
|
+
*
|
|
279
|
+
* @param validators - Validator map for the nested object's fields.
|
|
280
|
+
* @param options - Optional configuration.
|
|
281
|
+
* @param options.defaults - Built-in defaults for nested fields.
|
|
282
|
+
* @param options.is - Custom `is` predicate. When provided, replaces the
|
|
283
|
+
* default "is object" check. Use this to discriminate between object shapes
|
|
284
|
+
* in an `anyOf` (e.g., matching on a `type` discriminant field).
|
|
278
285
|
*/
|
|
279
|
-
function validatorOf(validators,
|
|
286
|
+
function validatorOf(validators, options) {
|
|
287
|
+
const builtInDefaults = options?.defaults;
|
|
280
288
|
return {
|
|
281
|
-
is: (u) => TypeValidators.Object.is(u),
|
|
289
|
+
is: options?.is ?? ((u) => TypeValidators.Object.is(u)),
|
|
282
290
|
getType: () => 'object',
|
|
283
291
|
validate(value, name, logger, defaults) {
|
|
284
292
|
if (!TypeValidators.Object.is(value)) {
|
|
@@ -386,6 +394,10 @@ function anyOf(...validators) {
|
|
|
386
394
|
*
|
|
387
395
|
* @param keyValidator - Validates that each key is an allowed value.
|
|
388
396
|
* @param valueValidator - Validates each value in the record.
|
|
397
|
+
* @param options - Optional configuration.
|
|
398
|
+
* @param options.defaults - Built-in defaults for the record entries. When
|
|
399
|
+
* provided, takes priority over defaults passed from the parent at
|
|
400
|
+
* validation time (same precedence as {@link validatorOf}).
|
|
389
401
|
*
|
|
390
402
|
* @example
|
|
391
403
|
* ```ts
|
|
@@ -397,7 +409,8 @@ function anyOf(...validators) {
|
|
|
397
409
|
* )
|
|
398
410
|
* ```
|
|
399
411
|
*/
|
|
400
|
-
function recordOf(keyValidator, valueValidator) {
|
|
412
|
+
function recordOf(keyValidator, valueValidator, options) {
|
|
413
|
+
const builtInDefaults = options?.defaults;
|
|
401
414
|
return {
|
|
402
415
|
is: (u) => TypeValidators.Object.is(u),
|
|
403
416
|
getType: () => 'object',
|
|
@@ -422,14 +435,14 @@ function recordOf(keyValidator, valueValidator) {
|
|
|
422
435
|
logger?.warn(OptionMessages.wrongOptionType(name, keyValidator.getType(), key));
|
|
423
436
|
}
|
|
424
437
|
});
|
|
425
|
-
const recordDefaults =
|
|
426
|
-
? defaults
|
|
427
|
-
: {};
|
|
438
|
+
const recordDefaults = builtInDefaults ??
|
|
439
|
+
(TypeValidators.Object.is(defaults) ? defaults : {});
|
|
428
440
|
return { value: validateOptions(filtered, validatorMap, recordDefaults, logger, name) };
|
|
429
441
|
},
|
|
430
442
|
};
|
|
431
443
|
}
|
|
432
444
|
|
|
445
|
+
const DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS = 300;
|
|
433
446
|
const BACKGROUND_POLL_INTERVAL_SECONDS = 3600;
|
|
434
447
|
const dataSourceTypeValidator = TypeValidators.oneOf('cache', 'polling', 'streaming');
|
|
435
448
|
const connectionModeValidator = TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
|
|
@@ -459,19 +472,25 @@ const synchronizerEntryArrayValidator = arrayOf('type', {
|
|
|
459
472
|
polling: pollingEntryValidators,
|
|
460
473
|
streaming: streamingEntryValidators,
|
|
461
474
|
});
|
|
475
|
+
const fdv1FallbackValidators = {
|
|
476
|
+
pollInterval: TypeValidators.numberWithMin(30),
|
|
477
|
+
endpoints: validatorOf(endpointValidators),
|
|
478
|
+
};
|
|
462
479
|
const modeDefinitionValidators = {
|
|
463
480
|
initializers: initializerEntryArrayValidator,
|
|
464
481
|
synchronizers: synchronizerEntryArrayValidator,
|
|
482
|
+
fdv1Fallback: validatorOf(fdv1FallbackValidators),
|
|
465
483
|
};
|
|
466
|
-
const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
|
|
467
484
|
const MODE_TABLE = {
|
|
468
485
|
streaming: {
|
|
469
486
|
initializers: [{ type: 'cache' }, { type: 'polling' }],
|
|
470
487
|
synchronizers: [{ type: 'streaming' }, { type: 'polling' }],
|
|
488
|
+
fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
|
|
471
489
|
},
|
|
472
490
|
polling: {
|
|
473
491
|
initializers: [{ type: 'cache' }],
|
|
474
492
|
synchronizers: [{ type: 'polling' }],
|
|
493
|
+
fdv1Fallback: { pollInterval: DEFAULT_FDV1_FALLBACK_POLL_INTERVAL_SECONDS },
|
|
475
494
|
},
|
|
476
495
|
offline: {
|
|
477
496
|
initializers: [{ type: 'cache' }],
|
|
@@ -484,24 +503,33 @@ const MODE_TABLE = {
|
|
|
484
503
|
background: {
|
|
485
504
|
initializers: [{ type: 'cache' }],
|
|
486
505
|
synchronizers: [{ type: 'polling', pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS }],
|
|
506
|
+
fdv1Fallback: { pollInterval: BACKGROUND_POLL_INTERVAL_SECONDS },
|
|
487
507
|
},
|
|
488
508
|
};
|
|
509
|
+
const connectionModesValidator = recordOf(connectionModeValidator, validatorOf(modeDefinitionValidators));
|
|
489
510
|
|
|
490
|
-
|
|
511
|
+
function hasType(u, type) {
|
|
512
|
+
return TypeValidators.Object.is(u) && u.type === type;
|
|
513
|
+
}
|
|
514
|
+
const automaticModeValidators = {
|
|
515
|
+
type: TypeValidators.oneOf('automatic'),
|
|
491
516
|
lifecycle: TypeValidators.Boolean,
|
|
492
517
|
network: TypeValidators.Boolean,
|
|
493
518
|
};
|
|
494
|
-
const
|
|
519
|
+
const manualModeValidators = {
|
|
520
|
+
type: TypeValidators.oneOf('manual'),
|
|
495
521
|
initialConnectionMode: connectionModeValidator,
|
|
522
|
+
};
|
|
523
|
+
const dataSystemValidators = {
|
|
496
524
|
backgroundConnectionMode: connectionModeValidator,
|
|
497
|
-
automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(
|
|
525
|
+
automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(automaticModeValidators, { is: (u) => hasType(u, 'automatic') }), validatorOf(manualModeValidators, { is: (u) => hasType(u, 'manual') })),
|
|
498
526
|
connectionModes: connectionModesValidator,
|
|
499
527
|
};
|
|
500
528
|
/**
|
|
501
529
|
* Default FDv2 data system configuration for browser SDKs.
|
|
502
530
|
*/
|
|
503
531
|
const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
504
|
-
|
|
532
|
+
foregroundConnectionMode: 'one-shot',
|
|
505
533
|
backgroundConnectionMode: undefined,
|
|
506
534
|
automaticModeSwitching: false,
|
|
507
535
|
};
|
|
@@ -509,7 +537,7 @@ const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
|
509
537
|
* Default FDv2 data system configuration for mobile (React Native) SDKs.
|
|
510
538
|
*/
|
|
511
539
|
const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
512
|
-
|
|
540
|
+
foregroundConnectionMode: 'streaming',
|
|
513
541
|
backgroundConnectionMode: 'background',
|
|
514
542
|
automaticModeSwitching: true,
|
|
515
543
|
};
|
|
@@ -517,10 +545,24 @@ const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
|
517
545
|
* Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
|
|
518
546
|
*/
|
|
519
547
|
const DESKTOP_DATA_SYSTEM_DEFAULTS = {
|
|
520
|
-
|
|
548
|
+
foregroundConnectionMode: 'streaming',
|
|
521
549
|
backgroundConnectionMode: undefined,
|
|
522
550
|
automaticModeSwitching: false,
|
|
523
551
|
};
|
|
552
|
+
function isManualModeSwitching(value) {
|
|
553
|
+
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'manual';
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Resolve the foreground connection mode from a validated data system config
|
|
557
|
+
* and platform defaults. Uses the mode from `ManualModeSwitching` when present,
|
|
558
|
+
* otherwise falls back to the platform default.
|
|
559
|
+
*/
|
|
560
|
+
function resolveForegroundMode(dataSystem, defaults) {
|
|
561
|
+
if (isManualModeSwitching(dataSystem.automaticModeSwitching)) {
|
|
562
|
+
return dataSystem.automaticModeSwitching.initialConnectionMode;
|
|
563
|
+
}
|
|
564
|
+
return dataSystem.foregroundConnectionMode ?? defaults.foregroundConnectionMode;
|
|
565
|
+
}
|
|
524
566
|
|
|
525
567
|
function createValidators(options) {
|
|
526
568
|
return {
|
|
@@ -550,7 +592,12 @@ function createValidators(options) {
|
|
|
550
592
|
inspectors: TypeValidators.createTypeArray('LDInspection', {}),
|
|
551
593
|
cleanOldPersistentData: TypeValidators.Boolean,
|
|
552
594
|
dataSystem: options?.dataSystemDefaults
|
|
553
|
-
? validatorOf(dataSystemValidators,
|
|
595
|
+
? validatorOf(dataSystemValidators, {
|
|
596
|
+
defaults: {
|
|
597
|
+
...options.dataSystemDefaults,
|
|
598
|
+
connectionModes: MODE_TABLE,
|
|
599
|
+
},
|
|
600
|
+
})
|
|
554
601
|
: TypeValidators.Object,
|
|
555
602
|
};
|
|
556
603
|
}
|
|
@@ -3157,7 +3204,6 @@ async function loadFromCache(config) {
|
|
|
3157
3204
|
object: flagToEvaluationResult(flag),
|
|
3158
3205
|
}));
|
|
3159
3206
|
const payload = {
|
|
3160
|
-
id: 'cache',
|
|
3161
3207
|
version: 0,
|
|
3162
3208
|
// No `state` field. The orchestrator sees a changeSet without a selector,
|
|
3163
3209
|
// records dataReceived=true, and continues to the next initializer.
|
|
@@ -3261,6 +3307,51 @@ function makeFDv2Requestor(plainContextString, serviceEndpoints, paths, requests
|
|
|
3261
3307
|
function processFlagEval(object) {
|
|
3262
3308
|
return object;
|
|
3263
3309
|
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Converts an FDv2 {@link internal.Update} with `kind: 'flag-eval'` into an
|
|
3312
|
+
* {@link ItemDescriptor} suitable for {@link FlagManager}.
|
|
3313
|
+
*
|
|
3314
|
+
* For put updates the envelope `version` is used as the {@link ItemDescriptor.version}
|
|
3315
|
+
* and as {@link Flag.version}. The rest of the fields are spread from the
|
|
3316
|
+
* {@link FlagEvaluationResult} object.
|
|
3317
|
+
*
|
|
3318
|
+
* For delete updates a tombstone descriptor is created with `deleted: true`.
|
|
3319
|
+
*/
|
|
3320
|
+
function flagEvalUpdateToItemDescriptor(update) {
|
|
3321
|
+
if (update.deleted) {
|
|
3322
|
+
return {
|
|
3323
|
+
version: update.version,
|
|
3324
|
+
flag: {
|
|
3325
|
+
version: update.version,
|
|
3326
|
+
deleted: true,
|
|
3327
|
+
value: undefined,
|
|
3328
|
+
trackEvents: false,
|
|
3329
|
+
},
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
const evalResult = update.object;
|
|
3333
|
+
return {
|
|
3334
|
+
version: update.version,
|
|
3335
|
+
flag: {
|
|
3336
|
+
...evalResult,
|
|
3337
|
+
version: update.version,
|
|
3338
|
+
},
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Converts an array of FDv2 payload updates into a map of flag key to
|
|
3343
|
+
* {@link ItemDescriptor}. Only `flag-eval` kind updates are processed;
|
|
3344
|
+
* unrecognized kinds are silently ignored.
|
|
3345
|
+
*/
|
|
3346
|
+
function flagEvalPayloadToItemDescriptors(updates) {
|
|
3347
|
+
const descriptors = {};
|
|
3348
|
+
updates.forEach((update) => {
|
|
3349
|
+
if (update.kind === 'flag-eval') {
|
|
3350
|
+
descriptors[update.key] = flagEvalUpdateToItemDescriptor(update);
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
return descriptors;
|
|
3354
|
+
}
|
|
3264
3355
|
|
|
3265
3356
|
function getFallback(headers) {
|
|
3266
3357
|
const value = headers.get('x-ld-fd-fallback');
|
|
@@ -3277,7 +3368,7 @@ function getEnvironmentId(headers) {
|
|
|
3277
3368
|
* it only forwards payloads and actionable errors. For polling results,
|
|
3278
3369
|
* we need full control over all protocol action types.
|
|
3279
3370
|
*/
|
|
3280
|
-
function processEvents(events,
|
|
3371
|
+
function processEvents(events, fdv1Fallback, environmentId, logger) {
|
|
3281
3372
|
const handler = internal.createProtocolHandler({
|
|
3282
3373
|
'flag-eval': processFlagEval,
|
|
3283
3374
|
}, logger);
|
|
@@ -3297,9 +3388,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3297
3388
|
case 'serverError': {
|
|
3298
3389
|
const errorInfo = errorInfoFromUnknown(action.reason);
|
|
3299
3390
|
logger?.error(`Server error during polling: ${action.reason}`);
|
|
3300
|
-
earlyResult =
|
|
3301
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3302
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3391
|
+
earlyResult = interrupted(errorInfo, fdv1Fallback);
|
|
3303
3392
|
break;
|
|
3304
3393
|
}
|
|
3305
3394
|
case 'error': {
|
|
@@ -3307,9 +3396,7 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3307
3396
|
if (action.kind === 'MISSING_PAYLOAD' || action.kind === 'PROTOCOL_ERROR') {
|
|
3308
3397
|
const errorInfo = errorInfoFromInvalidData(action.message);
|
|
3309
3398
|
logger?.warn(`Protocol error during polling: ${action.message}`);
|
|
3310
|
-
earlyResult =
|
|
3311
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3312
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3399
|
+
earlyResult = interrupted(errorInfo, fdv1Fallback);
|
|
3313
3400
|
}
|
|
3314
3401
|
else {
|
|
3315
3402
|
// Non-actionable errors (UNKNOWN_EVENT) are logged but don't stop processing
|
|
@@ -3325,19 +3412,18 @@ function processEvents(events, oneShot, fdv1Fallback, environmentId, logger) {
|
|
|
3325
3412
|
// Events didn't produce a result
|
|
3326
3413
|
const errorInfo = errorInfoFromUnknown('Unexpected end of polling response');
|
|
3327
3414
|
logger?.error('Unexpected end of polling response');
|
|
3328
|
-
return
|
|
3415
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3329
3416
|
}
|
|
3330
3417
|
/**
|
|
3331
3418
|
* Performs a single FDv2 poll request, processes the protocol response, and
|
|
3332
3419
|
* returns an {@link FDv2SourceResult}.
|
|
3333
3420
|
*
|
|
3334
|
-
*
|
|
3335
|
-
*
|
|
3336
|
-
* produce interrupted results.
|
|
3421
|
+
* Recoverable errors produce interrupted results; unrecoverable HTTP errors
|
|
3422
|
+
* produce terminal errors.
|
|
3337
3423
|
*
|
|
3338
3424
|
* @internal
|
|
3339
3425
|
*/
|
|
3340
|
-
async function poll(requestor, basis,
|
|
3426
|
+
async function poll(requestor, basis, logger) {
|
|
3341
3427
|
let fdv1Fallback = false;
|
|
3342
3428
|
let environmentId;
|
|
3343
3429
|
try {
|
|
@@ -3348,7 +3434,6 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3348
3434
|
// (Spec Requirement 10.1.2)
|
|
3349
3435
|
if (response.status === 304) {
|
|
3350
3436
|
const nonePayload = {
|
|
3351
|
-
id: '',
|
|
3352
3437
|
version: 0,
|
|
3353
3438
|
type: 'none',
|
|
3354
3439
|
updates: [],
|
|
@@ -3359,9 +3444,6 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3359
3444
|
if (response.status < 200 || response.status >= 300) {
|
|
3360
3445
|
const errorInfo = errorInfoFromHttpError(response.status);
|
|
3361
3446
|
logger?.error(`Polling request failed with HTTP error: ${response.status}`);
|
|
3362
|
-
if (oneShot) {
|
|
3363
|
-
return terminalError(errorInfo, fdv1Fallback);
|
|
3364
|
-
}
|
|
3365
3447
|
const recoverable = response.status <= 0 || isHttpRecoverable(response.status);
|
|
3366
3448
|
return recoverable
|
|
3367
3449
|
? interrupted(errorInfo, fdv1Fallback)
|
|
@@ -3371,9 +3453,7 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3371
3453
|
if (!response.body) {
|
|
3372
3454
|
const errorInfo = errorInfoFromInvalidData('Empty response body');
|
|
3373
3455
|
logger?.error('Polling request received empty response body');
|
|
3374
|
-
return
|
|
3375
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3376
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3456
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3377
3457
|
}
|
|
3378
3458
|
let parsed;
|
|
3379
3459
|
try {
|
|
@@ -3382,34 +3462,36 @@ async function poll(requestor, basis, oneShot, logger) {
|
|
|
3382
3462
|
catch {
|
|
3383
3463
|
const errorInfo = errorInfoFromInvalidData('Malformed JSON data in polling response');
|
|
3384
3464
|
logger?.error('Polling request received malformed data');
|
|
3385
|
-
return
|
|
3386
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3387
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3465
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3388
3466
|
}
|
|
3389
3467
|
if (!Array.isArray(parsed.events)) {
|
|
3390
3468
|
const errorInfo = errorInfoFromInvalidData('Invalid polling response: missing or invalid events array');
|
|
3391
3469
|
logger?.error('Polling response does not contain a valid events array');
|
|
3392
|
-
return
|
|
3393
|
-
? terminalError(errorInfo, fdv1Fallback)
|
|
3394
|
-
: interrupted(errorInfo, fdv1Fallback);
|
|
3470
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3395
3471
|
}
|
|
3396
|
-
return processEvents(parsed.events,
|
|
3472
|
+
return processEvents(parsed.events, fdv1Fallback, environmentId, logger);
|
|
3397
3473
|
}
|
|
3398
3474
|
catch (err) {
|
|
3399
3475
|
// Network or other I/O error from the fetch itself
|
|
3400
3476
|
const message = err?.message ?? String(err);
|
|
3401
3477
|
logger?.error(`Polling request failed with network error: ${message}`);
|
|
3402
3478
|
const errorInfo = errorInfoFromNetworkError(message);
|
|
3403
|
-
return
|
|
3479
|
+
return interrupted(errorInfo, fdv1Fallback);
|
|
3404
3480
|
}
|
|
3405
3481
|
}
|
|
3406
3482
|
|
|
3483
|
+
const SHUTDOWN = Symbol('shutdown');
|
|
3407
3484
|
/**
|
|
3408
|
-
* Creates a
|
|
3409
|
-
*
|
|
3485
|
+
* Creates a polling initializer that performs an FDv2 poll request with
|
|
3486
|
+
* retry logic. Retries up to 3 times on recoverable errors with a 1-second
|
|
3487
|
+
* delay between attempts.
|
|
3410
3488
|
*
|
|
3411
|
-
*
|
|
3412
|
-
*
|
|
3489
|
+
* Unrecoverable errors (401, 403, etc.) are returned immediately as terminal
|
|
3490
|
+
* errors. After exhausting retries on recoverable errors, the result is
|
|
3491
|
+
* converted to a terminal error.
|
|
3492
|
+
*
|
|
3493
|
+
* If `close()` is called during a poll or retry delay, the result will be
|
|
3494
|
+
* a shutdown status.
|
|
3413
3495
|
*
|
|
3414
3496
|
* @internal
|
|
3415
3497
|
*/
|
|
@@ -3420,12 +3502,40 @@ function createPollingInitializer(requestor, logger, selectorGetter) {
|
|
|
3420
3502
|
});
|
|
3421
3503
|
return {
|
|
3422
3504
|
async run() {
|
|
3423
|
-
const
|
|
3424
|
-
|
|
3425
|
-
|
|
3505
|
+
const maxRetries = 3;
|
|
3506
|
+
const retryDelayMs = 1000;
|
|
3507
|
+
const selector = selectorGetter();
|
|
3508
|
+
let lastResult;
|
|
3509
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
3510
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3511
|
+
const result = await Promise.race([shutdownPromise, poll(requestor, selector, logger)]);
|
|
3512
|
+
if (result === SHUTDOWN) {
|
|
3513
|
+
return shutdown();
|
|
3514
|
+
}
|
|
3515
|
+
if (result.type === 'changeSet') {
|
|
3516
|
+
return result;
|
|
3517
|
+
}
|
|
3518
|
+
// Non-retryable status (terminal_error, goodbye) -> return immediately
|
|
3519
|
+
if (result.state !== 'interrupted') {
|
|
3520
|
+
return result;
|
|
3521
|
+
}
|
|
3522
|
+
// Recoverable error — save and potentially retry
|
|
3523
|
+
lastResult = result;
|
|
3524
|
+
if (attempt < maxRetries) {
|
|
3525
|
+
logger?.warn(`Recoverable polling error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${retryDelayMs}ms...`);
|
|
3526
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3527
|
+
const sleepResult = await Promise.race([shutdownPromise, sleep(retryDelayMs)]);
|
|
3528
|
+
if (sleepResult === SHUTDOWN) {
|
|
3529
|
+
return shutdown();
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
// Convert final interrupted -> terminal_error
|
|
3534
|
+
const status = lastResult;
|
|
3535
|
+
return terminalError(status.errorInfo, status.fdv1Fallback);
|
|
3426
3536
|
},
|
|
3427
3537
|
close() {
|
|
3428
|
-
shutdownResolve?.(
|
|
3538
|
+
shutdownResolve?.(SHUTDOWN);
|
|
3429
3539
|
shutdownResolve = undefined;
|
|
3430
3540
|
},
|
|
3431
3541
|
};
|
|
@@ -3488,7 +3598,7 @@ function createPollingSynchronizer(requestor, logger, selectorGetter, pollInterv
|
|
|
3488
3598
|
}
|
|
3489
3599
|
const startTime = Date.now();
|
|
3490
3600
|
try {
|
|
3491
|
-
const result = await poll(requestor, selectorGetter(),
|
|
3601
|
+
const result = await poll(requestor, selectorGetter(), logger);
|
|
3492
3602
|
if (stopped) {
|
|
3493
3603
|
return;
|
|
3494
3604
|
}
|
|
@@ -3565,6 +3675,100 @@ function createSynchronizerSlot(factory, options) {
|
|
|
3565
3675
|
const state = options?.initialState ?? (isFDv1Fallback ? 'blocked' : 'available');
|
|
3566
3676
|
return { factory, isFDv1Fallback, state };
|
|
3567
3677
|
}
|
|
3678
|
+
/**
|
|
3679
|
+
* Creates a {@link SourceManager} that coordinates initializer and
|
|
3680
|
+
* synchronizer lifecycle.
|
|
3681
|
+
*
|
|
3682
|
+
* @param initializerFactories Ordered list of initializer factories.
|
|
3683
|
+
* @param synchronizerSlots Ordered list of synchronizer slots with state.
|
|
3684
|
+
* @param selectorGetter Closure that returns the current selector string.
|
|
3685
|
+
*/
|
|
3686
|
+
function createSourceManager(initializerFactories, synchronizerSlots, selectorGetter) {
|
|
3687
|
+
let activeSource;
|
|
3688
|
+
let initializerIndex = -1;
|
|
3689
|
+
let synchronizerIndex = -1;
|
|
3690
|
+
let isShutdown = false;
|
|
3691
|
+
function closeActiveSource() {
|
|
3692
|
+
if (activeSource) {
|
|
3693
|
+
activeSource.close();
|
|
3694
|
+
activeSource = undefined;
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
function findFirstAvailableIndex() {
|
|
3698
|
+
return synchronizerSlots.findIndex((slot) => slot.state === 'available');
|
|
3699
|
+
}
|
|
3700
|
+
return {
|
|
3701
|
+
get isShutdown() {
|
|
3702
|
+
return isShutdown;
|
|
3703
|
+
},
|
|
3704
|
+
getNextInitializerAndSetActive() {
|
|
3705
|
+
if (isShutdown) {
|
|
3706
|
+
return undefined;
|
|
3707
|
+
}
|
|
3708
|
+
initializerIndex += 1;
|
|
3709
|
+
if (initializerIndex >= initializerFactories.length) {
|
|
3710
|
+
return undefined;
|
|
3711
|
+
}
|
|
3712
|
+
closeActiveSource();
|
|
3713
|
+
const initializer = initializerFactories[initializerIndex](selectorGetter);
|
|
3714
|
+
activeSource = initializer;
|
|
3715
|
+
return initializer;
|
|
3716
|
+
},
|
|
3717
|
+
getNextAvailableSynchronizerAndSetActive() {
|
|
3718
|
+
if (isShutdown || synchronizerSlots.length === 0) {
|
|
3719
|
+
return undefined;
|
|
3720
|
+
}
|
|
3721
|
+
// Scan all slots starting from the position after the current one,
|
|
3722
|
+
// wrapping around to the beginning if needed. This matches the Java
|
|
3723
|
+
// SourceManager behavior where synchronizers cycle rather than exhausting.
|
|
3724
|
+
let visited = 0;
|
|
3725
|
+
while (visited < synchronizerSlots.length) {
|
|
3726
|
+
synchronizerIndex += 1;
|
|
3727
|
+
if (synchronizerIndex >= synchronizerSlots.length) {
|
|
3728
|
+
synchronizerIndex = 0;
|
|
3729
|
+
}
|
|
3730
|
+
const candidate = synchronizerSlots[synchronizerIndex];
|
|
3731
|
+
if (candidate.state === 'available') {
|
|
3732
|
+
closeActiveSource();
|
|
3733
|
+
const synchronizer = candidate.factory(selectorGetter);
|
|
3734
|
+
activeSource = synchronizer;
|
|
3735
|
+
return synchronizer;
|
|
3736
|
+
}
|
|
3737
|
+
visited += 1;
|
|
3738
|
+
}
|
|
3739
|
+
return undefined;
|
|
3740
|
+
},
|
|
3741
|
+
blockCurrentSynchronizer() {
|
|
3742
|
+
if (synchronizerIndex >= 0 && synchronizerIndex < synchronizerSlots.length) {
|
|
3743
|
+
// eslint-disable-next-line no-param-reassign
|
|
3744
|
+
synchronizerSlots[synchronizerIndex].state = 'blocked';
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3747
|
+
resetSourceIndex() {
|
|
3748
|
+
synchronizerIndex = -1;
|
|
3749
|
+
},
|
|
3750
|
+
fdv1Fallback() {
|
|
3751
|
+
synchronizerSlots.forEach((slot) => {
|
|
3752
|
+
// eslint-disable-next-line no-param-reassign
|
|
3753
|
+
slot.state = slot.isFDv1Fallback ? 'available' : 'blocked';
|
|
3754
|
+
});
|
|
3755
|
+
synchronizerIndex = -1;
|
|
3756
|
+
},
|
|
3757
|
+
isPrimeSynchronizer() {
|
|
3758
|
+
return synchronizerIndex === findFirstAvailableIndex();
|
|
3759
|
+
},
|
|
3760
|
+
getAvailableSynchronizerCount() {
|
|
3761
|
+
return synchronizerSlots.filter((slot) => slot.state === 'available').length;
|
|
3762
|
+
},
|
|
3763
|
+
hasFDv1Fallback() {
|
|
3764
|
+
return synchronizerSlots.some((slot) => slot.isFDv1Fallback);
|
|
3765
|
+
},
|
|
3766
|
+
close() {
|
|
3767
|
+
isShutdown = true;
|
|
3768
|
+
closeActiveSource();
|
|
3769
|
+
},
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3568
3772
|
|
|
3569
3773
|
/**
|
|
3570
3774
|
* FDv2 event names to listen for on the EventSource. This must stay in sync
|
|
@@ -3728,6 +3932,7 @@ function createStreamingBase(config) {
|
|
|
3728
3932
|
initialRetryDelayMillis: config.initialRetryDelayMillis,
|
|
3729
3933
|
readTimeoutMillis: 5 * 60 * 1000,
|
|
3730
3934
|
retryResetIntervalMillis: 60 * 1000,
|
|
3935
|
+
urlBuilder: buildStreamUri,
|
|
3731
3936
|
});
|
|
3732
3937
|
eventSource = es;
|
|
3733
3938
|
attachFDv2Listeners(es);
|
|
@@ -3859,7 +4064,7 @@ function createStreamingSynchronizer(base) {
|
|
|
3859
4064
|
|
|
3860
4065
|
function createPingHandler(requestor, selectorGetter, logger) {
|
|
3861
4066
|
return {
|
|
3862
|
-
handlePing: () => poll(requestor, selectorGetter(),
|
|
4067
|
+
handlePing: () => poll(requestor, selectorGetter(), logger),
|
|
3863
4068
|
};
|
|
3864
4069
|
}
|
|
3865
4070
|
/**
|
|
@@ -3950,5 +4155,929 @@ function createDefaultSourceFactoryProvider() {
|
|
|
3950
4155
|
};
|
|
3951
4156
|
}
|
|
3952
4157
|
|
|
3953
|
-
|
|
4158
|
+
function flagsToPayload(flags) {
|
|
4159
|
+
const updates = Object.entries(flags).map(([key, flag]) => ({
|
|
4160
|
+
kind: 'flag',
|
|
4161
|
+
key,
|
|
4162
|
+
version: flag.version ?? 1,
|
|
4163
|
+
object: flag,
|
|
4164
|
+
}));
|
|
4165
|
+
return {
|
|
4166
|
+
version: 1,
|
|
4167
|
+
type: 'full',
|
|
4168
|
+
updates,
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
/**
|
|
4172
|
+
* Creates a polling synchronizer that polls an FDv1 endpoint and produces
|
|
4173
|
+
* results in the FDv2 {@link Synchronizer} pull model.
|
|
4174
|
+
*
|
|
4175
|
+
* This is a standalone implementation (not a wrapper around the existing FDv1
|
|
4176
|
+
* `PollingProcessor`) because the FDv1 fallback is temporary and will be
|
|
4177
|
+
* removed in the next major version. A focused implementation is simpler than
|
|
4178
|
+
* an adapter layer.
|
|
4179
|
+
*
|
|
4180
|
+
* Polling starts lazily on the first call to `next()`. Each poll returns the
|
|
4181
|
+
* complete flag set as a `changeSet` result with `type: 'full'` and an empty
|
|
4182
|
+
* selector.
|
|
4183
|
+
*
|
|
4184
|
+
* @param requestor FDv1 requestor configured with the appropriate endpoint.
|
|
4185
|
+
* @param pollIntervalMs Interval between polls in milliseconds.
|
|
4186
|
+
* @param logger Optional logger.
|
|
4187
|
+
* @internal
|
|
4188
|
+
*/
|
|
4189
|
+
function createFDv1PollingSynchronizer(requestor, pollIntervalMs, logger) {
|
|
4190
|
+
const resultQueue = createAsyncQueue();
|
|
4191
|
+
let shutdownResolve;
|
|
4192
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
4193
|
+
shutdownResolve = resolve;
|
|
4194
|
+
});
|
|
4195
|
+
let timeoutHandle;
|
|
4196
|
+
let stopped = false;
|
|
4197
|
+
let started = false;
|
|
4198
|
+
function scheduleNextPoll(startTime) {
|
|
4199
|
+
if (!stopped) {
|
|
4200
|
+
const elapsed = Date.now() - startTime;
|
|
4201
|
+
const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
|
|
4202
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4203
|
+
timeoutHandle = setTimeout(doPoll, sleepFor);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
async function doPoll() {
|
|
4207
|
+
if (stopped) {
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
logger?.debug('Polling FDv1 endpoint for feature flag updates');
|
|
4211
|
+
const startTime = Date.now();
|
|
4212
|
+
try {
|
|
4213
|
+
const body = await requestor.requestPayload();
|
|
4214
|
+
if (stopped) {
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
let payload;
|
|
4218
|
+
try {
|
|
4219
|
+
const flags = JSON.parse(body);
|
|
4220
|
+
payload = flagsToPayload(flags);
|
|
4221
|
+
}
|
|
4222
|
+
catch {
|
|
4223
|
+
logger?.error('FDv1 polling received malformed data');
|
|
4224
|
+
resultQueue.put({
|
|
4225
|
+
type: 'status',
|
|
4226
|
+
state: 'interrupted',
|
|
4227
|
+
errorInfo: errorInfoFromInvalidData('Malformed data in FDv1 polling response'),
|
|
4228
|
+
fdv1Fallback: false,
|
|
4229
|
+
});
|
|
4230
|
+
scheduleNextPoll(startTime);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
resultQueue.put(changeSet(payload, false));
|
|
4234
|
+
}
|
|
4235
|
+
catch (err) {
|
|
4236
|
+
if (stopped) {
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
const requestError = err;
|
|
4240
|
+
if (requestError.status !== undefined) {
|
|
4241
|
+
if (!isHttpRecoverable(requestError.status)) {
|
|
4242
|
+
logger?.error(httpErrorMessage(err, 'FDv1 polling request'));
|
|
4243
|
+
stopped = true;
|
|
4244
|
+
shutdownResolve?.(terminalError(errorInfoFromHttpError(requestError.status), false));
|
|
4245
|
+
shutdownResolve = undefined;
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
logger?.warn(httpErrorMessage(err, 'FDv1 polling request', 'will retry'));
|
|
4250
|
+
resultQueue.put({
|
|
4251
|
+
type: 'status',
|
|
4252
|
+
state: 'interrupted',
|
|
4253
|
+
errorInfo: requestError.status
|
|
4254
|
+
? errorInfoFromHttpError(requestError.status)
|
|
4255
|
+
: errorInfoFromNetworkError(requestError.message),
|
|
4256
|
+
fdv1Fallback: false,
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
scheduleNextPoll(startTime);
|
|
4260
|
+
}
|
|
4261
|
+
return {
|
|
4262
|
+
next() {
|
|
4263
|
+
if (!started) {
|
|
4264
|
+
started = true;
|
|
4265
|
+
doPoll();
|
|
4266
|
+
}
|
|
4267
|
+
return Promise.race([shutdownPromise, resultQueue.take()]);
|
|
4268
|
+
},
|
|
4269
|
+
close() {
|
|
4270
|
+
stopped = true;
|
|
4271
|
+
if (timeoutHandle !== undefined) {
|
|
4272
|
+
clearTimeout(timeoutHandle);
|
|
4273
|
+
timeoutHandle = undefined;
|
|
4274
|
+
}
|
|
4275
|
+
shutdownResolve?.(shutdown());
|
|
4276
|
+
shutdownResolve = undefined;
|
|
4277
|
+
},
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const DEFAULT_FALLBACK_TIMEOUT_MS = 2 * 60 * 1000; // 120 seconds
|
|
4282
|
+
const DEFAULT_RECOVERY_TIMEOUT_MS = 5 * 60 * 1000; // 300 seconds
|
|
4283
|
+
/**
|
|
4284
|
+
* Creates a cancelable timer that resolves with the given {@link ConditionType}
|
|
4285
|
+
* when it fires. Wraps {@link cancelableTimedPromise} to convert its
|
|
4286
|
+
* reject-on-timeout semantics into resolve-with-type semantics.
|
|
4287
|
+
*/
|
|
4288
|
+
function conditionTimer(timeoutMs, type, taskName) {
|
|
4289
|
+
const timed = cancelableTimedPromise(timeoutMs / 1000, taskName);
|
|
4290
|
+
return {
|
|
4291
|
+
promise: timed.promise.then(() => new Promise(() => { }), // cancelled — never settle
|
|
4292
|
+
() => type),
|
|
4293
|
+
cancel: timed.cancel,
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* Creates a timed {@link Condition}.
|
|
4298
|
+
*
|
|
4299
|
+
* @param timeoutMs Time in milliseconds before the condition fires.
|
|
4300
|
+
* @param type The {@link ConditionType} to resolve with when the timer fires.
|
|
4301
|
+
* @param informHandler Optional callback invoked on each `inform()` call.
|
|
4302
|
+
* When omitted, the timer starts immediately and `inform()` is a no-op.
|
|
4303
|
+
* When provided, the timer must be started explicitly via `controls.start()`.
|
|
4304
|
+
*/
|
|
4305
|
+
function createCondition(timeoutMs, type, informHandler) {
|
|
4306
|
+
let resolve;
|
|
4307
|
+
let timer;
|
|
4308
|
+
let closed = false;
|
|
4309
|
+
const promise = new Promise((res) => {
|
|
4310
|
+
resolve = res;
|
|
4311
|
+
});
|
|
4312
|
+
function startTimer() {
|
|
4313
|
+
if (!timer && !closed) {
|
|
4314
|
+
timer = conditionTimer(timeoutMs, type, `${type} condition`);
|
|
4315
|
+
timer.promise.then((t) => {
|
|
4316
|
+
timer = undefined;
|
|
4317
|
+
resolve?.(t);
|
|
4318
|
+
});
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
function cancelTimer() {
|
|
4322
|
+
timer?.cancel();
|
|
4323
|
+
timer = undefined;
|
|
4324
|
+
}
|
|
4325
|
+
// No inform handler — start immediately (recovery behavior).
|
|
4326
|
+
if (!informHandler) {
|
|
4327
|
+
startTimer();
|
|
4328
|
+
}
|
|
4329
|
+
return {
|
|
4330
|
+
promise,
|
|
4331
|
+
inform(result) {
|
|
4332
|
+
if (closed) {
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
informHandler?.(result, { start: startTimer, cancel: cancelTimer });
|
|
4336
|
+
},
|
|
4337
|
+
close() {
|
|
4338
|
+
closed = true;
|
|
4339
|
+
cancelTimer();
|
|
4340
|
+
},
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Creates a fallback condition. The condition starts a timer when an
|
|
4345
|
+
* `interrupted` status is received and cancels it when a `changeSet` is
|
|
4346
|
+
* received. If the timer fires, the condition resolves with `'fallback'`.
|
|
4347
|
+
*/
|
|
4348
|
+
function createFallbackCondition(timeoutMs) {
|
|
4349
|
+
return createCondition(timeoutMs, 'fallback', (result, { start, cancel }) => {
|
|
4350
|
+
if (result.type === 'changeSet') {
|
|
4351
|
+
cancel();
|
|
4352
|
+
}
|
|
4353
|
+
else if (result.type === 'status' && result.state === 'interrupted') {
|
|
4354
|
+
start();
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
/**
|
|
4359
|
+
* Creates a recovery condition. The condition starts a timer immediately
|
|
4360
|
+
* and resolves with `'recovery'` when it fires. It ignores all `inform()`
|
|
4361
|
+
* calls.
|
|
4362
|
+
*/
|
|
4363
|
+
function createRecoveryCondition(timeoutMs) {
|
|
4364
|
+
return createCondition(timeoutMs, 'recovery');
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Creates a group of conditions that are managed together.
|
|
4368
|
+
*
|
|
4369
|
+
* @param conditions The conditions to group.
|
|
4370
|
+
*/
|
|
4371
|
+
function createConditionGroup(conditions) {
|
|
4372
|
+
return {
|
|
4373
|
+
promise: conditions.length === 0 ? undefined : Promise.race(conditions.map((c) => c.promise)),
|
|
4374
|
+
inform(result) {
|
|
4375
|
+
conditions.forEach((condition) => condition.inform(result));
|
|
4376
|
+
},
|
|
4377
|
+
close() {
|
|
4378
|
+
conditions.forEach((condition) => condition.close());
|
|
4379
|
+
},
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* Determines which conditions to create based on the synchronizer's position
|
|
4384
|
+
* and availability.
|
|
4385
|
+
*
|
|
4386
|
+
* - If there is only one available synchronizer, no conditions are needed
|
|
4387
|
+
* (there is nowhere to fall back to).
|
|
4388
|
+
* - If the current synchronizer is the primary (first available), only a
|
|
4389
|
+
* fallback condition is created.
|
|
4390
|
+
* - If the current synchronizer is non-primary, both fallback and recovery
|
|
4391
|
+
* conditions are created.
|
|
4392
|
+
*
|
|
4393
|
+
* @param availableSyncCount Number of available (non-blocked) synchronizers.
|
|
4394
|
+
* @param isPrime Whether the current synchronizer is the primary.
|
|
4395
|
+
* @param fallbackTimeoutMs Fallback condition timeout.
|
|
4396
|
+
* @param recoveryTimeoutMs Recovery condition timeout.
|
|
4397
|
+
*/
|
|
4398
|
+
function getConditions(availableSyncCount, isPrime, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS) {
|
|
4399
|
+
if (availableSyncCount <= 1) {
|
|
4400
|
+
return createConditionGroup([]);
|
|
4401
|
+
}
|
|
4402
|
+
if (isPrime) {
|
|
4403
|
+
return createConditionGroup([createFallbackCondition(fallbackTimeoutMs)]);
|
|
4404
|
+
}
|
|
4405
|
+
return createConditionGroup([
|
|
4406
|
+
createFallbackCondition(fallbackTimeoutMs),
|
|
4407
|
+
createRecoveryCondition(recoveryTimeoutMs),
|
|
4408
|
+
]);
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
/**
|
|
4412
|
+
* Creates an {@link FDv2DataSource} orchestrator.
|
|
4413
|
+
*/
|
|
4414
|
+
function createFDv2DataSource(config) {
|
|
4415
|
+
const { initializerFactories, synchronizerSlots, dataCallback, statusManager, selectorGetter, logger, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS, } = config;
|
|
4416
|
+
let initialized = false;
|
|
4417
|
+
let closed = false;
|
|
4418
|
+
let dataReceived = false;
|
|
4419
|
+
let initResolve;
|
|
4420
|
+
let initReject;
|
|
4421
|
+
const sourceManager = createSourceManager(initializerFactories, synchronizerSlots, selectorGetter);
|
|
4422
|
+
function markInitialized() {
|
|
4423
|
+
if (!initialized) {
|
|
4424
|
+
initialized = true;
|
|
4425
|
+
initResolve?.();
|
|
4426
|
+
initResolve = undefined;
|
|
4427
|
+
initReject = undefined;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
function applyChangeSet(result) {
|
|
4431
|
+
dataCallback(result.payload);
|
|
4432
|
+
statusManager.requestStateUpdate('VALID');
|
|
4433
|
+
}
|
|
4434
|
+
function reportStatusError(result) {
|
|
4435
|
+
if (result.errorInfo) {
|
|
4436
|
+
statusManager.reportError(result.errorInfo.kind, result.errorInfo.message, result.errorInfo.statusCode, result.state === 'interrupted');
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
function handleFdv1Fallback(result) {
|
|
4440
|
+
if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) {
|
|
4441
|
+
sourceManager.fdv1Fallback();
|
|
4442
|
+
return true;
|
|
4443
|
+
}
|
|
4444
|
+
return false;
|
|
4445
|
+
}
|
|
4446
|
+
/* eslint-disable no-await-in-loop */
|
|
4447
|
+
// The orchestration loops intentionally use await-in-loop for sequential
|
|
4448
|
+
// state machine processing — one result at a time.
|
|
4449
|
+
async function runInitializers() {
|
|
4450
|
+
while (!closed) {
|
|
4451
|
+
const initializer = sourceManager.getNextInitializerAndSetActive();
|
|
4452
|
+
if (initializer === undefined) {
|
|
4453
|
+
break;
|
|
4454
|
+
}
|
|
4455
|
+
const result = await initializer.run();
|
|
4456
|
+
if (closed) {
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
if (result.type === 'changeSet') {
|
|
4460
|
+
applyChangeSet(result);
|
|
4461
|
+
if (handleFdv1Fallback(result)) {
|
|
4462
|
+
// FDv1 fallback triggered during initialization — data was received
|
|
4463
|
+
// but we should move to synchronizers where the FDv1 adapter will run.
|
|
4464
|
+
dataReceived = true;
|
|
4465
|
+
break;
|
|
4466
|
+
}
|
|
4467
|
+
if (result.payload.state) {
|
|
4468
|
+
// Got basis data with a selector — initialization is complete.
|
|
4469
|
+
markInitialized();
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
// Got data but no selector (e.g., cache). Record that data was
|
|
4473
|
+
// received and continue to the next initializer.
|
|
4474
|
+
dataReceived = true;
|
|
4475
|
+
}
|
|
4476
|
+
else if (result.type === 'status') {
|
|
4477
|
+
switch (result.state) {
|
|
4478
|
+
case 'interrupted':
|
|
4479
|
+
case 'terminal_error':
|
|
4480
|
+
logger?.warn(`Initializer failed: ${result.errorInfo?.message ?? 'unknown error'}`);
|
|
4481
|
+
reportStatusError(result);
|
|
4482
|
+
break;
|
|
4483
|
+
case 'shutdown':
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
handleFdv1Fallback(result);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
// All initializers exhausted.
|
|
4490
|
+
if (dataReceived) {
|
|
4491
|
+
markInitialized();
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
async function runSynchronizers() {
|
|
4495
|
+
while (!closed) {
|
|
4496
|
+
const synchronizer = sourceManager.getNextAvailableSynchronizerAndSetActive();
|
|
4497
|
+
if (synchronizer === undefined) {
|
|
4498
|
+
if (!initialized) {
|
|
4499
|
+
initReject?.(new Error('All data sources exhausted without receiving data.'));
|
|
4500
|
+
initResolve = undefined;
|
|
4501
|
+
initReject = undefined;
|
|
4502
|
+
}
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
const conditions = getConditions(sourceManager.getAvailableSynchronizerCount(), sourceManager.isPrimeSynchronizer(), fallbackTimeoutMs, recoveryTimeoutMs);
|
|
4506
|
+
if (conditions.promise) {
|
|
4507
|
+
logger?.debug('Fallback condition active for current synchronizer.');
|
|
4508
|
+
}
|
|
4509
|
+
// try/finally ensures conditions are closed on all code paths.
|
|
4510
|
+
let synchronizerRunning = true;
|
|
4511
|
+
try {
|
|
4512
|
+
while (!closed && synchronizerRunning) {
|
|
4513
|
+
const syncPromise = synchronizer
|
|
4514
|
+
.next()
|
|
4515
|
+
.then((value) => ({ source: 'sync', value }));
|
|
4516
|
+
const racers = [syncPromise];
|
|
4517
|
+
if (conditions.promise !== undefined) {
|
|
4518
|
+
racers.push(conditions.promise.then((value) => ({ source: 'condition', value })));
|
|
4519
|
+
}
|
|
4520
|
+
const winner = await Promise.race(racers);
|
|
4521
|
+
if (closed) {
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
if (winner.source === 'condition') {
|
|
4525
|
+
const conditionType = winner.value;
|
|
4526
|
+
if (conditionType === 'fallback') {
|
|
4527
|
+
logger?.warn('Fallback condition fired, moving to next synchronizer.');
|
|
4528
|
+
}
|
|
4529
|
+
else if (conditionType === 'recovery') {
|
|
4530
|
+
logger?.info('Recovery condition fired, resetting to primary synchronizer.');
|
|
4531
|
+
sourceManager.resetSourceIndex();
|
|
4532
|
+
}
|
|
4533
|
+
synchronizerRunning = false;
|
|
4534
|
+
}
|
|
4535
|
+
else {
|
|
4536
|
+
// Synchronizer produced a result.
|
|
4537
|
+
const syncResult = winner.value;
|
|
4538
|
+
conditions.inform(syncResult);
|
|
4539
|
+
if (syncResult.type === 'changeSet') {
|
|
4540
|
+
applyChangeSet(syncResult);
|
|
4541
|
+
if (!initialized) {
|
|
4542
|
+
markInitialized();
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
else if (syncResult.type === 'status') {
|
|
4546
|
+
switch (syncResult.state) {
|
|
4547
|
+
case 'interrupted':
|
|
4548
|
+
logger?.warn(`Synchronizer interrupted: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
|
|
4549
|
+
reportStatusError(syncResult);
|
|
4550
|
+
break;
|
|
4551
|
+
case 'terminal_error':
|
|
4552
|
+
logger?.error(`Synchronizer terminal error: ${syncResult.errorInfo?.message ?? 'unknown error'}`);
|
|
4553
|
+
reportStatusError(syncResult);
|
|
4554
|
+
sourceManager.blockCurrentSynchronizer();
|
|
4555
|
+
synchronizerRunning = false;
|
|
4556
|
+
break;
|
|
4557
|
+
case 'shutdown':
|
|
4558
|
+
return;
|
|
4559
|
+
case 'goodbye':
|
|
4560
|
+
// The synchronizer will handle reconnection internally.
|
|
4561
|
+
break;
|
|
4562
|
+
default:
|
|
4563
|
+
break;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
// Check for FDv1 fallback after all result handling — single location.
|
|
4567
|
+
if (handleFdv1Fallback(syncResult)) {
|
|
4568
|
+
synchronizerRunning = false;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
finally {
|
|
4574
|
+
conditions.close();
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
/* eslint-enable no-await-in-loop */
|
|
4579
|
+
async function runOrchestration() {
|
|
4580
|
+
// No sources configured at all — nothing to wait for, immediately valid.
|
|
4581
|
+
if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
|
|
4582
|
+
statusManager.requestStateUpdate('VALID');
|
|
4583
|
+
markInitialized();
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
await runInitializers();
|
|
4587
|
+
if (!closed) {
|
|
4588
|
+
await runSynchronizers();
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
return {
|
|
4592
|
+
start() {
|
|
4593
|
+
return new Promise((resolve, reject) => {
|
|
4594
|
+
initResolve = resolve;
|
|
4595
|
+
initReject = reject;
|
|
4596
|
+
statusManager.requestStateUpdate('INITIALIZING');
|
|
4597
|
+
runOrchestration()
|
|
4598
|
+
.then(() => {
|
|
4599
|
+
// Orchestration completed without error. If the init promise was
|
|
4600
|
+
// never resolved (e.g., close() called during init, or all sources
|
|
4601
|
+
// exhausted without data and no synchronizers), resolve it now.
|
|
4602
|
+
// This prevents the start() promise from hanging forever.
|
|
4603
|
+
if (!initialized) {
|
|
4604
|
+
initReject?.(new Error('Data source closed before initialization completed.'));
|
|
4605
|
+
initResolve = undefined;
|
|
4606
|
+
initReject = undefined;
|
|
4607
|
+
}
|
|
4608
|
+
})
|
|
4609
|
+
.catch((err) => {
|
|
4610
|
+
if (!initialized) {
|
|
4611
|
+
initReject?.(err instanceof Error ? err : new Error(String(err)));
|
|
4612
|
+
initResolve = undefined;
|
|
4613
|
+
initReject = undefined;
|
|
4614
|
+
}
|
|
4615
|
+
else {
|
|
4616
|
+
logger?.error(`Orchestration error: ${err}`);
|
|
4617
|
+
}
|
|
4618
|
+
});
|
|
4619
|
+
});
|
|
4620
|
+
},
|
|
4621
|
+
close() {
|
|
4622
|
+
closed = true;
|
|
4623
|
+
sourceManager.close();
|
|
4624
|
+
},
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
/** Default debounce window duration in milliseconds. */
|
|
4629
|
+
const DEFAULT_DEBOUNCE_MS = 1000;
|
|
4630
|
+
/**
|
|
4631
|
+
* Creates a {@link StateDebounceManager}.
|
|
4632
|
+
*
|
|
4633
|
+
* The manager accumulates state changes from network, lifecycle, and
|
|
4634
|
+
* connection mode events. Each event updates the relevant component
|
|
4635
|
+
* of the pending state and resets the debounce timer. When the timer
|
|
4636
|
+
* fires, the reconciliation callback receives the final combined state.
|
|
4637
|
+
*
|
|
4638
|
+
* @param config Configuration for the debounce manager.
|
|
4639
|
+
*/
|
|
4640
|
+
function createStateDebounceManager(config) {
|
|
4641
|
+
const { initialState, onReconcile, debounceMs = DEFAULT_DEBOUNCE_MS } = config;
|
|
4642
|
+
let { networkState, lifecycleState, requestedMode } = initialState;
|
|
4643
|
+
let timer;
|
|
4644
|
+
let closed = false;
|
|
4645
|
+
function getPendingState() {
|
|
4646
|
+
return { networkState, lifecycleState, requestedMode };
|
|
4647
|
+
}
|
|
4648
|
+
function resetTimer() {
|
|
4649
|
+
if (closed) {
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
if (timer !== undefined) {
|
|
4653
|
+
clearTimeout(timer);
|
|
4654
|
+
}
|
|
4655
|
+
timer = setTimeout(() => {
|
|
4656
|
+
timer = undefined;
|
|
4657
|
+
if (!closed) {
|
|
4658
|
+
onReconcile(getPendingState());
|
|
4659
|
+
}
|
|
4660
|
+
}, debounceMs);
|
|
4661
|
+
}
|
|
4662
|
+
return {
|
|
4663
|
+
setNetworkState(state) {
|
|
4664
|
+
if (networkState === state) {
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
networkState = state;
|
|
4668
|
+
resetTimer();
|
|
4669
|
+
},
|
|
4670
|
+
setLifecycleState(state) {
|
|
4671
|
+
if (lifecycleState === state) {
|
|
4672
|
+
return;
|
|
4673
|
+
}
|
|
4674
|
+
lifecycleState = state;
|
|
4675
|
+
resetTimer();
|
|
4676
|
+
},
|
|
4677
|
+
setRequestedMode(mode) {
|
|
4678
|
+
if (requestedMode === mode) {
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
requestedMode = mode;
|
|
4682
|
+
resetTimer();
|
|
4683
|
+
},
|
|
4684
|
+
close() {
|
|
4685
|
+
closed = true;
|
|
4686
|
+
if (timer !== undefined) {
|
|
4687
|
+
clearTimeout(timer);
|
|
4688
|
+
timer = undefined;
|
|
4689
|
+
}
|
|
4690
|
+
},
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
const logTag = '[FDv2DataManagerBase]';
|
|
4695
|
+
/**
|
|
4696
|
+
* Creates a shared FDv2 data manager that owns mode resolution, debouncing,
|
|
4697
|
+
* selector state, and FDv2DataSource lifecycle. Platform SDKs (browser, RN)
|
|
4698
|
+
* wrap this with platform-specific config and event wiring.
|
|
4699
|
+
*/
|
|
4700
|
+
function createFDv2DataManagerBase(baseConfig) {
|
|
4701
|
+
const { platform, flagManager, config, baseHeaders, emitter, transitionTable, foregroundMode: configuredForegroundMode, backgroundMode, modeTable, sourceFactoryProvider, buildQueryParams, fdv1Endpoints, fallbackTimeoutMs, recoveryTimeoutMs, } = baseConfig;
|
|
4702
|
+
const { logger } = config;
|
|
4703
|
+
const statusManager = createDataSourceStatusManager(emitter);
|
|
4704
|
+
const endpoints = fdv2Endpoints();
|
|
4705
|
+
// Merge user-provided connection mode overrides into the mode table.
|
|
4706
|
+
const effectiveModeTable = config.dataSystem?.connectionModes
|
|
4707
|
+
? { ...modeTable, ...config.dataSystem.connectionModes }
|
|
4708
|
+
: modeTable;
|
|
4709
|
+
// --- Mutable state ---
|
|
4710
|
+
let selector;
|
|
4711
|
+
let currentResolvedMode = configuredForegroundMode;
|
|
4712
|
+
let foregroundMode = configuredForegroundMode;
|
|
4713
|
+
let dataSource;
|
|
4714
|
+
let debounceManager;
|
|
4715
|
+
let identifiedContext;
|
|
4716
|
+
let factoryContext;
|
|
4717
|
+
let initialized = false;
|
|
4718
|
+
let bootstrapped = false;
|
|
4719
|
+
let closed = false;
|
|
4720
|
+
let flushCallback;
|
|
4721
|
+
// Explicit connection mode override — bypasses transition table entirely.
|
|
4722
|
+
let connectionModeOverride;
|
|
4723
|
+
// Forced/automatic streaming state for browser listener-driven streaming.
|
|
4724
|
+
let forcedStreaming;
|
|
4725
|
+
let automaticStreamingState = false;
|
|
4726
|
+
// Outstanding identify promise callbacks — needed so that mode switches
|
|
4727
|
+
// during identify can wire the new data source's completion to the
|
|
4728
|
+
// original identify promise.
|
|
4729
|
+
let pendingIdentifyResolve;
|
|
4730
|
+
let pendingIdentifyReject;
|
|
4731
|
+
// Current debounce input state.
|
|
4732
|
+
let networkState = 'available';
|
|
4733
|
+
let lifecycleState = 'foreground';
|
|
4734
|
+
// --- Helpers ---
|
|
4735
|
+
function getModeDefinition(mode) {
|
|
4736
|
+
return effectiveModeTable[mode];
|
|
4737
|
+
}
|
|
4738
|
+
function buildModeState() {
|
|
4739
|
+
return {
|
|
4740
|
+
lifecycle: lifecycleState,
|
|
4741
|
+
networkAvailable: networkState === 'available',
|
|
4742
|
+
foregroundMode,
|
|
4743
|
+
backgroundMode: backgroundMode ?? 'offline',
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Resolve the current effective connection mode.
|
|
4748
|
+
*
|
|
4749
|
+
* Priority:
|
|
4750
|
+
* 1. connectionModeOverride (set via setConnectionMode) — bypasses everything
|
|
4751
|
+
* 2. Transition table (network/lifecycle state + foreground/background modes)
|
|
4752
|
+
*/
|
|
4753
|
+
function resolveMode() {
|
|
4754
|
+
if (connectionModeOverride !== undefined) {
|
|
4755
|
+
return connectionModeOverride;
|
|
4756
|
+
}
|
|
4757
|
+
return resolveConnectionMode(transitionTable, buildModeState());
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* Resolve the foreground mode input for the transition table based on
|
|
4761
|
+
* forced/automatic streaming state.
|
|
4762
|
+
*
|
|
4763
|
+
* Priority: forcedStreaming > automaticStreaming > configuredForegroundMode
|
|
4764
|
+
*/
|
|
4765
|
+
function resolveStreamingForeground() {
|
|
4766
|
+
if (forcedStreaming === true) {
|
|
4767
|
+
return 'streaming';
|
|
4768
|
+
}
|
|
4769
|
+
if (forcedStreaming === false) {
|
|
4770
|
+
return configuredForegroundMode === 'streaming' ? 'one-shot' : configuredForegroundMode;
|
|
4771
|
+
}
|
|
4772
|
+
return automaticStreamingState ? 'streaming' : configuredForegroundMode;
|
|
4773
|
+
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Compute the effective foreground mode from streaming state and push it
|
|
4776
|
+
* through the debounce manager. Used by setForcedStreaming and
|
|
4777
|
+
* setAutomaticStreamingState.
|
|
4778
|
+
*/
|
|
4779
|
+
function pushForegroundMode() {
|
|
4780
|
+
foregroundMode = resolveStreamingForeground();
|
|
4781
|
+
debounceManager?.setRequestedMode(foregroundMode);
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* Convert a ModeDefinition's entries into concrete InitializerFactory[]
|
|
4785
|
+
* and SynchronizerSlot[] using the source factory provider.
|
|
4786
|
+
*/
|
|
4787
|
+
function buildFactories(modeDef, ctx, includeInitializers) {
|
|
4788
|
+
const initializerFactories = [];
|
|
4789
|
+
if (includeInitializers) {
|
|
4790
|
+
modeDef.initializers
|
|
4791
|
+
// Skip cache when bootstrapped — bootstrap data was applied to the
|
|
4792
|
+
// flag store before identify, so the cache would only load older data.
|
|
4793
|
+
.filter((entry) => !(bootstrapped && entry.type === 'cache'))
|
|
4794
|
+
.forEach((entry) => {
|
|
4795
|
+
const factory = sourceFactoryProvider.createInitializerFactory(entry, ctx);
|
|
4796
|
+
if (factory) {
|
|
4797
|
+
initializerFactories.push(factory);
|
|
4798
|
+
}
|
|
4799
|
+
else {
|
|
4800
|
+
logger.warn(`${logTag} Unsupported initializer type '${entry.type}'. It will be skipped.`);
|
|
4801
|
+
}
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
const synchronizerSlots = [];
|
|
4805
|
+
modeDef.synchronizers.forEach((entry) => {
|
|
4806
|
+
const slot = sourceFactoryProvider.createSynchronizerSlot(entry, ctx);
|
|
4807
|
+
if (slot) {
|
|
4808
|
+
synchronizerSlots.push(slot);
|
|
4809
|
+
}
|
|
4810
|
+
else {
|
|
4811
|
+
logger.warn(`${logTag} Unsupported synchronizer type '${entry.type}'. It will be skipped.`);
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
// Append a blocked FDv1 fallback synchronizer when configured and
|
|
4815
|
+
// when there are FDv2 synchronizers to fall back from.
|
|
4816
|
+
if (fdv1Endpoints && synchronizerSlots.length > 0) {
|
|
4817
|
+
const fallbackConfig = modeDef.fdv1Fallback;
|
|
4818
|
+
const fallbackPollIntervalMs = (fallbackConfig?.pollInterval ?? config.pollInterval) * 1000;
|
|
4819
|
+
const fallbackServiceEndpoints = fallbackConfig?.endpoints?.pollingBaseUri || fallbackConfig?.endpoints?.streamingBaseUri
|
|
4820
|
+
? new ServiceEndpoints(fallbackConfig.endpoints.streamingBaseUri ?? ctx.serviceEndpoints.streaming, fallbackConfig.endpoints.pollingBaseUri ?? ctx.serviceEndpoints.polling, ctx.serviceEndpoints.events, ctx.serviceEndpoints.analyticsEventPath, ctx.serviceEndpoints.diagnosticEventPath, ctx.serviceEndpoints.includeAuthorizationHeader, ctx.serviceEndpoints.payloadFilterKey)
|
|
4821
|
+
: ctx.serviceEndpoints;
|
|
4822
|
+
const fdv1RequestorFactory = () => makeRequestor(ctx.plainContextString, fallbackServiceEndpoints, fdv1Endpoints.polling(), ctx.requests, ctx.encoding, ctx.baseHeaders, ctx.queryParams, config.withReasons, config.useReport);
|
|
4823
|
+
const fdv1SyncFactory = () => createFDv1PollingSynchronizer(fdv1RequestorFactory(), fallbackPollIntervalMs, logger);
|
|
4824
|
+
synchronizerSlots.push(createSynchronizerSlot(fdv1SyncFactory, { isFDv1Fallback: true }));
|
|
4825
|
+
}
|
|
4826
|
+
return { initializerFactories, synchronizerSlots };
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* The data callback shared across all FDv2DataSource instances for
|
|
4830
|
+
* the current identify. Handles selector tracking and flag updates.
|
|
4831
|
+
*/
|
|
4832
|
+
function dataCallback(payload) {
|
|
4833
|
+
logger.debug(`${logTag} dataCallback: type=${payload.type}, updates=${payload.updates.length}, state=${payload.state}`);
|
|
4834
|
+
selector = payload.state;
|
|
4835
|
+
const context = identifiedContext;
|
|
4836
|
+
if (!context) {
|
|
4837
|
+
logger.warn(`${logTag} dataCallback called without an identified context.`);
|
|
4838
|
+
return;
|
|
4839
|
+
}
|
|
4840
|
+
const descriptors = flagEvalPayloadToItemDescriptors(payload.updates ?? []);
|
|
4841
|
+
// Flag updates and change events happen synchronously inside applyChanges.
|
|
4842
|
+
// The returned promise is only for async cache persistence — we intentionally
|
|
4843
|
+
// do not await it so the data source pipeline is not blocked by storage I/O.
|
|
4844
|
+
flagManager.applyChanges(context, descriptors, payload.type).catch((e) => {
|
|
4845
|
+
logger.warn(`${logTag} Failed to persist flag cache: ${e}`);
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
/**
|
|
4849
|
+
* Create and start a new FDv2DataSource for the given mode.
|
|
4850
|
+
*
|
|
4851
|
+
* @param mode The connection mode to use.
|
|
4852
|
+
* @param includeInitializers Whether to include initializers (true on
|
|
4853
|
+
* first identify, false on mode switch after initialization).
|
|
4854
|
+
*/
|
|
4855
|
+
function createAndStartDataSource(mode, includeInitializers) {
|
|
4856
|
+
if (!factoryContext) {
|
|
4857
|
+
logger.warn(`${logTag} Cannot create data source without factory context.`);
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
const modeDef = getModeDefinition(mode);
|
|
4861
|
+
const { initializerFactories, synchronizerSlots } = buildFactories(modeDef, factoryContext, includeInitializers);
|
|
4862
|
+
currentResolvedMode = mode;
|
|
4863
|
+
// If there are no sources at all (e.g., offline or one-shot mode
|
|
4864
|
+
// post-initialization), don't create a data source.
|
|
4865
|
+
if (initializerFactories.length === 0 && synchronizerSlots.length === 0) {
|
|
4866
|
+
logger.debug(`${logTag} Mode '${mode}' has no sources. No data source created.`);
|
|
4867
|
+
if (!initialized && pendingIdentifyResolve) {
|
|
4868
|
+
// Offline mode during initial identify — resolve immediately.
|
|
4869
|
+
// The SDK will use cached data if any.
|
|
4870
|
+
initialized = true;
|
|
4871
|
+
pendingIdentifyResolve();
|
|
4872
|
+
pendingIdentifyResolve = undefined;
|
|
4873
|
+
pendingIdentifyReject = undefined;
|
|
4874
|
+
}
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4877
|
+
const selectorGetter = () => selector;
|
|
4878
|
+
dataSource = createFDv2DataSource({
|
|
4879
|
+
initializerFactories,
|
|
4880
|
+
synchronizerSlots,
|
|
4881
|
+
dataCallback,
|
|
4882
|
+
statusManager,
|
|
4883
|
+
selectorGetter,
|
|
4884
|
+
logger,
|
|
4885
|
+
fallbackTimeoutMs,
|
|
4886
|
+
recoveryTimeoutMs,
|
|
4887
|
+
});
|
|
4888
|
+
dataSource
|
|
4889
|
+
.start()
|
|
4890
|
+
.then(() => {
|
|
4891
|
+
initialized = true;
|
|
4892
|
+
if (pendingIdentifyResolve) {
|
|
4893
|
+
pendingIdentifyResolve();
|
|
4894
|
+
pendingIdentifyResolve = undefined;
|
|
4895
|
+
pendingIdentifyReject = undefined;
|
|
4896
|
+
}
|
|
4897
|
+
})
|
|
4898
|
+
.catch((err) => {
|
|
4899
|
+
if (pendingIdentifyReject) {
|
|
4900
|
+
pendingIdentifyReject(err instanceof Error ? err : new Error(String(err)));
|
|
4901
|
+
pendingIdentifyResolve = undefined;
|
|
4902
|
+
pendingIdentifyReject = undefined;
|
|
4903
|
+
}
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Reconciliation callback invoked when the debounce timer fires.
|
|
4908
|
+
* Resolves the new mode and switches data sources if needed.
|
|
4909
|
+
*/
|
|
4910
|
+
function onReconcile(pendingState) {
|
|
4911
|
+
if (closed || !factoryContext) {
|
|
4912
|
+
return;
|
|
4913
|
+
}
|
|
4914
|
+
// Update local state from the debounced pending state.
|
|
4915
|
+
networkState = pendingState.networkState;
|
|
4916
|
+
lifecycleState = pendingState.lifecycleState;
|
|
4917
|
+
foregroundMode = pendingState.requestedMode;
|
|
4918
|
+
const newMode = resolveMode();
|
|
4919
|
+
if (newMode === currentResolvedMode) {
|
|
4920
|
+
logger.debug(`${logTag} Reconcile: mode unchanged (${newMode}). No action.`);
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
logger.debug(`${logTag} Reconcile: mode switching from '${currentResolvedMode}' to '${newMode}'.`);
|
|
4924
|
+
// Close the current data source.
|
|
4925
|
+
dataSource?.close();
|
|
4926
|
+
dataSource = undefined;
|
|
4927
|
+
// Include initializers if we don't have a selector yet. This covers:
|
|
4928
|
+
// - Not yet initialized (normal case)
|
|
4929
|
+
// - Initialized from bootstrap (no selector) — need initializers to
|
|
4930
|
+
// get a full payload via poll before starting synchronizers
|
|
4931
|
+
// When we have a selector, only synchronizers change (spec 5.3.8).
|
|
4932
|
+
const includeInitializers = !selector;
|
|
4933
|
+
createAndStartDataSource(newMode, includeInitializers);
|
|
4934
|
+
}
|
|
4935
|
+
// --- Public interface ---
|
|
4936
|
+
return {
|
|
4937
|
+
get configuredForegroundMode() {
|
|
4938
|
+
return configuredForegroundMode;
|
|
4939
|
+
},
|
|
4940
|
+
async identify(identifyResolve, identifyReject, context, identifyOptions) {
|
|
4941
|
+
if (closed) {
|
|
4942
|
+
logger.debug(`${logTag} Identify called after close.`);
|
|
4943
|
+
return;
|
|
4944
|
+
}
|
|
4945
|
+
// Tear down previous state.
|
|
4946
|
+
dataSource?.close();
|
|
4947
|
+
dataSource = undefined;
|
|
4948
|
+
debounceManager?.close();
|
|
4949
|
+
debounceManager = undefined;
|
|
4950
|
+
selector = undefined;
|
|
4951
|
+
initialized = false;
|
|
4952
|
+
bootstrapped = false;
|
|
4953
|
+
identifiedContext = context;
|
|
4954
|
+
pendingIdentifyResolve = identifyResolve;
|
|
4955
|
+
pendingIdentifyReject = identifyReject;
|
|
4956
|
+
const plainContextString = JSON.stringify(Context.toLDContext(context));
|
|
4957
|
+
const queryParams = buildQueryParams(identifyOptions);
|
|
4958
|
+
if (config.withReasons) {
|
|
4959
|
+
queryParams.push({ key: 'withReasons', value: 'true' });
|
|
4960
|
+
}
|
|
4961
|
+
const streamingEndpoints = endpoints.streaming();
|
|
4962
|
+
const pollingEndpoints = endpoints.polling();
|
|
4963
|
+
const requestor = makeFDv2Requestor(plainContextString, config.serviceEndpoints, pollingEndpoints, platform.requests, platform.encoding, baseHeaders, queryParams);
|
|
4964
|
+
const environmentNamespace = await namespaceForEnvironment(platform.crypto, baseConfig.credential);
|
|
4965
|
+
// Re-check after the await — close() may have been called while
|
|
4966
|
+
// namespaceForEnvironment was pending.
|
|
4967
|
+
if (closed) {
|
|
4968
|
+
logger.debug(`${logTag} Identify aborted: closed during async setup.`);
|
|
4969
|
+
return;
|
|
4970
|
+
}
|
|
4971
|
+
factoryContext = {
|
|
4972
|
+
requestor,
|
|
4973
|
+
requests: platform.requests,
|
|
4974
|
+
encoding: platform.encoding,
|
|
4975
|
+
serviceEndpoints: config.serviceEndpoints,
|
|
4976
|
+
baseHeaders,
|
|
4977
|
+
queryParams,
|
|
4978
|
+
plainContextString,
|
|
4979
|
+
logger,
|
|
4980
|
+
polling: {
|
|
4981
|
+
paths: pollingEndpoints,
|
|
4982
|
+
intervalSeconds: config.pollInterval,
|
|
4983
|
+
},
|
|
4984
|
+
streaming: {
|
|
4985
|
+
paths: streamingEndpoints,
|
|
4986
|
+
initialReconnectDelaySeconds: config.streamInitialReconnectDelay,
|
|
4987
|
+
},
|
|
4988
|
+
storage: platform.storage,
|
|
4989
|
+
crypto: platform.crypto,
|
|
4990
|
+
environmentNamespace,
|
|
4991
|
+
context,
|
|
4992
|
+
};
|
|
4993
|
+
// Ensure foreground mode reflects current streaming state before resolving.
|
|
4994
|
+
foregroundMode = resolveStreamingForeground();
|
|
4995
|
+
// Resolve the initial mode.
|
|
4996
|
+
const mode = resolveMode();
|
|
4997
|
+
logger.debug(`${logTag} Identify: initial mode resolved to '${mode}'.`);
|
|
4998
|
+
bootstrapped = identifyOptions?.bootstrap !== undefined;
|
|
4999
|
+
if (bootstrapped) {
|
|
5000
|
+
// Bootstrap data was already applied to the flag store by the
|
|
5001
|
+
// caller (BrowserClient.start → presetFlags) before identify
|
|
5002
|
+
// was called. Resolve immediately — flag evaluations will use
|
|
5003
|
+
// the bootstrap data synchronously.
|
|
5004
|
+
initialized = true;
|
|
5005
|
+
statusManager.requestStateUpdate('VALID');
|
|
5006
|
+
// selector remains undefined — bootstrap data has no selector.
|
|
5007
|
+
pendingIdentifyResolve?.();
|
|
5008
|
+
pendingIdentifyResolve = undefined;
|
|
5009
|
+
pendingIdentifyReject = undefined;
|
|
5010
|
+
// Only create a data source if the mode has synchronizers.
|
|
5011
|
+
// For one-shot (no synchronizers), there's nothing more to do.
|
|
5012
|
+
const modeDef = getModeDefinition(mode);
|
|
5013
|
+
if (modeDef.synchronizers.length > 0) {
|
|
5014
|
+
// Start synchronizers without initializers — we already have
|
|
5015
|
+
// data from bootstrap. Initializers will run on mode switches
|
|
5016
|
+
// if selector is still undefined (see onReconcile).
|
|
5017
|
+
createAndStartDataSource(mode, false);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
else {
|
|
5021
|
+
// Normal identify — create and start the data source with full pipeline.
|
|
5022
|
+
createAndStartDataSource(mode, true);
|
|
5023
|
+
}
|
|
5024
|
+
// Set up debouncing for subsequent state changes.
|
|
5025
|
+
debounceManager = createStateDebounceManager({
|
|
5026
|
+
initialState: {
|
|
5027
|
+
networkState,
|
|
5028
|
+
lifecycleState,
|
|
5029
|
+
requestedMode: foregroundMode,
|
|
5030
|
+
},
|
|
5031
|
+
onReconcile,
|
|
5032
|
+
});
|
|
5033
|
+
},
|
|
5034
|
+
close() {
|
|
5035
|
+
closed = true;
|
|
5036
|
+
dataSource?.close();
|
|
5037
|
+
dataSource = undefined;
|
|
5038
|
+
debounceManager?.close();
|
|
5039
|
+
debounceManager = undefined;
|
|
5040
|
+
pendingIdentifyResolve = undefined;
|
|
5041
|
+
pendingIdentifyReject = undefined;
|
|
5042
|
+
},
|
|
5043
|
+
setNetworkState(state) {
|
|
5044
|
+
networkState = state;
|
|
5045
|
+
debounceManager?.setNetworkState(state);
|
|
5046
|
+
},
|
|
5047
|
+
setLifecycleState(state) {
|
|
5048
|
+
// Flush immediately when going to background — the app may be
|
|
5049
|
+
// about to close. This is not debounced (CONNMODE spec 3.3.1).
|
|
5050
|
+
if (state === 'background' && lifecycleState !== 'background') {
|
|
5051
|
+
flushCallback?.();
|
|
5052
|
+
}
|
|
5053
|
+
lifecycleState = state;
|
|
5054
|
+
debounceManager?.setLifecycleState(state);
|
|
5055
|
+
},
|
|
5056
|
+
setConnectionMode(mode) {
|
|
5057
|
+
connectionModeOverride = mode;
|
|
5058
|
+
if (mode !== undefined) {
|
|
5059
|
+
debounceManager?.setRequestedMode(mode);
|
|
5060
|
+
}
|
|
5061
|
+
else {
|
|
5062
|
+
pushForegroundMode();
|
|
5063
|
+
}
|
|
5064
|
+
},
|
|
5065
|
+
getCurrentMode() {
|
|
5066
|
+
return currentResolvedMode;
|
|
5067
|
+
},
|
|
5068
|
+
setFlushCallback(callback) {
|
|
5069
|
+
flushCallback = callback;
|
|
5070
|
+
},
|
|
5071
|
+
setForcedStreaming(streaming) {
|
|
5072
|
+
forcedStreaming = streaming;
|
|
5073
|
+
pushForegroundMode();
|
|
5074
|
+
},
|
|
5075
|
+
setAutomaticStreamingState(streaming) {
|
|
5076
|
+
automaticStreamingState = streaming;
|
|
5077
|
+
pushForegroundMode();
|
|
5078
|
+
},
|
|
5079
|
+
};
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
export { BROWSER_DATA_SYSTEM_DEFAULTS, BROWSER_TRANSITION_TABLE, BaseDataManager, DESKTOP_DATA_SYSTEM_DEFAULTS, DESKTOP_TRANSITION_TABLE, DataSourceState, LDClientImpl, MOBILE_DATA_SYSTEM_DEFAULTS, MOBILE_TRANSITION_TABLE, MODE_TABLE, browserFdv1Endpoints, createDataSourceStatusManager, createDefaultSourceFactoryProvider, createFDv2DataManagerBase, createStateDebounceManager, dataSystemValidators, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, resolveConnectionMode, resolveForegroundMode, safeRegisterDebugOverridePlugins, validateOptions };
|
|
3954
5083
|
//# sourceMappingURL=index.mjs.map
|