@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/esm/index.mjs
CHANGED
|
@@ -271,35 +271,141 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
|
|
|
271
271
|
});
|
|
272
272
|
return result;
|
|
273
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Creates a validator for nested objects. When used in a validator map,
|
|
276
|
+
* `validateOptions` will recursively validate the nested object's properties.
|
|
277
|
+
* Defaults for nested fields are passed through from the parent.
|
|
278
|
+
*/
|
|
279
|
+
function validatorOf(validators, builtInDefaults) {
|
|
280
|
+
return {
|
|
281
|
+
is: (u) => TypeValidators.Object.is(u),
|
|
282
|
+
getType: () => 'object',
|
|
283
|
+
validate(value, name, logger, defaults) {
|
|
284
|
+
if (!TypeValidators.Object.is(value)) {
|
|
285
|
+
logger?.warn(OptionMessages.wrongOptionType(name, 'object', typeof value));
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const nestedDefaults = builtInDefaults ??
|
|
289
|
+
(TypeValidators.Object.is(defaults) ? defaults : {});
|
|
290
|
+
const nested = validateOptions(value, validators, nestedDefaults, logger, name);
|
|
291
|
+
return Object.keys(nested).length > 0 ? { value: nested } : undefined;
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Creates a validator that tries each provided validator in order and uses the
|
|
297
|
+
* first one whose `is()` check passes. For compound validators the value is
|
|
298
|
+
* processed through `validate()`; for simple validators the value is accepted
|
|
299
|
+
* as-is. If no validator matches, a warning is logged and the default is
|
|
300
|
+
* preserved.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* // Accepts either a boolean or a nested object with specific fields:
|
|
305
|
+
* anyOf(TypeValidators.Boolean, validatorOf({ lifecycle: TypeValidators.Boolean }))
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
function anyOf(...validators) {
|
|
309
|
+
return {
|
|
310
|
+
is: (u) => validators.some((v) => v.is(u)),
|
|
311
|
+
getType: () => validators.map((v) => v.getType()).join(' | '),
|
|
312
|
+
validate(value, name, logger, defaults) {
|
|
313
|
+
const match = validators.find((v) => v.is(value));
|
|
314
|
+
if (match) {
|
|
315
|
+
return isCompoundValidator(match)
|
|
316
|
+
? match.validate(value, name, logger, defaults)
|
|
317
|
+
: { value };
|
|
318
|
+
}
|
|
319
|
+
logger?.warn(OptionMessages.wrongOptionType(name, this.getType(), typeof value));
|
|
320
|
+
return undefined;
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
274
324
|
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
diagnosticRecordingInterval: TypeValidators.numberWithMin(2),
|
|
284
|
-
flushInterval: TypeValidators.numberWithMin(2),
|
|
285
|
-
streamInitialReconnectDelay: TypeValidators.numberWithMin(0),
|
|
286
|
-
allAttributesPrivate: TypeValidators.Boolean,
|
|
287
|
-
debug: TypeValidators.Boolean,
|
|
288
|
-
diagnosticOptOut: TypeValidators.Boolean,
|
|
289
|
-
withReasons: TypeValidators.Boolean,
|
|
290
|
-
sendEvents: TypeValidators.Boolean,
|
|
325
|
+
const dataSourceTypeValidator = TypeValidators.oneOf('cache', 'polling', 'streaming');
|
|
326
|
+
const connectionModeValidator = TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
|
|
327
|
+
const endpointValidators = {
|
|
328
|
+
pollingBaseUri: TypeValidators.String,
|
|
329
|
+
streamingBaseUri: TypeValidators.String,
|
|
330
|
+
};
|
|
331
|
+
({
|
|
332
|
+
type: dataSourceTypeValidator,
|
|
291
333
|
pollInterval: TypeValidators.numberWithMin(30),
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
334
|
+
endpoints: validatorOf(endpointValidators),
|
|
335
|
+
});
|
|
336
|
+
({
|
|
337
|
+
type: dataSourceTypeValidator,
|
|
338
|
+
initialReconnectDelay: TypeValidators.numberWithMin(1),
|
|
339
|
+
endpoints: validatorOf(endpointValidators),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const modeSwitchingValidators = {
|
|
343
|
+
lifecycle: TypeValidators.Boolean,
|
|
344
|
+
network: TypeValidators.Boolean,
|
|
345
|
+
};
|
|
346
|
+
const dataSystemValidators = {
|
|
347
|
+
initialConnectionMode: connectionModeValidator,
|
|
348
|
+
backgroundConnectionMode: connectionModeValidator,
|
|
349
|
+
automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Default FDv2 data system configuration for browser SDKs.
|
|
353
|
+
*/
|
|
354
|
+
const BROWSER_DATA_SYSTEM_DEFAULTS = {
|
|
355
|
+
initialConnectionMode: 'one-shot',
|
|
356
|
+
backgroundConnectionMode: undefined,
|
|
357
|
+
automaticModeSwitching: false,
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Default FDv2 data system configuration for mobile (React Native) SDKs.
|
|
361
|
+
*/
|
|
362
|
+
const MOBILE_DATA_SYSTEM_DEFAULTS = {
|
|
363
|
+
initialConnectionMode: 'streaming',
|
|
364
|
+
backgroundConnectionMode: 'background',
|
|
365
|
+
automaticModeSwitching: true,
|
|
366
|
+
};
|
|
367
|
+
/**
|
|
368
|
+
* Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
|
|
369
|
+
*/
|
|
370
|
+
const DESKTOP_DATA_SYSTEM_DEFAULTS = {
|
|
371
|
+
initialConnectionMode: 'streaming',
|
|
372
|
+
backgroundConnectionMode: undefined,
|
|
373
|
+
automaticModeSwitching: false,
|
|
301
374
|
};
|
|
302
375
|
|
|
376
|
+
function createValidators(options) {
|
|
377
|
+
return {
|
|
378
|
+
logger: TypeValidators.Object,
|
|
379
|
+
maxCachedContexts: TypeValidators.numberWithMin(0),
|
|
380
|
+
baseUri: TypeValidators.String,
|
|
381
|
+
streamUri: TypeValidators.String,
|
|
382
|
+
eventsUri: TypeValidators.String,
|
|
383
|
+
capacity: TypeValidators.numberWithMin(1),
|
|
384
|
+
diagnosticRecordingInterval: TypeValidators.numberWithMin(2),
|
|
385
|
+
flushInterval: TypeValidators.numberWithMin(2),
|
|
386
|
+
streamInitialReconnectDelay: TypeValidators.numberWithMin(0),
|
|
387
|
+
allAttributesPrivate: TypeValidators.Boolean,
|
|
388
|
+
debug: TypeValidators.Boolean,
|
|
389
|
+
diagnosticOptOut: TypeValidators.Boolean,
|
|
390
|
+
withReasons: TypeValidators.Boolean,
|
|
391
|
+
sendEvents: TypeValidators.Boolean,
|
|
392
|
+
pollInterval: TypeValidators.numberWithMin(30),
|
|
393
|
+
useReport: TypeValidators.Boolean,
|
|
394
|
+
privateAttributes: TypeValidators.StringArray,
|
|
395
|
+
disableCache: TypeValidators.Boolean,
|
|
396
|
+
applicationInfo: TypeValidators.Object,
|
|
397
|
+
wrapperName: TypeValidators.String,
|
|
398
|
+
wrapperVersion: TypeValidators.String,
|
|
399
|
+
payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
|
|
400
|
+
hooks: TypeValidators.createTypeArray('Hook[]', {}),
|
|
401
|
+
inspectors: TypeValidators.createTypeArray('LDInspection', {}),
|
|
402
|
+
cleanOldPersistentData: TypeValidators.Boolean,
|
|
403
|
+
dataSystem: options?.dataSystemDefaults
|
|
404
|
+
? validatorOf(dataSystemValidators, options.dataSystemDefaults)
|
|
405
|
+
: TypeValidators.Object,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
303
409
|
const DEFAULT_POLLING_INTERVAL = 60 * 5;
|
|
304
410
|
const DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com';
|
|
305
411
|
const DEFAULT_STREAM = 'https://clientstream.launchdarkly.com';
|
|
@@ -325,6 +431,7 @@ class ConfigurationImpl {
|
|
|
325
431
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
326
432
|
this.streamUri = DEFAULT_STREAM;
|
|
327
433
|
this.maxCachedContexts = 5;
|
|
434
|
+
this.disableCache = false;
|
|
328
435
|
this.capacity = 100;
|
|
329
436
|
this.diagnosticRecordingInterval = 900;
|
|
330
437
|
this.flushInterval = 30;
|
|
@@ -341,6 +448,9 @@ class ConfigurationImpl {
|
|
|
341
448
|
this.hooks = [];
|
|
342
449
|
this.inspectors = [];
|
|
343
450
|
this.logger = ensureSafeLogger(pristineOptions.logger);
|
|
451
|
+
const validators = createValidators({
|
|
452
|
+
dataSystemDefaults: internalOptions.dataSystemDefaults,
|
|
453
|
+
});
|
|
344
454
|
const validated = validateOptions(pristineOptions, validators, {}, this.logger);
|
|
345
455
|
Object.entries(validated).forEach(([k, v]) => {
|
|
346
456
|
if (k !== 'logger') {
|
|
@@ -693,6 +803,71 @@ class EventFactory extends internal.EventFactoryBase {
|
|
|
693
803
|
}
|
|
694
804
|
}
|
|
695
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Suffix appended to context storage keys to form the freshness storage key.
|
|
808
|
+
*/
|
|
809
|
+
const FRESHNESS_SUFFIX = '_freshness';
|
|
810
|
+
/**
|
|
811
|
+
* Computes a SHA-256 hash of the context's full canonical JSON.
|
|
812
|
+
* Returns `undefined` if the context cannot be serialized.
|
|
813
|
+
*/
|
|
814
|
+
async function hashContext(crypto, context) {
|
|
815
|
+
const json = context.canonicalUnfilteredJson();
|
|
816
|
+
if (!json) {
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
return digest(crypto.createHash('sha256').update(json), 'base64');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function isValidFlag(value) {
|
|
823
|
+
return value !== null && typeof value === 'object' && typeof value.version === 'number';
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Loads cached flag data from storage for the given context.
|
|
827
|
+
*
|
|
828
|
+
* Checks the current storage key first, then falls back to the legacy
|
|
829
|
+
* canonical key location (pre-10.3.1). Does NOT perform migration — the
|
|
830
|
+
* caller is responsible for migrating data if {@link CachedFlagData.fromLegacyKey}
|
|
831
|
+
* is true.
|
|
832
|
+
*
|
|
833
|
+
* @returns The cached flag data, or `undefined` on cache miss or parse error.
|
|
834
|
+
*/
|
|
835
|
+
async function loadCachedFlags(storage, crypto, environmentNamespace, context, logger) {
|
|
836
|
+
const storageKey = await namespaceForContextData(crypto, environmentNamespace, context);
|
|
837
|
+
let flagsJson = await storage.get(storageKey);
|
|
838
|
+
let fromLegacyKey = false;
|
|
839
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
840
|
+
// Fallback: in version <10.3.1 flag data was stored under the canonical key.
|
|
841
|
+
flagsJson = await storage.get(context.canonicalKey);
|
|
842
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
fromLegacyKey = true;
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const parsed = JSON.parse(flagsJson);
|
|
849
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
850
|
+
logger?.warn('Cached flag data is not a valid object');
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
const entries = Object.entries(parsed);
|
|
854
|
+
const invalidKey = entries.find(([, value]) => !isValidFlag(value));
|
|
855
|
+
if (invalidKey) {
|
|
856
|
+
logger?.warn(`Discarding cached flags due to invalid entry: ${invalidKey[0]}`);
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
const flags = entries.reduce((acc, [key, value]) => {
|
|
860
|
+
acc[key] = value;
|
|
861
|
+
return acc;
|
|
862
|
+
}, {});
|
|
863
|
+
return { flags, storageKey, fromLegacyKey };
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
logger?.warn(`Could not parse cached flag evaluations from persistent storage: ${e.message}`);
|
|
867
|
+
return undefined;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
696
871
|
/**
|
|
697
872
|
* An index for tracking the most recently used contexts by timestamp with the ability to
|
|
698
873
|
* update entry timestamps and prune out least used contexts above a max capacity provided.
|
|
@@ -758,12 +933,18 @@ class ContextIndex {
|
|
|
758
933
|
* This class handles persisting and loading flag values from a persistent
|
|
759
934
|
* store. It intercepts updates and forwards them to the flag updater and
|
|
760
935
|
* then persists changes after the updater has completed.
|
|
936
|
+
*
|
|
937
|
+
* Freshness metadata (timestamp + context attribute hash) is stored in a
|
|
938
|
+
* separate storage key (`{contextKey}_freshness`) alongside the flag data.
|
|
939
|
+
* Both keys are managed together — when a context is evicted, both the flag
|
|
940
|
+
* data and freshness record are cleared.
|
|
761
941
|
*/
|
|
762
942
|
class FlagPersistence {
|
|
763
|
-
constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
943
|
+
constructor(_platform, _environmentNamespace, _maxCachedContexts, _disableCache, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
764
944
|
this._platform = _platform;
|
|
765
945
|
this._environmentNamespace = _environmentNamespace;
|
|
766
946
|
this._maxCachedContexts = _maxCachedContexts;
|
|
947
|
+
this._disableCache = _disableCache;
|
|
767
948
|
this._flagStore = _flagStore;
|
|
768
949
|
this._flagUpdater = _flagUpdater;
|
|
769
950
|
this._logger = _logger;
|
|
@@ -795,35 +976,38 @@ class FlagPersistence {
|
|
|
795
976
|
* {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
|
|
796
977
|
*/
|
|
797
978
|
async loadCached(context) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (flagsJson === null || flagsJson === undefined) {
|
|
801
|
-
// Fallback: in version <10.3.1 flag data was stored under the canonical key, check
|
|
802
|
-
// to see if data is present and migrate the data if present.
|
|
803
|
-
flagsJson = await this._platform.storage?.get(context.canonicalKey);
|
|
804
|
-
if (flagsJson === null || flagsJson === undefined) {
|
|
805
|
-
// return false indicating cache did not load if flag json is still absent
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
// migrate data from version <10.3.1 and cleanup data that was under canonical key
|
|
809
|
-
await this._platform.storage?.set(storageKey, flagsJson);
|
|
810
|
-
await this._platform.storage?.clear(context.canonicalKey);
|
|
979
|
+
if (this._disableCache || this._maxCachedContexts <= 0) {
|
|
980
|
+
return false;
|
|
811
981
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
// mapping flags to item descriptors
|
|
815
|
-
const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
|
|
816
|
-
acc[key] = { version: flag.version, flag };
|
|
817
|
-
return acc;
|
|
818
|
-
}, {});
|
|
819
|
-
this._flagUpdater.initCached(context, descriptors);
|
|
820
|
-
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
821
|
-
return true;
|
|
982
|
+
if (!this._platform.storage) {
|
|
983
|
+
return false;
|
|
822
984
|
}
|
|
823
|
-
|
|
824
|
-
|
|
985
|
+
const cached = await loadCachedFlags(this._platform.storage, this._platform.crypto, this._environmentNamespace, context, this._logger);
|
|
986
|
+
if (!cached) {
|
|
825
987
|
return false;
|
|
826
988
|
}
|
|
989
|
+
// Migrate data from version <10.3.1 stored under the canonical key
|
|
990
|
+
if (cached.fromLegacyKey) {
|
|
991
|
+
await this._platform.storage.set(cached.storageKey, JSON.stringify(cached.flags));
|
|
992
|
+
await this._platform.storage.clear(context.canonicalKey);
|
|
993
|
+
}
|
|
994
|
+
// mapping flags to item descriptors
|
|
995
|
+
const descriptors = Object.entries(cached.flags).reduce((acc, [key, flag]) => {
|
|
996
|
+
acc[key] = { version: flag.version, flag };
|
|
997
|
+
return acc;
|
|
998
|
+
}, {});
|
|
999
|
+
this._flagUpdater.initCached(context, descriptors);
|
|
1000
|
+
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
async _storeFreshness(contextStorageKey, context, timestamp) {
|
|
1004
|
+
const contextHash = await hashContext(this._platform.crypto, context);
|
|
1005
|
+
if (contextHash === undefined) {
|
|
1006
|
+
this._logger.error('Could not serialize context for freshness tracking');
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const record = { timestamp, contextHash };
|
|
1010
|
+
await this._platform.storage?.set(`${contextStorageKey}${FRESHNESS_SUFFIX}`, JSON.stringify(record));
|
|
827
1011
|
}
|
|
828
1012
|
async _loadIndex() {
|
|
829
1013
|
if (this._contextIndex !== undefined) {
|
|
@@ -845,13 +1029,26 @@ class FlagPersistence {
|
|
|
845
1029
|
return this._contextIndex;
|
|
846
1030
|
}
|
|
847
1031
|
async _storeCache(context) {
|
|
1032
|
+
if (this._disableCache) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const now = this._timeStamper();
|
|
848
1036
|
const index = await this._loadIndex();
|
|
849
1037
|
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
850
|
-
|
|
1038
|
+
if (this._maxCachedContexts > 0) {
|
|
1039
|
+
index.notice(storageKey, now);
|
|
1040
|
+
}
|
|
851
1041
|
const pruned = index.prune(this._maxCachedContexts);
|
|
852
|
-
|
|
853
|
-
|
|
1042
|
+
// If maxCachedContexts <= 0, current context was never added, so always skip flag write
|
|
1043
|
+
const currentContextWasPruned = this._maxCachedContexts <= 0 || pruned.some((it) => it.id === storageKey);
|
|
1044
|
+
await Promise.all(pruned.flatMap((it) => [
|
|
1045
|
+
this._platform.storage?.clear(it.id),
|
|
1046
|
+
this._platform.storage?.clear(`${it.id}${FRESHNESS_SUFFIX}`),
|
|
1047
|
+
]));
|
|
854
1048
|
await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
|
|
1049
|
+
if (currentContextWasPruned) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
855
1052
|
const allFlags = this._flagStore.getAll();
|
|
856
1053
|
// mapping item descriptors to flags
|
|
857
1054
|
const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
|
|
@@ -861,8 +1058,15 @@ class FlagPersistence {
|
|
|
861
1058
|
return acc;
|
|
862
1059
|
}, {});
|
|
863
1060
|
const jsonAll = JSON.stringify(flags);
|
|
864
|
-
// store flag data
|
|
1061
|
+
// store flag data first, so freshness is never newer than the flags it describes
|
|
865
1062
|
await this._platform.storage?.set(storageKey, jsonAll);
|
|
1063
|
+
// store freshness — best-effort, must not block flag persistence
|
|
1064
|
+
try {
|
|
1065
|
+
await this._storeFreshness(storageKey, context, now);
|
|
1066
|
+
}
|
|
1067
|
+
catch (e) {
|
|
1068
|
+
this._logger.warn(`Failed to store freshness data: ${e.message}`);
|
|
1069
|
+
}
|
|
866
1070
|
}
|
|
867
1071
|
}
|
|
868
1072
|
|
|
@@ -978,17 +1182,18 @@ class DefaultFlagManager {
|
|
|
978
1182
|
* @param platform implementation of various platform provided functionality
|
|
979
1183
|
* @param sdkKey that will be used to distinguish different environments
|
|
980
1184
|
* @param maxCachedContexts that specifies the max number of contexts that will be cached in persistence
|
|
1185
|
+
* @param disableCache set to true to completely disable the persistent flag cache
|
|
981
1186
|
* @param logger used for logging various messages
|
|
982
1187
|
* @param timeStamper exists for testing purposes
|
|
983
1188
|
*/
|
|
984
|
-
constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1189
|
+
constructor(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
985
1190
|
this._flagStore = createDefaultFlagStore();
|
|
986
1191
|
this._flagUpdater = createFlagUpdater(this._flagStore, logger);
|
|
987
|
-
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
1192
|
+
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper);
|
|
988
1193
|
}
|
|
989
|
-
async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
1194
|
+
async _initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
|
|
990
1195
|
const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
|
|
991
|
-
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
1196
|
+
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, disableCache, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
992
1197
|
}
|
|
993
1198
|
get(key) {
|
|
994
1199
|
if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
|
|
@@ -1464,7 +1669,7 @@ class LDClientImpl {
|
|
|
1464
1669
|
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1465
1670
|
this.logger = this._config.logger;
|
|
1466
1671
|
this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1467
|
-
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.logger);
|
|
1672
|
+
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
|
|
1468
1673
|
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
1469
1674
|
this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
|
|
1470
1675
|
this.emitter = new LDEmitter();
|
|
@@ -2558,5 +2763,68 @@ class BaseDataManager {
|
|
|
2558
2763
|
}
|
|
2559
2764
|
}
|
|
2560
2765
|
|
|
2561
|
-
|
|
2766
|
+
function allConditionsMatch(conditions, input) {
|
|
2767
|
+
return Object.entries(conditions).every(([key, value]) => value === undefined || input[key] === value);
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Given a mode resolution table and the current input state, returns the
|
|
2771
|
+
* resolved FDv2 connection mode.
|
|
2772
|
+
*
|
|
2773
|
+
* Iterates entries in order. The first entry whose conditions all match the
|
|
2774
|
+
* input wins. If no entry matches (should not happen when the table ends with
|
|
2775
|
+
* a catch-all), falls back to `input.foregroundMode`.
|
|
2776
|
+
*/
|
|
2777
|
+
const CONFIGURED_MODE_MAP = {
|
|
2778
|
+
foreground: 'foregroundMode',
|
|
2779
|
+
background: 'backgroundMode',
|
|
2780
|
+
};
|
|
2781
|
+
function resolveConnectionMode(table, input) {
|
|
2782
|
+
const match = table.find((entry) => allConditionsMatch(entry.conditions, input));
|
|
2783
|
+
if (match) {
|
|
2784
|
+
const { mode } = match;
|
|
2785
|
+
if (typeof mode === 'object') {
|
|
2786
|
+
return input[CONFIGURED_MODE_MAP[mode.configured]];
|
|
2787
|
+
}
|
|
2788
|
+
return mode;
|
|
2789
|
+
}
|
|
2790
|
+
return input.foregroundMode;
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Mode resolution table for mobile platforms (React Native, etc.).
|
|
2794
|
+
*
|
|
2795
|
+
* - No network → offline.
|
|
2796
|
+
* - Background → configured background mode.
|
|
2797
|
+
* - Foreground → configured foreground mode.
|
|
2798
|
+
*/
|
|
2799
|
+
const MOBILE_TRANSITION_TABLE = [
|
|
2800
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2801
|
+
{ conditions: { lifecycle: 'background' }, mode: { configured: 'background' } },
|
|
2802
|
+
{ conditions: { lifecycle: 'foreground' }, mode: { configured: 'foreground' } },
|
|
2803
|
+
];
|
|
2804
|
+
/**
|
|
2805
|
+
* Mode resolution table for browser platforms.
|
|
2806
|
+
*
|
|
2807
|
+
* - No network → offline.
|
|
2808
|
+
* - Otherwise → configured foreground mode.
|
|
2809
|
+
*
|
|
2810
|
+
* Browser listener-driven streaming (auto-promotion to streaming when change
|
|
2811
|
+
* listeners are registered) is handled externally by the caller modifying
|
|
2812
|
+
* `foregroundMode` before consulting this table.
|
|
2813
|
+
*/
|
|
2814
|
+
const BROWSER_TRANSITION_TABLE = [
|
|
2815
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2816
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
2817
|
+
];
|
|
2818
|
+
/**
|
|
2819
|
+
* Mode resolution table for desktop platforms (Electron, etc.).
|
|
2820
|
+
*
|
|
2821
|
+
* - No network → offline.
|
|
2822
|
+
* - Otherwise → configured foreground mode.
|
|
2823
|
+
*/
|
|
2824
|
+
const DESKTOP_TRANSITION_TABLE = [
|
|
2825
|
+
{ conditions: { networkAvailable: false }, mode: 'offline' },
|
|
2826
|
+
{ conditions: {}, mode: { configured: 'foreground' } },
|
|
2827
|
+
];
|
|
2828
|
+
|
|
2829
|
+
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, browserFdv1Endpoints, dataSystemValidators, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, resolveConnectionMode, safeRegisterDebugOverridePlugins, validateOptions };
|
|
2562
2830
|
//# sourceMappingURL=index.mjs.map
|