@launchdarkly/js-client-sdk-common 1.21.0 → 1.22.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 +20 -0
- 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/ModeResolution.d.ts +75 -0
- package/dist/cjs/api/datasource/ModeResolution.d.ts.map +1 -0
- package/dist/cjs/api/datasource/index.d.ts +1 -0
- 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/ModeResolver.d.ts +30 -0
- package/dist/cjs/datasource/ModeResolver.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/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 +2 -1
- package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/cjs/flag-manager/FlagPersistence.d.ts +8 -1
- package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/cjs/index.cjs +336 -60
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +3 -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/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/ModeResolution.d.ts +75 -0
- package/dist/esm/api/datasource/ModeResolution.d.ts.map +1 -0
- package/dist/esm/api/datasource/index.d.ts +1 -0
- 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/ModeResolver.d.ts +30 -0
- package/dist/esm/datasource/ModeResolver.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/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 +2 -1
- package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/esm/flag-manager/FlagPersistence.d.ts +8 -1
- package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.mjs +329 -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/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -289,35 +289,141 @@ 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 that tries each provided validator in order and uses the
|
|
315
|
+
* first one whose `is()` check passes. For compound validators the value is
|
|
316
|
+
* processed through `validate()`; for simple validators the value is accepted
|
|
317
|
+
* as-is. If no validator matches, a warning is logged and the default is
|
|
318
|
+
* preserved.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* // Accepts either a boolean or a nested object with specific fields:
|
|
323
|
+
* anyOf(TypeValidators.Boolean, validatorOf({ lifecycle: TypeValidators.Boolean }))
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
function anyOf(...validators) {
|
|
327
|
+
return {
|
|
328
|
+
is: (u) => validators.some((v) => v.is(u)),
|
|
329
|
+
getType: () => validators.map((v) => v.getType()).join(' | '),
|
|
330
|
+
validate(value, name, logger, defaults) {
|
|
331
|
+
const match = validators.find((v) => v.is(value));
|
|
332
|
+
if (match) {
|
|
333
|
+
return isCompoundValidator(match)
|
|
334
|
+
? match.validate(value, name, logger, defaults)
|
|
335
|
+
: { value };
|
|
336
|
+
}
|
|
337
|
+
logger?.warn(jsSdkCommon.OptionMessages.wrongOptionType(name, this.getType(), typeof value));
|
|
338
|
+
return undefined;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
292
342
|
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
diagnosticRecordingInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
302
|
-
flushInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
303
|
-
streamInitialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(0),
|
|
304
|
-
allAttributesPrivate: jsSdkCommon.TypeValidators.Boolean,
|
|
305
|
-
debug: jsSdkCommon.TypeValidators.Boolean,
|
|
306
|
-
diagnosticOptOut: jsSdkCommon.TypeValidators.Boolean,
|
|
307
|
-
withReasons: jsSdkCommon.TypeValidators.Boolean,
|
|
308
|
-
sendEvents: jsSdkCommon.TypeValidators.Boolean,
|
|
343
|
+
const dataSourceTypeValidator = jsSdkCommon.TypeValidators.oneOf('cache', 'polling', 'streaming');
|
|
344
|
+
const connectionModeValidator = jsSdkCommon.TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
|
|
345
|
+
const endpointValidators = {
|
|
346
|
+
pollingBaseUri: jsSdkCommon.TypeValidators.String,
|
|
347
|
+
streamingBaseUri: jsSdkCommon.TypeValidators.String,
|
|
348
|
+
};
|
|
349
|
+
({
|
|
350
|
+
type: dataSourceTypeValidator,
|
|
309
351
|
pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
352
|
+
endpoints: validatorOf(endpointValidators),
|
|
353
|
+
});
|
|
354
|
+
({
|
|
355
|
+
type: dataSourceTypeValidator,
|
|
356
|
+
initialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(1),
|
|
357
|
+
endpoints: validatorOf(endpointValidators),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const modeSwitchingValidators = {
|
|
361
|
+
lifecycle: jsSdkCommon.TypeValidators.Boolean,
|
|
362
|
+
network: jsSdkCommon.TypeValidators.Boolean,
|
|
363
|
+
};
|
|
364
|
+
const dataSystemValidators = {
|
|
365
|
+
initialConnectionMode: connectionModeValidator,
|
|
366
|
+
backgroundConnectionMode: connectionModeValidator,
|
|
367
|
+
automaticModeSwitching: anyOf(jsSdkCommon.TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Default FDv2 data system configuration for browser SDKs.
|
|
371
|
+
*/
|
|
372
|
+
const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
373
|
+
initialConnectionMode: 'one-shot',
|
|
374
|
+
backgroundConnectionMode: undefined,
|
|
375
|
+
automaticModeSwitching: false,
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Default FDv2 data system configuration for mobile (React Native) SDKs.
|
|
379
|
+
*/
|
|
380
|
+
const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
381
|
+
initialConnectionMode: 'streaming',
|
|
382
|
+
backgroundConnectionMode: 'background',
|
|
383
|
+
automaticModeSwitching: true,
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
|
|
387
|
+
*/
|
|
388
|
+
const DESKTOP_DATA_SYSTEM_DEFAULTS = {
|
|
389
|
+
initialConnectionMode: 'streaming',
|
|
390
|
+
backgroundConnectionMode: undefined,
|
|
391
|
+
automaticModeSwitching: false,
|
|
319
392
|
};
|
|
320
393
|
|
|
394
|
+
function createValidators(options) {
|
|
395
|
+
return {
|
|
396
|
+
logger: jsSdkCommon.TypeValidators.Object,
|
|
397
|
+
maxCachedContexts: jsSdkCommon.TypeValidators.numberWithMin(0),
|
|
398
|
+
baseUri: jsSdkCommon.TypeValidators.String,
|
|
399
|
+
streamUri: jsSdkCommon.TypeValidators.String,
|
|
400
|
+
eventsUri: jsSdkCommon.TypeValidators.String,
|
|
401
|
+
capacity: jsSdkCommon.TypeValidators.numberWithMin(1),
|
|
402
|
+
diagnosticRecordingInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
403
|
+
flushInterval: jsSdkCommon.TypeValidators.numberWithMin(2),
|
|
404
|
+
streamInitialReconnectDelay: jsSdkCommon.TypeValidators.numberWithMin(0),
|
|
405
|
+
allAttributesPrivate: jsSdkCommon.TypeValidators.Boolean,
|
|
406
|
+
debug: jsSdkCommon.TypeValidators.Boolean,
|
|
407
|
+
diagnosticOptOut: jsSdkCommon.TypeValidators.Boolean,
|
|
408
|
+
withReasons: jsSdkCommon.TypeValidators.Boolean,
|
|
409
|
+
sendEvents: jsSdkCommon.TypeValidators.Boolean,
|
|
410
|
+
pollInterval: jsSdkCommon.TypeValidators.numberWithMin(30),
|
|
411
|
+
useReport: jsSdkCommon.TypeValidators.Boolean,
|
|
412
|
+
privateAttributes: jsSdkCommon.TypeValidators.StringArray,
|
|
413
|
+
disableCache: jsSdkCommon.TypeValidators.Boolean,
|
|
414
|
+
applicationInfo: jsSdkCommon.TypeValidators.Object,
|
|
415
|
+
wrapperName: jsSdkCommon.TypeValidators.String,
|
|
416
|
+
wrapperVersion: jsSdkCommon.TypeValidators.String,
|
|
417
|
+
payloadFilterKey: jsSdkCommon.TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
|
|
418
|
+
hooks: jsSdkCommon.TypeValidators.createTypeArray('Hook[]', {}),
|
|
419
|
+
inspectors: jsSdkCommon.TypeValidators.createTypeArray('LDInspection', {}),
|
|
420
|
+
cleanOldPersistentData: jsSdkCommon.TypeValidators.Boolean,
|
|
421
|
+
dataSystem: options?.dataSystemDefaults
|
|
422
|
+
? validatorOf(dataSystemValidators, options.dataSystemDefaults)
|
|
423
|
+
: jsSdkCommon.TypeValidators.Object,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
321
427
|
const DEFAULT_POLLING_INTERVAL = 60 * 5;
|
|
322
428
|
const DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com';
|
|
323
429
|
const DEFAULT_STREAM = 'https://clientstream.launchdarkly.com';
|
|
@@ -343,6 +449,7 @@ class ConfigurationImpl {
|
|
|
343
449
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
344
450
|
this.streamUri = DEFAULT_STREAM;
|
|
345
451
|
this.maxCachedContexts = 5;
|
|
452
|
+
this.disableCache = false;
|
|
346
453
|
this.capacity = 100;
|
|
347
454
|
this.diagnosticRecordingInterval = 900;
|
|
348
455
|
this.flushInterval = 30;
|
|
@@ -359,6 +466,9 @@ class ConfigurationImpl {
|
|
|
359
466
|
this.hooks = [];
|
|
360
467
|
this.inspectors = [];
|
|
361
468
|
this.logger = ensureSafeLogger(pristineOptions.logger);
|
|
469
|
+
const validators = createValidators({
|
|
470
|
+
dataSystemDefaults: internalOptions.dataSystemDefaults,
|
|
471
|
+
});
|
|
362
472
|
const validated = validateOptions(pristineOptions, validators, {}, this.logger);
|
|
363
473
|
Object.entries(validated).forEach(([k, v]) => {
|
|
364
474
|
if (k !== 'logger') {
|
|
@@ -711,6 +821,71 @@ class EventFactory extends jsSdkCommon.internal.EventFactoryBase {
|
|
|
711
821
|
}
|
|
712
822
|
}
|
|
713
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Suffix appended to context storage keys to form the freshness storage key.
|
|
826
|
+
*/
|
|
827
|
+
const FRESHNESS_SUFFIX = '_freshness';
|
|
828
|
+
/**
|
|
829
|
+
* Computes a SHA-256 hash of the context's full canonical JSON.
|
|
830
|
+
* Returns `undefined` if the context cannot be serialized.
|
|
831
|
+
*/
|
|
832
|
+
async function hashContext(crypto, context) {
|
|
833
|
+
const json = context.canonicalUnfilteredJson();
|
|
834
|
+
if (!json) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
return digest(crypto.createHash('sha256').update(json), 'base64');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function isValidFlag(value) {
|
|
841
|
+
return value !== null && typeof value === 'object' && typeof value.version === 'number';
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Loads cached flag data from storage for the given context.
|
|
845
|
+
*
|
|
846
|
+
* Checks the current storage key first, then falls back to the legacy
|
|
847
|
+
* canonical key location (pre-10.3.1). Does NOT perform migration — the
|
|
848
|
+
* caller is responsible for migrating data if {@link CachedFlagData.fromLegacyKey}
|
|
849
|
+
* is true.
|
|
850
|
+
*
|
|
851
|
+
* @returns The cached flag data, or `undefined` on cache miss or parse error.
|
|
852
|
+
*/
|
|
853
|
+
async function loadCachedFlags(storage, crypto, environmentNamespace, context, logger) {
|
|
854
|
+
const storageKey = await namespaceForContextData(crypto, environmentNamespace, context);
|
|
855
|
+
let flagsJson = await storage.get(storageKey);
|
|
856
|
+
let fromLegacyKey = false;
|
|
857
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
858
|
+
// Fallback: in version <10.3.1 flag data was stored under the canonical key.
|
|
859
|
+
flagsJson = await storage.get(context.canonicalKey);
|
|
860
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
861
|
+
return undefined;
|
|
862
|
+
}
|
|
863
|
+
fromLegacyKey = true;
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const parsed = JSON.parse(flagsJson);
|
|
867
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
868
|
+
logger?.warn('Cached flag data is not a valid object');
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
const entries = Object.entries(parsed);
|
|
872
|
+
const invalidKey = entries.find(([, value]) => !isValidFlag(value));
|
|
873
|
+
if (invalidKey) {
|
|
874
|
+
logger?.warn(`Discarding cached flags due to invalid entry: ${invalidKey[0]}`);
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
const flags = entries.reduce((acc, [key, value]) => {
|
|
878
|
+
acc[key] = value;
|
|
879
|
+
return acc;
|
|
880
|
+
}, {});
|
|
881
|
+
return { flags, storageKey, fromLegacyKey };
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
logger?.warn(`Could not parse cached flag evaluations from persistent storage: ${e.message}`);
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
714
889
|
/**
|
|
715
890
|
* An index for tracking the most recently used contexts by timestamp with the ability to
|
|
716
891
|
* update entry timestamps and prune out least used contexts above a max capacity provided.
|
|
@@ -776,12 +951,18 @@ class ContextIndex {
|
|
|
776
951
|
* This class handles persisting and loading flag values from a persistent
|
|
777
952
|
* store. It intercepts updates and forwards them to the flag updater and
|
|
778
953
|
* then persists changes after the updater has completed.
|
|
954
|
+
*
|
|
955
|
+
* Freshness metadata (timestamp + context attribute hash) is stored in a
|
|
956
|
+
* separate storage key (`{contextKey}_freshness`) alongside the flag data.
|
|
957
|
+
* Both keys are managed together — when a context is evicted, both the flag
|
|
958
|
+
* data and freshness record are cleared.
|
|
779
959
|
*/
|
|
780
960
|
class FlagPersistence {
|
|
781
|
-
constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
961
|
+
constructor(_platform, _environmentNamespace, _maxCachedContexts, _disableCache, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
782
962
|
this._platform = _platform;
|
|
783
963
|
this._environmentNamespace = _environmentNamespace;
|
|
784
964
|
this._maxCachedContexts = _maxCachedContexts;
|
|
965
|
+
this._disableCache = _disableCache;
|
|
785
966
|
this._flagStore = _flagStore;
|
|
786
967
|
this._flagUpdater = _flagUpdater;
|
|
787
968
|
this._logger = _logger;
|
|
@@ -813,35 +994,38 @@ class FlagPersistence {
|
|
|
813
994
|
* {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
|
|
814
995
|
*/
|
|
815
996
|
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);
|
|
997
|
+
if (this._disableCache || this._maxCachedContexts <= 0) {
|
|
998
|
+
return false;
|
|
829
999
|
}
|
|
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;
|
|
1000
|
+
if (!this._platform.storage) {
|
|
1001
|
+
return false;
|
|
840
1002
|
}
|
|
841
|
-
|
|
842
|
-
|
|
1003
|
+
const cached = await loadCachedFlags(this._platform.storage, this._platform.crypto, this._environmentNamespace, context, this._logger);
|
|
1004
|
+
if (!cached) {
|
|
843
1005
|
return false;
|
|
844
1006
|
}
|
|
1007
|
+
// Migrate data from version <10.3.1 stored under the canonical key
|
|
1008
|
+
if (cached.fromLegacyKey) {
|
|
1009
|
+
await this._platform.storage.set(cached.storageKey, JSON.stringify(cached.flags));
|
|
1010
|
+
await this._platform.storage.clear(context.canonicalKey);
|
|
1011
|
+
}
|
|
1012
|
+
// mapping flags to item descriptors
|
|
1013
|
+
const descriptors = Object.entries(cached.flags).reduce((acc, [key, flag]) => {
|
|
1014
|
+
acc[key] = { version: flag.version, flag };
|
|
1015
|
+
return acc;
|
|
1016
|
+
}, {});
|
|
1017
|
+
this._flagUpdater.initCached(context, descriptors);
|
|
1018
|
+
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
async _storeFreshness(contextStorageKey, context, timestamp) {
|
|
1022
|
+
const contextHash = await hashContext(this._platform.crypto, context);
|
|
1023
|
+
if (contextHash === undefined) {
|
|
1024
|
+
this._logger.error('Could not serialize context for freshness tracking');
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const record = { timestamp, contextHash };
|
|
1028
|
+
await this._platform.storage?.set(`${contextStorageKey}${FRESHNESS_SUFFIX}`, JSON.stringify(record));
|
|
845
1029
|
}
|
|
846
1030
|
async _loadIndex() {
|
|
847
1031
|
if (this._contextIndex !== undefined) {
|
|
@@ -863,13 +1047,26 @@ class FlagPersistence {
|
|
|
863
1047
|
return this._contextIndex;
|
|
864
1048
|
}
|
|
865
1049
|
async _storeCache(context) {
|
|
1050
|
+
if (this._disableCache) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const now = this._timeStamper();
|
|
866
1054
|
const index = await this._loadIndex();
|
|
867
1055
|
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
868
|
-
|
|
1056
|
+
if (this._maxCachedContexts > 0) {
|
|
1057
|
+
index.notice(storageKey, now);
|
|
1058
|
+
}
|
|
869
1059
|
const pruned = index.prune(this._maxCachedContexts);
|
|
870
|
-
|
|
871
|
-
|
|
1060
|
+
// If maxCachedContexts <= 0, current context was never added, so always skip flag write
|
|
1061
|
+
const currentContextWasPruned = this._maxCachedContexts <= 0 || pruned.some((it) => it.id === storageKey);
|
|
1062
|
+
await Promise.all(pruned.flatMap((it) => [
|
|
1063
|
+
this._platform.storage?.clear(it.id),
|
|
1064
|
+
this._platform.storage?.clear(`${it.id}${FRESHNESS_SUFFIX}`),
|
|
1065
|
+
]));
|
|
872
1066
|
await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
|
|
1067
|
+
if (currentContextWasPruned) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
873
1070
|
const allFlags = this._flagStore.getAll();
|
|
874
1071
|
// mapping item descriptors to flags
|
|
875
1072
|
const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
|
|
@@ -879,8 +1076,15 @@ class FlagPersistence {
|
|
|
879
1076
|
return acc;
|
|
880
1077
|
}, {});
|
|
881
1078
|
const jsonAll = JSON.stringify(flags);
|
|
882
|
-
// store flag data
|
|
1079
|
+
// store flag data first, so freshness is never newer than the flags it describes
|
|
883
1080
|
await this._platform.storage?.set(storageKey, jsonAll);
|
|
1081
|
+
// store freshness — best-effort, must not block flag persistence
|
|
1082
|
+
try {
|
|
1083
|
+
await this._storeFreshness(storageKey, context, now);
|
|
1084
|
+
}
|
|
1085
|
+
catch (e) {
|
|
1086
|
+
this._logger.warn(`Failed to store freshness data: ${e.message}`);
|
|
1087
|
+
}
|
|
884
1088
|
}
|
|
885
1089
|
}
|
|
886
1090
|
|
|
@@ -996,17 +1200,18 @@ class DefaultFlagManager {
|
|
|
996
1200
|
* @param platform implementation of various platform provided functionality
|
|
997
1201
|
* @param sdkKey that will be used to distinguish different environments
|
|
998
1202
|
* @param maxCachedContexts that specifies the max number of contexts that will be cached in persistence
|
|
1203
|
+
* @param disableCache set to true to completely disable the persistent flag cache
|
|
999
1204
|
* @param logger used for logging various messages
|
|
1000
1205
|
* @param timeStamper exists for testing purposes
|
|
1001
1206
|
*/
|
|
1002
|
-
constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1207
|
+
constructor(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
1003
1208
|
this._flagStore = createDefaultFlagStore();
|
|
1004
1209
|
this._flagUpdater = createFlagUpdater(this._flagStore, logger);
|
|
1005
|
-
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
1210
|
+
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper);
|
|
1006
1211
|
}
|
|
1007
|
-
async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1212
|
+
async _initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
1008
1213
|
const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
|
|
1009
|
-
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
1214
|
+
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, disableCache, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
1010
1215
|
}
|
|
1011
1216
|
get(key) {
|
|
1012
1217
|
if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
|
|
@@ -1482,7 +1687,7 @@ class LDClientImpl {
|
|
|
1482
1687
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1483
1688
|
this.logger = this._config.logger;
|
|
1484
1689
|
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);
|
|
1690
|
+
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1486
1691
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
1487
1692
|
this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
|
|
1488
1693
|
this.emitter = new LDEmitter();
|
|
@@ -2576,15 +2781,86 @@ class BaseDataManager {
|
|
|
2576
2781
|
}
|
|
2577
2782
|
}
|
|
2578
2783
|
|
|
2784
|
+
function allConditionsMatch(conditions, input) {
|
|
2785
|
+
return Object.entries(conditions).every(([key, value]) => value === undefined || input[key] === value);
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Given a mode resolution table and the current input state, returns the
|
|
2789
|
+
* resolved FDv2 connection mode.
|
|
2790
|
+
*
|
|
2791
|
+
* Iterates entries in order. The first entry whose conditions all match the
|
|
2792
|
+
* input wins. If no entry matches (should not happen when the table ends with
|
|
2793
|
+
* a catch-all), falls back to `input.foregroundMode`.
|
|
2794
|
+
*/
|
|
2795
|
+
const CONFIGURED_MODE_MAP = {
|
|
2796
|
+
foreground: 'foregroundMode',
|
|
2797
|
+
background: 'backgroundMode',
|
|
2798
|
+
};
|
|
2799
|
+
function resolveConnectionMode(table, input) {
|
|
2800
|
+
const match = table.find((entry) => allConditionsMatch(entry.conditions, input));
|
|
2801
|
+
if (match) {
|
|
2802
|
+
const { mode } = match;
|
|
2803
|
+
if (typeof mode === 'object') {
|
|
2804
|
+
return input[CONFIGURED_MODE_MAP[mode.configured]];
|
|
2805
|
+
}
|
|
2806
|
+
return mode;
|
|
2807
|
+
}
|
|
2808
|
+
return input.foregroundMode;
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Mode resolution table for mobile platforms (React Native, etc.).
|
|
2812
|
+
*
|
|
2813
|
+
* - No network → offline.
|
|
2814
|
+
* - Background → configured background mode.
|
|
2815
|
+
* - Foreground → configured foreground mode.
|
|
2816
|
+
*/
|
|
2817
|
+
const MOBILE_TRANSITION_TABLE = [
|
|
2818
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2819
|
+
{ conditions: { lifecycle: 'background' }, mode: { configured: 'background' } },
|
|
2820
|
+
{ conditions: { lifecycle: 'foreground' }, mode: { configured: 'foreground' } },
|
|
2821
|
+
];
|
|
2822
|
+
/**
|
|
2823
|
+
* Mode resolution table for browser platforms.
|
|
2824
|
+
*
|
|
2825
|
+
* - No network → offline.
|
|
2826
|
+
* - Otherwise → configured foreground mode.
|
|
2827
|
+
*
|
|
2828
|
+
* Browser listener-driven streaming (auto-promotion to streaming when change
|
|
2829
|
+
* listeners are registered) is handled externally by the caller modifying
|
|
2830
|
+
* `foregroundMode` before consulting this table.
|
|
2831
|
+
*/
|
|
2832
|
+
const BROWSER_TRANSITION_TABLE = [
|
|
2833
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2834
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
2835
|
+
];
|
|
2836
|
+
/**
|
|
2837
|
+
* Mode resolution table for desktop platforms (Electron, etc.).
|
|
2838
|
+
*
|
|
2839
|
+
* - No network → offline.
|
|
2840
|
+
* - Otherwise → configured foreground mode.
|
|
2841
|
+
*/
|
|
2842
|
+
const DESKTOP_TRANSITION_TABLE = [
|
|
2843
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2844
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
2845
|
+
];
|
|
2846
|
+
|
|
2579
2847
|
exports.platform = jsSdkCommon__namespace;
|
|
2848
|
+
exports.BROWSER_DATA_SYSTEM_DEFAULTS = BROWSER_DATA_SYSTEM_DEFAULTS;
|
|
2849
|
+
exports.BROWSER_TRANSITION_TABLE = BROWSER_TRANSITION_TABLE;
|
|
2580
2850
|
exports.BaseDataManager = BaseDataManager;
|
|
2851
|
+
exports.DESKTOP_DATA_SYSTEM_DEFAULTS = DESKTOP_DATA_SYSTEM_DEFAULTS;
|
|
2852
|
+
exports.DESKTOP_TRANSITION_TABLE = DESKTOP_TRANSITION_TABLE;
|
|
2581
2853
|
exports.DataSourceState = DataSourceState;
|
|
2582
2854
|
exports.LDClientImpl = LDClientImpl;
|
|
2855
|
+
exports.MOBILE_DATA_SYSTEM_DEFAULTS = MOBILE_DATA_SYSTEM_DEFAULTS;
|
|
2856
|
+
exports.MOBILE_TRANSITION_TABLE = MOBILE_TRANSITION_TABLE;
|
|
2583
2857
|
exports.browserFdv1Endpoints = browserFdv1Endpoints;
|
|
2858
|
+
exports.dataSystemValidators = dataSystemValidators;
|
|
2584
2859
|
exports.fdv2Endpoints = fdv2Endpoints;
|
|
2585
2860
|
exports.makeRequestor = makeRequestor;
|
|
2586
2861
|
exports.mobileFdv1Endpoints = mobileFdv1Endpoints;
|
|
2587
2862
|
exports.readFlagsFromBootstrap = readFlagsFromBootstrap;
|
|
2863
|
+
exports.resolveConnectionMode = resolveConnectionMode;
|
|
2588
2864
|
exports.safeRegisterDebugOverridePlugins = safeRegisterDebugOverridePlugins;
|
|
2589
2865
|
exports.validateOptions = validateOptions;
|
|
2590
2866
|
Object.keys(jsSdkCommon).forEach(function (k) {
|