@configdirector/nuxt-sdk 1.11.1 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/module.d.mts +13 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -1
  4. package/dist/runtime/app/composables/useConfigDirectorClient.d.mts +1 -1
  5. package/dist/runtime/app/composables/useConfigDirectorClient.d.ts +1 -1
  6. package/dist/runtime/app/composables/useConfigDirectorClientHooks.d.mts +1 -1
  7. package/dist/runtime/app/composables/useConfigDirectorClientHooks.d.ts +1 -1
  8. package/dist/runtime/app/composables/useConfigDirectorContext.d.mts +1 -1
  9. package/dist/runtime/app/composables/useConfigDirectorContext.d.ts +1 -1
  10. package/dist/runtime/app/composables/useConfigDirectorContext.mjs +41 -2
  11. package/dist/runtime/app/composables/useConfigDirectorValue.d.mts +1 -1
  12. package/dist/runtime/app/composables/useConfigDirectorValue.d.ts +1 -1
  13. package/dist/runtime/{native-values-BV2k_xR2.d.mts → native-values-ChQdi7c7.d.mts} +6 -4
  14. package/dist/runtime/nitro/offline-values.d.mts +1 -1
  15. package/dist/runtime/nitro/offline-values.d.ts +1 -1
  16. package/dist/runtime/nitro/plugin.d.mts +2 -2
  17. package/dist/runtime/nitro/plugin.d.ts +2 -2
  18. package/dist/runtime/nitro/plugin.mjs +1 -1
  19. package/dist/runtime/plugin.client.mjs +403 -73
  20. package/dist/runtime/plugin.server.mjs +46 -2
  21. package/dist/runtime/server/composables/useConfigDirectorServerClient.d.mts +1 -1
  22. package/dist/runtime/server/composables/useConfigDirectorServerClient.d.ts +1 -1
  23. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.mts +1 -1
  24. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.ts +1 -1
  25. package/dist/runtime/{types-CS7AMNWe.d.mts → types-BoDJiQsr.d.mts} +1 -1
  26. package/dist/runtime/{types-Dt3bENKf.d.mts → types-CuzrFq9I.d.mts} +3 -1
  27. package/dist/testing.d.mts +7 -4
  28. package/dist/testing.mjs +280 -31
  29. package/package.json +2 -2
@@ -58,6 +58,37 @@ const admitContext = (context) => {
58
58
  };
59
59
  };
60
60
  //#endregion
61
+ //#region ../shared/src/deepEqual.ts
62
+ const deepEqual = (left, right) => {
63
+ if (Object.is(left, right)) return true;
64
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
65
+ if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right)) return false;
66
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
67
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => deepEqual(item, right[index]));
68
+ if (!isPlainObject(left) || !isPlainObject(right)) return false;
69
+ const leftKeys = Object.keys(left);
70
+ const rightKeys = Object.keys(right);
71
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && deepEqual(left[key], right[key]));
72
+ };
73
+ const isPlainObject = (value) => {
74
+ const prototype = Object.getPrototypeOf(value);
75
+ return prototype === Object.prototype || prototype === null;
76
+ };
77
+ //#endregion
78
+ //#region ../js-client-core/src/mountAction.ts
79
+ const decideMountAction = (client, context) => {
80
+ if (client.isInitializing) return "none";
81
+ if (!client.isReady) return "initialize";
82
+ if (context !== void 0 && !deepEqual(context, client.context)) return "updateContext";
83
+ return "none";
84
+ };
85
+ //#endregion
86
+ //#region src/runtime/client-startup.ts
87
+ const decideContextUpdateAction = (client, context, waitsForFirstContext) => {
88
+ if (waitsForFirstContext && decideMountAction(client, context) === "initialize") return "startClient";
89
+ return "updateContext";
90
+ };
91
+ //#endregion
61
92
  //#region src/runtime/app/composables/useConfigDirectorContext.ts
62
93
  const DEFAULT_TIMEOUT = 3e3;
63
94
  const getEffectiveTimeout = (option) => {
@@ -66,6 +97,14 @@ const getEffectiveTimeout = (option) => {
66
97
  const useConfigDirectorContext = () => {
67
98
  const state = useState("configdirector-context");
68
99
  const { client } = useConfigDirectorClient();
100
+ const { $configDirectorClientStartup } = useNuxtApp();
101
+ const startup = $configDirectorClientStartup;
102
+ const handContextToClient = (rawContext) => {
103
+ switch (decideContextUpdateAction(client, rawContext, startup.waitsForFirstContext)) {
104
+ case "startClient": return startup.start(rawContext);
105
+ case "updateContext": return client.updateContext(rawContext);
106
+ }
107
+ };
69
108
  const updateContext = async (context, options) => {
70
109
  const rawContext = toRaw(context);
71
110
  const oldContext = JSON.stringify(state.value);
@@ -75,7 +114,7 @@ const useConfigDirectorContext = () => {
75
114
  const timeout = getEffectiveTimeout(options?.timeoutMilliseconds);
76
115
  let timer;
77
116
  try {
78
- await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
117
+ await Promise.race([handContextToClient(rawContext), new Promise((resolve) => {
79
118
  timer = setTimeout(() => resolve(), timeout);
80
119
  })]);
81
120
  } finally {
@@ -98,6 +137,10 @@ var plugin_server_default = defineNuxtPlugin((nuxtApp) => {
98
137
  if (!serverSdkClient) throw "The nitro ConfigDirector plugin was not initialized";
99
138
  const client = createSsrClient(serverSdkClient);
100
139
  const readyStatus = shallowRef(serverSdkClient.isReady ? "ready" : "default");
140
+ const startup = {
141
+ waitsForFirstContext: false,
142
+ start: (context) => client.initialize(context)
143
+ };
101
144
  nuxtApp.hooks.hook("app:created", async () => {
102
145
  logger.debug("Updating ConfigDirector context for SSR evaluation");
103
146
  const { context } = useConfigDirectorContext();
@@ -105,7 +148,8 @@ var plugin_server_default = defineNuxtPlugin((nuxtApp) => {
105
148
  });
106
149
  return { provide: {
107
150
  configDirectorClient: client,
108
- configDirectorClientReadyStatus: readonly(readyStatus)
151
+ configDirectorClientReadyStatus: readonly(readyStatus),
152
+ configDirectorClientStartup: startup
109
153
  } };
110
154
  });
111
155
  var SsrClient = class {
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-CS7AMNWe.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-BoDJiQsr.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerClient.d.ts
4
4
  declare const useConfigDirectorServerClient: () => ConfigDirectorClient;
@@ -1,4 +1,4 @@
1
- import { n as ConfigDirectorClient } from "../../types-CS7AMNWe.mjs";
1
+ import { n as ConfigDirectorClient } from "../../types-BoDJiQsr.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerClient.d.ts
4
4
  declare const useConfigDirectorServerClient: () => ConfigDirectorClient;
@@ -1,4 +1,4 @@
1
- import { t as ClientHooks } from "../../types-CS7AMNWe.mjs";
1
+ import { t as ClientHooks } from "../../types-BoDJiQsr.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
4
  declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
@@ -1,4 +1,4 @@
1
- import { t as ClientHooks } from "../../types-CS7AMNWe.mjs";
1
+ import { t as ClientHooks } from "../../types-BoDJiQsr.mjs";
2
2
 
3
3
  //#region src/runtime/server/composables/useConfigDirectorServerHooks.d.ts
4
4
  declare const useConfigDirectorServerHooks: (hooks: ClientHooks) => void;
@@ -1,4 +1,4 @@
1
- import { a as ConfigValueType, i as ConfigState, n as ConfigDirectorContext, r as ConfigEvaluation } from "./native-values-BV2k_xR2.mjs";
1
+ import { a as ConfigValueType, i as ConfigState, n as ConfigDirectorContext, r as ConfigEvaluation } from "./native-values-ChQdi7c7.mjs";
2
2
 
3
3
  //#region ../js-server-sdk/src/types.d.ts
4
4
  type ClientEvents = {
@@ -1,4 +1,4 @@
1
- import { a as ConfigValueType, n as ConfigDirectorContext, r as ConfigEvaluation } from "./native-values-BV2k_xR2.mjs";
1
+ import { a as ConfigValueType, n as ConfigDirectorContext, r as ConfigEvaluation } from "./native-values-ChQdi7c7.mjs";
2
2
 
3
3
  //#region ../js-client-core/src/Emitter.d.ts
4
4
  type EventType = string | symbol;
@@ -17,6 +17,7 @@ type ClientHooks = {
17
17
  contextUpdated?: HookHandler<"contextUpdated"> | HookHandler<"contextUpdated">[];
18
18
  configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
19
19
  connectionError?: HookHandler<"connectionError"> | HookHandler<"connectionError">[];
20
+ cachedValuesLoaded?: HookHandler<"cachedValuesLoaded"> | HookHandler<"cachedValuesLoaded">[];
20
21
  };
21
22
  type ClientConnectAction = "initialization" | "context update" | "network resume";
22
23
  type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
@@ -37,6 +38,7 @@ type ClientEvents = {
37
38
  connectionError: {
38
39
  error: Error;
39
40
  };
41
+ cachedValuesLoaded: Record<string, never>;
40
42
  };
41
43
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
42
44
  /**
@@ -19,7 +19,7 @@ interface ConfigDirectorLogger {
19
19
  warn(message: string, ...args: any): void;
20
20
  error(message: string, ...args: any): void;
21
21
  }
22
- type EvaluationReason = "found-match" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean";
22
+ type EvaluationReason = "found-match" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean" | "cached";
23
23
  type ConfigEvaluation = {
24
24
  key: string;
25
25
  value: ConfigValueType;
@@ -38,9 +38,11 @@ type ConfigDirectorContext = {
38
38
  * The context's identifier. This should be a value that uniquely identifies the user or other
39
39
  * entity among contexts of the same {@link kind}.
40
40
  * In the case of anonymous users, you could generate a UUID or alternatively not provide
41
- * the {@link id} and the SDK will generate a random UUID. However, keep in mind that this
42
- * value is used for segmenting contexts in percentage rollouts, and changes to the {@link id}
43
- * could result in the context being assigned to a different percentile.
41
+ * the {@link id}. In a client SDK, ConfigDirector then derives an identifier from an ID the SDK
42
+ * stores for the app install or the website, so it stays the same until the app is uninstalled
43
+ * or the site's data is cleared. However, keep in mind that this value is used for segmenting
44
+ * contexts in percentage rollouts, and changes to the {@link id} could result in the context
45
+ * being assigned to a different percentile.
44
46
  */
45
47
  id?: string;
46
48
  /**
@@ -105,6 +107,7 @@ type ClientEvents = {
105
107
  connectionError: {
106
108
  error: Error;
107
109
  };
110
+ cachedValuesLoaded: Record<string, never>;
108
111
  };
109
112
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
110
113
  /**
package/dist/testing.mjs CHANGED
@@ -920,10 +920,27 @@ const generateInstanceId = () => {
920
920
  });
921
921
  };
922
922
  //#endregion
923
+ //#region ../js-client-core/src/browser-globals.ts
924
+ const read = (readValue) => {
925
+ try {
926
+ return readValue(globalThis);
927
+ } catch {
928
+ return;
929
+ }
930
+ };
931
+ const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
932
+ const readHost = () => read((globals) => globals.location?.host);
933
+ const localStorageIfAvailable = () => {
934
+ try {
935
+ return globalThis.localStorage ?? void 0;
936
+ } catch {
937
+ return;
938
+ }
939
+ };
940
+ //#endregion
923
941
  //#region ../js-client-core/src/client-session.ts
924
942
  const STORAGE_KEY = "configdirector-sdk:instance-id";
925
943
  const RECORD_VERSION = 1;
926
- const MAX_AGE_MS = 360 * 60 * 1e3;
927
944
  const STORAGE_DEADLINE_MS = 1e3;
928
945
  const CHECKSUM_SALT = "configdirector.instanceId.v1";
929
946
  const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -940,13 +957,6 @@ const checksumFor = (id, createdAt) => {
940
957
  const low = fnv1a(payload, 2166136261);
941
958
  return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
942
959
  };
943
- const localStorageIfAvailable = () => {
944
- try {
945
- return globalThis.localStorage ?? void 0;
946
- } catch {
947
- return;
948
- }
949
- };
950
960
  const isUsableRecord = (record) => {
951
961
  if (!record || typeof record !== "object") return false;
952
962
  const { v, id, createdAt, checksum } = record;
@@ -975,11 +985,6 @@ const readStoredInstanceId = async (storage, logger) => {
975
985
  logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
976
986
  return { outcome: "nothing usable" };
977
987
  }
978
- const age = Date.now() - record.createdAt;
979
- if (age < 0 || age >= MAX_AGE_MS) {
980
- logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
981
- return { outcome: "nothing usable" };
982
- }
983
988
  return {
984
989
  outcome: "stored",
985
990
  instanceId: record.id
@@ -1195,17 +1200,6 @@ var PollingTransport = class extends AbstractPollingTransport {
1195
1200
  }
1196
1201
  };
1197
1202
  //#endregion
1198
- //#region ../js-client-core/src/browser-globals.ts
1199
- const read = (readValue) => {
1200
- try {
1201
- return readValue(globalThis);
1202
- } catch {
1203
- return;
1204
- }
1205
- };
1206
- const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
1207
- const readHost = () => read((globals) => globals.location?.host);
1208
- //#endregion
1209
1203
  //#region ../shared/src/context-kind.ts
1210
1204
  const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
1211
1205
  const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
@@ -1246,6 +1240,176 @@ const admitContext = (context) => {
1246
1240
  };
1247
1241
  };
1248
1242
  //#endregion
1243
+ //#region ../shared/src/utf8-byte-length.ts
1244
+ const isHighSurrogate = (codeUnit) => codeUnit >= 55296 && codeUnit <= 56319;
1245
+ const isLowSurrogate = (codeUnit) => codeUnit >= 56320 && codeUnit <= 57343;
1246
+ const utf8ByteLength = (text) => {
1247
+ let bytes = 0;
1248
+ for (let i = 0; i < text.length; i++) {
1249
+ const codeUnit = text.charCodeAt(i);
1250
+ if (codeUnit <= 127) bytes += 1;
1251
+ else if (codeUnit <= 2047) bytes += 2;
1252
+ else if (isHighSurrogate(codeUnit) && i + 1 < text.length && isLowSurrogate(text.charCodeAt(i + 1))) {
1253
+ bytes += 4;
1254
+ i++;
1255
+ } else bytes += 3;
1256
+ }
1257
+ return bytes;
1258
+ };
1259
+ //#endregion
1260
+ //#region ../js-client-core/src/cached-values.ts
1261
+ const ENTRY_FORMAT_VERSION = 1;
1262
+ const DEFAULT_CONTEXT_KIND = "user";
1263
+ const planCaching = (connectionPlan, caching, platformProvider) => {
1264
+ if (connectionPlan.kind === "stay-offline") return {
1265
+ kind: "no cache",
1266
+ reason: "offline mode"
1267
+ };
1268
+ if (caching?.enabled === false) return {
1269
+ kind: "no cache",
1270
+ reason: "turned off"
1271
+ };
1272
+ const provider = caching?.provider ?? platformProvider?.(connectionPlan.clientSdkKey);
1273
+ if (!provider) return {
1274
+ kind: "no cache",
1275
+ reason: "no provider available"
1276
+ };
1277
+ return {
1278
+ kind: "cache",
1279
+ provider,
1280
+ clientSdkKey: connectionPlan.clientSdkKey
1281
+ };
1282
+ };
1283
+ const contextTagFor = (context, instanceId) => {
1284
+ const contextKind = context?.kind ?? DEFAULT_CONTEXT_KIND;
1285
+ if (context?.id != null) return {
1286
+ contextKind,
1287
+ contextId: context.id
1288
+ };
1289
+ return {
1290
+ contextKind,
1291
+ anonymousInstanceId: instanceId
1292
+ };
1293
+ };
1294
+ const sameContextTag = (stored, launch) => {
1295
+ if (stored.contextKind !== launch.contextKind) return false;
1296
+ if ("contextId" in stored && "contextId" in launch) return stored.contextId === launch.contextId;
1297
+ if ("anonymousInstanceId" in stored && "anonymousInstanceId" in launch) return stored.anonymousInstanceId === launch.anonymousInstanceId;
1298
+ return false;
1299
+ };
1300
+ const decideCachedValuesAdoption = (cachedValues, launchContextTag, hasReceivedLiveValues) => {
1301
+ if (hasReceivedLiveValues) return "live values already arrived";
1302
+ if (!sameContextTag(cachedValues.contextTag, launchContextTag)) return "context mismatch";
1303
+ return "adopt";
1304
+ };
1305
+ const isContextTag = (candidate) => {
1306
+ if (!candidate || typeof candidate !== "object") return false;
1307
+ const { contextKind, contextId, anonymousInstanceId } = candidate;
1308
+ if (typeof contextKind !== "string") return false;
1309
+ return typeof contextId === "string" || typeof anonymousInstanceId === "string";
1310
+ };
1311
+ const isConfigStateMap = (candidate) => {
1312
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return false;
1313
+ return Object.values(candidate).every((config) => config && typeof config === "object" && typeof config.id === "string" && typeof config.key === "string");
1314
+ };
1315
+ const isEntryFor = (clientSdkKey, candidate) => {
1316
+ if (!candidate || typeof candidate !== "object") return false;
1317
+ const entry = candidate;
1318
+ return entry.formatVersion === ENTRY_FORMAT_VERSION && entry.clientSdkKey === clientSdkKey && isContextTag(entry.contextTag) && typeof entry.environmentId === "string" && typeof entry.projectId === "string" && isConfigStateMap(entry.configs) && typeof entry.receivedAt === "number";
1319
+ };
1320
+ var CachedValuesStore = class {
1321
+ warned = /* @__PURE__ */ new Set();
1322
+ pendingRead;
1323
+ queuedCachedValues;
1324
+ writing = false;
1325
+ constructor(provider, clientSdkKey, logger) {
1326
+ this.provider = provider;
1327
+ this.clientSdkKey = clientSdkKey;
1328
+ this.logger = logger;
1329
+ }
1330
+ read() {
1331
+ this.pendingRead ??= this.readEntry();
1332
+ return this.pendingRead;
1333
+ }
1334
+ replace(cachedValues) {
1335
+ this.queuedCachedValues = cachedValues;
1336
+ this.writeQueued();
1337
+ }
1338
+ async readEntry() {
1339
+ let raw;
1340
+ try {
1341
+ raw = await this.provider.read();
1342
+ } catch (error) {
1343
+ this.warnOnce("read failed", "Could not read the values kept on the device, starting without them:", error);
1344
+ return { outcome: "failed" };
1345
+ }
1346
+ if (raw == null || raw === "") return { outcome: "nothing stored" };
1347
+ const entry = this.parseEntry(raw);
1348
+ if (!entry) {
1349
+ this.warnOnce("unreadable", "The values kept on the device could not be read and were removed, starting without them");
1350
+ await this.removeEntry();
1351
+ return { outcome: "unreadable" };
1352
+ }
1353
+ return {
1354
+ outcome: "stored",
1355
+ cachedValues: entry
1356
+ };
1357
+ }
1358
+ parseEntry(raw) {
1359
+ try {
1360
+ const candidate = JSON.parse(raw);
1361
+ return isEntryFor(this.clientSdkKey, candidate) ? candidate : void 0;
1362
+ } catch {
1363
+ return;
1364
+ }
1365
+ }
1366
+ async writeQueued() {
1367
+ if (this.writing) return;
1368
+ this.writing = true;
1369
+ try {
1370
+ while (this.queuedCachedValues) {
1371
+ const cachedValues = this.queuedCachedValues;
1372
+ this.queuedCachedValues = void 0;
1373
+ await this.writeEntry(cachedValues);
1374
+ }
1375
+ } finally {
1376
+ this.writing = false;
1377
+ }
1378
+ }
1379
+ async writeEntry(cachedValues) {
1380
+ const entry = {
1381
+ formatVersion: ENTRY_FORMAT_VERSION,
1382
+ clientSdkKey: this.clientSdkKey,
1383
+ ...cachedValues
1384
+ };
1385
+ const serialized = JSON.stringify(entry);
1386
+ const size = utf8ByteLength(serialized);
1387
+ const limit = this.provider.sizeLimit();
1388
+ if (size > limit) {
1389
+ this.warnOnce("over the size limit", `The values received (${size} bytes) exceed the caching provider's limit of ${limit} bytes, so no values are kept on the device`);
1390
+ await this.removeEntry();
1391
+ return;
1392
+ }
1393
+ try {
1394
+ await this.provider.write(serialized);
1395
+ } catch (error) {
1396
+ this.warnOnce("write failed", "Could not keep the values on the device:", error);
1397
+ }
1398
+ }
1399
+ async removeEntry() {
1400
+ try {
1401
+ await this.provider.remove();
1402
+ } catch (error) {
1403
+ this.warnOnce("remove failed", "Could not remove the values kept on the device:", error);
1404
+ }
1405
+ }
1406
+ warnOnce(warning, message, ...details) {
1407
+ if (this.warned.has(warning)) return;
1408
+ this.warned.add(warning);
1409
+ this.logger.warn(`[ConfigDirectorClient] ${message}`, ...details);
1410
+ }
1411
+ };
1412
+ //#endregion
1249
1413
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
1250
1414
  const MAX_EXPONENTIAL_DELAY = 9;
1251
1415
  var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
@@ -1263,11 +1427,15 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1263
1427
  readyPromise;
1264
1428
  readyResolve;
1265
1429
  currentContext;
1430
+ submittedContext;
1266
1431
  connectionPlan;
1267
1432
  clientSession;
1268
- static from(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions) {
1269
- if (typeof clientSdkKeyOrOptions === "object") return new DefaultConfigDirectorClient(liveTelemetryClient, void 0, sdkOptions, clientSdkKeyOrOptions);
1270
- return new DefaultConfigDirectorClient(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions);
1433
+ cachedValuesStore;
1434
+ servingCachedValues = false;
1435
+ cachedValuesAdoptionStarted = false;
1436
+ static from(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions, internalClientOptions) {
1437
+ if (typeof clientSdkKeyOrOptions === "object") return new DefaultConfigDirectorClient(liveTelemetryClient, void 0, sdkOptions, clientSdkKeyOrOptions, internalClientOptions);
1438
+ return new DefaultConfigDirectorClient(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions, internalClientOptions);
1271
1439
  }
1272
1440
  constructor(liveTelemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1273
1441
  this.logger = clientOptions?.logger ?? createDefaultLogger();
@@ -1275,10 +1443,12 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1275
1443
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
1276
1444
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
1277
1445
  this.connectionPlan = planConnection(clientSdkKey, clientOptions?.connection, urlFactory);
1446
+ this.cachedValuesStore = this.buildCachedValuesStore(planCaching(this.connectionPlan, clientOptions?.caching, internalClientOptions?.platformCachingProvider));
1278
1447
  this.telemetryClient = this.buildTelemetryClient(liveTelemetryClient, clientOptions);
1279
1448
  this.transport = internalClientOptions?.transport ?? this.buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions);
1280
1449
  this.transport.on("configSetReceived", (configSet) => {
1281
1450
  this.hasReceivedConfigSet = true;
1451
+ this.servingCachedValues = false;
1282
1452
  this.readyResolve?.();
1283
1453
  const removedKeys = this.findKeysRemovedBy(configSet);
1284
1454
  if (!this.configSet || configSet.kind == "full") this.configSet = configSet;
@@ -1286,6 +1456,7 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1286
1456
  ...this.configSet.configs,
1287
1457
  ...configSet.configs
1288
1458
  };
1459
+ this.keepValuesOnTheDevice(this.configSet);
1289
1460
  const keys = Object.keys(configSet.configs);
1290
1461
  this.emit("configsUpdated", {
1291
1462
  keys,
@@ -1307,6 +1478,80 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1307
1478
  this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
1308
1479
  this.registerHandler("contextUpdated", clientOptions?.hooks?.contextUpdated);
1309
1480
  this.registerHandler("connectionError", clientOptions?.hooks?.connectionError);
1481
+ this.registerHandler("cachedValuesLoaded", clientOptions?.hooks?.cachedValuesLoaded);
1482
+ }
1483
+ buildCachedValuesStore(cachingPlan) {
1484
+ switch (cachingPlan.kind) {
1485
+ case "no cache":
1486
+ this.logger.debug(`[ConfigDirectorClient] No values are kept on the device: ${cachingPlan.reason}`);
1487
+ return;
1488
+ case "cache": {
1489
+ const store = new CachedValuesStore(cachingPlan.provider, cachingPlan.clientSdkKey, this.logger);
1490
+ store.read();
1491
+ return store;
1492
+ }
1493
+ }
1494
+ }
1495
+ startCachedValuesAdoption() {
1496
+ const store = this.cachedValuesStore;
1497
+ if (!store || this.cachedValuesAdoptionStarted) return;
1498
+ this.cachedValuesAdoptionStarted = true;
1499
+ this.adoptCachedValues(store);
1500
+ }
1501
+ async adoptCachedValues(store) {
1502
+ const read = await store.read();
1503
+ switch (read.outcome) {
1504
+ case "nothing stored":
1505
+ case "unreadable":
1506
+ case "failed": return;
1507
+ case "stored": break;
1508
+ }
1509
+ const { instanceId } = await this.clientSession;
1510
+ const launchContextTag = contextTagFor(this.submittedContext, instanceId);
1511
+ const adoption = decideCachedValuesAdoption(read.cachedValues, launchContextTag, this.hasReceivedConfigSet);
1512
+ switch (adoption) {
1513
+ case "adopt":
1514
+ this.serveCachedValues(read.cachedValues);
1515
+ return;
1516
+ case "live values already arrived":
1517
+ case "context mismatch":
1518
+ this.logger.debug(`[ConfigDirectorClient] The values kept on the device were not adopted: ${adoption}`);
1519
+ return;
1520
+ }
1521
+ }
1522
+ serveCachedValues(cachedValues) {
1523
+ const { environmentId, projectId, configs } = cachedValues;
1524
+ this.configSet = {
1525
+ environmentId,
1526
+ projectId,
1527
+ configs,
1528
+ kind: "full"
1529
+ };
1530
+ this.servingCachedValues = true;
1531
+ const keys = Object.keys(configs);
1532
+ this.emit("cachedValuesLoaded", {});
1533
+ this.emit("configsUpdated", {
1534
+ keys,
1535
+ removedKeys: []
1536
+ });
1537
+ this.updateWatchers(keys);
1538
+ this.logger.debug("[ConfigDirectorClient] Serving the values kept on the device until live values arrive:", { keys });
1539
+ }
1540
+ keepValuesOnTheDevice(configSet) {
1541
+ const store = this.cachedValuesStore;
1542
+ if (!store) return;
1543
+ const context = this.submittedContext;
1544
+ const receivedAt = Date.now();
1545
+ this.clientSession.then(({ instanceId }) => store.replace({
1546
+ contextTag: contextTagFor(context, instanceId),
1547
+ environmentId: configSet.environmentId,
1548
+ projectId: configSet.projectId,
1549
+ configs: configSet.configs,
1550
+ receivedAt
1551
+ }));
1552
+ }
1553
+ servedReason(reason) {
1554
+ return this.servingCachedValues && reason === "found-match" ? "cached" : reason;
1310
1555
  }
1311
1556
  registerHandler(event, handler) {
1312
1557
  if (!handler) return;
@@ -1330,7 +1575,8 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1330
1575
  clientSdkKey: plan.clientSdkKey,
1331
1576
  baseUrl: plan.baseUrl,
1332
1577
  logger: this.logger,
1333
- metadata: clientOptions?.metadata
1578
+ metadata: clientOptions?.metadata,
1579
+ clientSession: this.clientSession
1334
1580
  });
1335
1581
  }
1336
1582
  }
@@ -1400,6 +1646,8 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1400
1646
  return this.currentContext;
1401
1647
  }
1402
1648
  async connectToTransport(context, caller) {
1649
+ this.submittedContext = context;
1650
+ if (caller === "initialization") this.startCachedValuesAdoption();
1403
1651
  try {
1404
1652
  this.ready = false;
1405
1653
  this.readyPromise = new Promise((resolve) => {
@@ -1500,6 +1748,7 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1500
1748
  return defaultValue;
1501
1749
  }
1502
1750
  const parseResult = parseConfigValue(configState, defaultValue);
1751
+ const reason = this.servedReason(parseResult.reason);
1503
1752
  this.telemetryClient.evaluatedConfig({
1504
1753
  contextId: this.currentContext?.id,
1505
1754
  type: configState.type,
@@ -1509,14 +1758,14 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1509
1758
  evaluatedValue: parseResult.parsedValue,
1510
1759
  evaluatedValueId: parseResult.parsedValueId,
1511
1760
  usedDefault: parseResult.usedDefault,
1512
- evaluationReason: parseResult.reason
1761
+ evaluationReason: reason
1513
1762
  });
1514
1763
  this.dispatchEvaluationEvent({
1515
1764
  key: configKey,
1516
1765
  value: parseResult.parsedValue,
1517
1766
  valueId: parseResult.parsedValueId ?? void 0,
1518
1767
  isDefaultValue: parseResult.usedDefault,
1519
- reason: parseResult.reason,
1768
+ reason,
1520
1769
  context: this.context
1521
1770
  });
1522
1771
  this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
@@ -1882,7 +2131,7 @@ const installTestClient = (testClient) => {
1882
2131
  const createTestClient = (options) => {
1883
2132
  return createTestClient$1({
1884
2133
  sdkName: "nuxt-sdk",
1885
- sdkVersion: "1.11.1"
2134
+ sdkVersion: "1.12.1"
1886
2135
  }, options);
1887
2136
  };
1888
2137
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/nuxt-sdk",
3
- "version": "1.11.1",
3
+ "version": "1.12.1",
4
4
  "description": "Nuxt SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",
@@ -73,7 +73,7 @@
73
73
  "eslint": "^10.7.0",
74
74
  "jsdom": "^30.0.1",
75
75
  "nuxt": "^4.5.2",
76
- "playwright": "^1.60.0",
76
+ "playwright": "^1.64.1",
77
77
  "rimraf": "^6.1.3",
78
78
  "tsdown": "^0.21.7",
79
79
  "tslib": "^2.8.1",