@configdirector/nuxt-sdk 1.11.0 → 1.12.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.
Files changed (31) 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 +385 -69
  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/useConfigDirectorServerClient.mjs +1 -1
  24. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.mts +1 -1
  25. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.ts +1 -1
  26. package/dist/runtime/server/composables/useConfigDirectorServerHooks.mjs +2 -3
  27. package/dist/runtime/{types-CS7AMNWe.d.mts → types-BoDJiQsr.d.mts} +1 -1
  28. package/dist/runtime/{types-Dt3bENKf.d.mts → types-CuzrFq9I.d.mts} +3 -1
  29. package/dist/testing.d.mts +7 -4
  30. package/dist/testing.mjs +278 -30
  31. package/package.json +1 -1
@@ -923,10 +923,27 @@ const generateInstanceId = () => {
923
923
  });
924
924
  };
925
925
  //#endregion
926
+ //#region ../js-client-core/src/browser-globals.ts
927
+ const read = (readValue) => {
928
+ try {
929
+ return readValue(globalThis);
930
+ } catch {
931
+ return;
932
+ }
933
+ };
934
+ const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
935
+ const readHost = () => read((globals) => globals.location?.host);
936
+ const localStorageIfAvailable = () => {
937
+ try {
938
+ return globalThis.localStorage ?? void 0;
939
+ } catch {
940
+ return;
941
+ }
942
+ };
943
+ //#endregion
926
944
  //#region ../js-client-core/src/client-session.ts
927
945
  const STORAGE_KEY = "configdirector-sdk:instance-id";
928
946
  const RECORD_VERSION = 1;
929
- const MAX_AGE_MS = 360 * 60 * 1e3;
930
947
  const STORAGE_DEADLINE_MS = 1e3;
931
948
  const CHECKSUM_SALT = "configdirector.instanceId.v1";
932
949
  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}$/;
@@ -943,13 +960,6 @@ const checksumFor = (id, createdAt) => {
943
960
  const low = fnv1a(payload, 2166136261);
944
961
  return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
945
962
  };
946
- const localStorageIfAvailable = () => {
947
- try {
948
- return globalThis.localStorage ?? void 0;
949
- } catch {
950
- return;
951
- }
952
- };
953
963
  const isUsableRecord = (record) => {
954
964
  if (!record || typeof record !== "object") return false;
955
965
  const { v, id, createdAt, checksum } = record;
@@ -978,11 +988,6 @@ const readStoredInstanceId = async (storage, logger) => {
978
988
  logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
979
989
  return { outcome: "nothing usable" };
980
990
  }
981
- const age = Date.now() - record.createdAt;
982
- if (age < 0 || age >= MAX_AGE_MS) {
983
- logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
984
- return { outcome: "nothing usable" };
985
- }
986
991
  return {
987
992
  outcome: "stored",
988
993
  instanceId: record.id
@@ -1198,17 +1203,6 @@ var PollingTransport = class extends AbstractPollingTransport {
1198
1203
  }
1199
1204
  };
1200
1205
  //#endregion
1201
- //#region ../js-client-core/src/browser-globals.ts
1202
- const read = (readValue) => {
1203
- try {
1204
- return readValue(globalThis);
1205
- } catch {
1206
- return;
1207
- }
1208
- };
1209
- const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
1210
- const readHost = () => read((globals) => globals.location?.host);
1211
- //#endregion
1212
1206
  //#region ../shared/src/context-kind.ts
1213
1207
  const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
1214
1208
  const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
@@ -1253,6 +1247,178 @@ const admitContext = (context) => {
1253
1247
  };
1254
1248
  };
1255
1249
  //#endregion
1250
+ //#region ../shared/src/utf8-byte-length.ts
1251
+ const isHighSurrogate = (codeUnit) => codeUnit >= 55296 && codeUnit <= 56319;
1252
+ const isLowSurrogate = (codeUnit) => codeUnit >= 56320 && codeUnit <= 57343;
1253
+ const utf8ByteLength = (text) => {
1254
+ let bytes = 0;
1255
+ for (let i = 0; i < text.length; i++) {
1256
+ const codeUnit = text.charCodeAt(i);
1257
+ if (codeUnit <= 127) bytes += 1;
1258
+ else if (codeUnit <= 2047) bytes += 2;
1259
+ else if (isHighSurrogate(codeUnit) && i + 1 < text.length && isLowSurrogate(text.charCodeAt(i + 1))) {
1260
+ bytes += 4;
1261
+ i++;
1262
+ } else bytes += 3;
1263
+ }
1264
+ return bytes;
1265
+ };
1266
+ //#endregion
1267
+ //#region ../js-client-core/src/cached-values.ts
1268
+ const ENTRY_FORMAT_VERSION = 1;
1269
+ const DEFAULT_CONTEXT_KIND = "user";
1270
+ const CACHED_VALUES_STORAGE_KEY_PREFIX = "configdirector-sdk:cached-values:";
1271
+ const DEFAULT_CACHING_SIZE_LIMIT_BYTES = 256 * 1024;
1272
+ const planCaching = (connectionPlan, caching, platformProvider) => {
1273
+ if (connectionPlan.kind === "stay-offline") return {
1274
+ kind: "no cache",
1275
+ reason: "offline mode"
1276
+ };
1277
+ if (caching?.enabled === false) return {
1278
+ kind: "no cache",
1279
+ reason: "turned off"
1280
+ };
1281
+ const provider = caching?.provider ?? platformProvider?.(connectionPlan.clientSdkKey);
1282
+ if (!provider) return {
1283
+ kind: "no cache",
1284
+ reason: "no provider available"
1285
+ };
1286
+ return {
1287
+ kind: "cache",
1288
+ provider,
1289
+ clientSdkKey: connectionPlan.clientSdkKey
1290
+ };
1291
+ };
1292
+ const contextTagFor = (context, instanceId) => {
1293
+ const contextKind = context?.kind ?? DEFAULT_CONTEXT_KIND;
1294
+ if (context?.id != null) return {
1295
+ contextKind,
1296
+ contextId: context.id
1297
+ };
1298
+ return {
1299
+ contextKind,
1300
+ anonymousInstanceId: instanceId
1301
+ };
1302
+ };
1303
+ const sameContextTag = (stored, launch) => {
1304
+ if (stored.contextKind !== launch.contextKind) return false;
1305
+ if ("contextId" in stored && "contextId" in launch) return stored.contextId === launch.contextId;
1306
+ if ("anonymousInstanceId" in stored && "anonymousInstanceId" in launch) return stored.anonymousInstanceId === launch.anonymousInstanceId;
1307
+ return false;
1308
+ };
1309
+ const decideCachedValuesAdoption = (cachedValues, launchContextTag, hasReceivedLiveValues) => {
1310
+ if (hasReceivedLiveValues) return "live values already arrived";
1311
+ if (!sameContextTag(cachedValues.contextTag, launchContextTag)) return "context mismatch";
1312
+ return "adopt";
1313
+ };
1314
+ const isContextTag = (candidate) => {
1315
+ if (!candidate || typeof candidate !== "object") return false;
1316
+ const { contextKind, contextId, anonymousInstanceId } = candidate;
1317
+ if (typeof contextKind !== "string") return false;
1318
+ return typeof contextId === "string" || typeof anonymousInstanceId === "string";
1319
+ };
1320
+ const isConfigStateMap = (candidate) => {
1321
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return false;
1322
+ return Object.values(candidate).every((config) => config && typeof config === "object" && typeof config.id === "string" && typeof config.key === "string");
1323
+ };
1324
+ const isEntryFor = (clientSdkKey, candidate) => {
1325
+ if (!candidate || typeof candidate !== "object") return false;
1326
+ const entry = candidate;
1327
+ 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";
1328
+ };
1329
+ var CachedValuesStore = class {
1330
+ warned = /* @__PURE__ */ new Set();
1331
+ pendingRead;
1332
+ queuedCachedValues;
1333
+ writing = false;
1334
+ constructor(provider, clientSdkKey, logger) {
1335
+ this.provider = provider;
1336
+ this.clientSdkKey = clientSdkKey;
1337
+ this.logger = logger;
1338
+ }
1339
+ read() {
1340
+ this.pendingRead ??= this.readEntry();
1341
+ return this.pendingRead;
1342
+ }
1343
+ replace(cachedValues) {
1344
+ this.queuedCachedValues = cachedValues;
1345
+ this.writeQueued();
1346
+ }
1347
+ async readEntry() {
1348
+ let raw;
1349
+ try {
1350
+ raw = await this.provider.read();
1351
+ } catch (error) {
1352
+ this.warnOnce("read failed", "Could not read the values kept on the device, starting without them:", error);
1353
+ return { outcome: "failed" };
1354
+ }
1355
+ if (raw == null || raw === "") return { outcome: "nothing stored" };
1356
+ const entry = this.parseEntry(raw);
1357
+ if (!entry) {
1358
+ this.warnOnce("unreadable", "The values kept on the device could not be read and were removed, starting without them");
1359
+ await this.removeEntry();
1360
+ return { outcome: "unreadable" };
1361
+ }
1362
+ return {
1363
+ outcome: "stored",
1364
+ cachedValues: entry
1365
+ };
1366
+ }
1367
+ parseEntry(raw) {
1368
+ try {
1369
+ const candidate = JSON.parse(raw);
1370
+ return isEntryFor(this.clientSdkKey, candidate) ? candidate : void 0;
1371
+ } catch {
1372
+ return;
1373
+ }
1374
+ }
1375
+ async writeQueued() {
1376
+ if (this.writing) return;
1377
+ this.writing = true;
1378
+ try {
1379
+ while (this.queuedCachedValues) {
1380
+ const cachedValues = this.queuedCachedValues;
1381
+ this.queuedCachedValues = void 0;
1382
+ await this.writeEntry(cachedValues);
1383
+ }
1384
+ } finally {
1385
+ this.writing = false;
1386
+ }
1387
+ }
1388
+ async writeEntry(cachedValues) {
1389
+ const entry = {
1390
+ formatVersion: ENTRY_FORMAT_VERSION,
1391
+ clientSdkKey: this.clientSdkKey,
1392
+ ...cachedValues
1393
+ };
1394
+ const serialized = JSON.stringify(entry);
1395
+ const size = utf8ByteLength(serialized);
1396
+ const limit = this.provider.sizeLimit();
1397
+ if (size > limit) {
1398
+ 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`);
1399
+ await this.removeEntry();
1400
+ return;
1401
+ }
1402
+ try {
1403
+ await this.provider.write(serialized);
1404
+ } catch (error) {
1405
+ this.warnOnce("write failed", "Could not keep the values on the device:", error);
1406
+ }
1407
+ }
1408
+ async removeEntry() {
1409
+ try {
1410
+ await this.provider.remove();
1411
+ } catch (error) {
1412
+ this.warnOnce("remove failed", "Could not remove the values kept on the device:", error);
1413
+ }
1414
+ }
1415
+ warnOnce(warning, message, ...details) {
1416
+ if (this.warned.has(warning)) return;
1417
+ this.warned.add(warning);
1418
+ this.logger.warn(`[ConfigDirectorClient] ${message}`, ...details);
1419
+ }
1420
+ };
1421
+ //#endregion
1256
1422
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
1257
1423
  const MAX_EXPONENTIAL_DELAY = 9;
1258
1424
  var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
@@ -1270,11 +1436,15 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1270
1436
  readyPromise;
1271
1437
  readyResolve;
1272
1438
  currentContext;
1439
+ submittedContext;
1273
1440
  connectionPlan;
1274
1441
  clientSession;
1275
- static from(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions) {
1276
- if (typeof clientSdkKeyOrOptions === "object") return new DefaultConfigDirectorClient(liveTelemetryClient, void 0, sdkOptions, clientSdkKeyOrOptions);
1277
- return new DefaultConfigDirectorClient(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions);
1442
+ cachedValuesStore;
1443
+ servingCachedValues = false;
1444
+ cachedValuesAdoptionStarted = false;
1445
+ static from(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions, internalClientOptions) {
1446
+ if (typeof clientSdkKeyOrOptions === "object") return new DefaultConfigDirectorClient(liveTelemetryClient, void 0, sdkOptions, clientSdkKeyOrOptions, internalClientOptions);
1447
+ return new DefaultConfigDirectorClient(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions, internalClientOptions);
1278
1448
  }
1279
1449
  constructor(liveTelemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1280
1450
  this.logger = clientOptions?.logger ?? createDefaultLogger$1();
@@ -1282,10 +1452,12 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1282
1452
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
1283
1453
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
1284
1454
  this.connectionPlan = planConnection(clientSdkKey, clientOptions?.connection, urlFactory);
1455
+ this.cachedValuesStore = this.buildCachedValuesStore(planCaching(this.connectionPlan, clientOptions?.caching, internalClientOptions?.platformCachingProvider));
1285
1456
  this.telemetryClient = this.buildTelemetryClient(liveTelemetryClient, clientOptions);
1286
1457
  this.transport = internalClientOptions?.transport ?? this.buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions);
1287
1458
  this.transport.on("configSetReceived", (configSet) => {
1288
1459
  this.hasReceivedConfigSet = true;
1460
+ this.servingCachedValues = false;
1289
1461
  this.readyResolve?.();
1290
1462
  const removedKeys = this.findKeysRemovedBy(configSet);
1291
1463
  if (!this.configSet || configSet.kind == "full") this.configSet = configSet;
@@ -1293,6 +1465,7 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1293
1465
  ...this.configSet.configs,
1294
1466
  ...configSet.configs
1295
1467
  };
1468
+ this.keepValuesOnTheDevice(this.configSet);
1296
1469
  const keys = Object.keys(configSet.configs);
1297
1470
  this.emit("configsUpdated", {
1298
1471
  keys,
@@ -1314,6 +1487,80 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1314
1487
  this.registerHandler("configsUpdated", clientOptions?.hooks?.configsUpdated);
1315
1488
  this.registerHandler("contextUpdated", clientOptions?.hooks?.contextUpdated);
1316
1489
  this.registerHandler("connectionError", clientOptions?.hooks?.connectionError);
1490
+ this.registerHandler("cachedValuesLoaded", clientOptions?.hooks?.cachedValuesLoaded);
1491
+ }
1492
+ buildCachedValuesStore(cachingPlan) {
1493
+ switch (cachingPlan.kind) {
1494
+ case "no cache":
1495
+ this.logger.debug(`[ConfigDirectorClient] No values are kept on the device: ${cachingPlan.reason}`);
1496
+ return;
1497
+ case "cache": {
1498
+ const store = new CachedValuesStore(cachingPlan.provider, cachingPlan.clientSdkKey, this.logger);
1499
+ store.read();
1500
+ return store;
1501
+ }
1502
+ }
1503
+ }
1504
+ startCachedValuesAdoption() {
1505
+ const store = this.cachedValuesStore;
1506
+ if (!store || this.cachedValuesAdoptionStarted) return;
1507
+ this.cachedValuesAdoptionStarted = true;
1508
+ this.adoptCachedValues(store);
1509
+ }
1510
+ async adoptCachedValues(store) {
1511
+ const read = await store.read();
1512
+ switch (read.outcome) {
1513
+ case "nothing stored":
1514
+ case "unreadable":
1515
+ case "failed": return;
1516
+ case "stored": break;
1517
+ }
1518
+ const { instanceId } = await this.clientSession;
1519
+ const launchContextTag = contextTagFor(this.submittedContext, instanceId);
1520
+ const adoption = decideCachedValuesAdoption(read.cachedValues, launchContextTag, this.hasReceivedConfigSet);
1521
+ switch (adoption) {
1522
+ case "adopt":
1523
+ this.serveCachedValues(read.cachedValues);
1524
+ return;
1525
+ case "live values already arrived":
1526
+ case "context mismatch":
1527
+ this.logger.debug(`[ConfigDirectorClient] The values kept on the device were not adopted: ${adoption}`);
1528
+ return;
1529
+ }
1530
+ }
1531
+ serveCachedValues(cachedValues) {
1532
+ const { environmentId, projectId, configs } = cachedValues;
1533
+ this.configSet = {
1534
+ environmentId,
1535
+ projectId,
1536
+ configs,
1537
+ kind: "full"
1538
+ };
1539
+ this.servingCachedValues = true;
1540
+ const keys = Object.keys(configs);
1541
+ this.emit("cachedValuesLoaded", {});
1542
+ this.emit("configsUpdated", {
1543
+ keys,
1544
+ removedKeys: []
1545
+ });
1546
+ this.updateWatchers(keys);
1547
+ this.logger.debug("[ConfigDirectorClient] Serving the values kept on the device until live values arrive:", { keys });
1548
+ }
1549
+ keepValuesOnTheDevice(configSet) {
1550
+ const store = this.cachedValuesStore;
1551
+ if (!store) return;
1552
+ const context = this.submittedContext;
1553
+ const receivedAt = Date.now();
1554
+ this.clientSession.then(({ instanceId }) => store.replace({
1555
+ contextTag: contextTagFor(context, instanceId),
1556
+ environmentId: configSet.environmentId,
1557
+ projectId: configSet.projectId,
1558
+ configs: configSet.configs,
1559
+ receivedAt
1560
+ }));
1561
+ }
1562
+ servedReason(reason) {
1563
+ return this.servingCachedValues && reason === "found-match" ? "cached" : reason;
1317
1564
  }
1318
1565
  registerHandler(event, handler) {
1319
1566
  if (!handler) return;
@@ -1407,6 +1654,8 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1407
1654
  return this.currentContext;
1408
1655
  }
1409
1656
  async connectToTransport(context, caller) {
1657
+ this.submittedContext = context;
1658
+ if (caller === "initialization") this.startCachedValuesAdoption();
1410
1659
  try {
1411
1660
  this.ready = false;
1412
1661
  this.readyPromise = new Promise((resolve) => {
@@ -1507,6 +1756,7 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1507
1756
  return defaultValue;
1508
1757
  }
1509
1758
  const parseResult = parseConfigValue(configState, defaultValue);
1759
+ const reason = this.servedReason(parseResult.reason);
1510
1760
  this.telemetryClient.evaluatedConfig({
1511
1761
  contextId: this.currentContext?.id,
1512
1762
  type: configState.type,
@@ -1516,14 +1766,14 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1516
1766
  evaluatedValue: parseResult.parsedValue,
1517
1767
  evaluatedValueId: parseResult.parsedValueId,
1518
1768
  usedDefault: parseResult.usedDefault,
1519
- evaluationReason: parseResult.reason
1769
+ evaluationReason: reason
1520
1770
  });
1521
1771
  this.dispatchEvaluationEvent({
1522
1772
  key: configKey,
1523
1773
  value: parseResult.parsedValue,
1524
1774
  valueId: parseResult.parsedValueId ?? void 0,
1525
1775
  isDefaultValue: parseResult.usedDefault,
1526
- reason: parseResult.reason,
1776
+ reason,
1527
1777
  context: this.context
1528
1778
  });
1529
1779
  this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
@@ -1879,6 +2129,25 @@ const generateValueId = async (v) => {
1879
2129
  return toBase62(new Uint8Array(hashBuffer, 0, 16), BASE62_LENGTH);
1880
2130
  };
1881
2131
  //#endregion
2132
+ //#region ../js-browser-client/src/local-storage-caching-provider.ts
2133
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2134
+ const storageKeyFor = async (clientSdkKey) => {
2135
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(clientSdkKey));
2136
+ return CACHED_VALUES_STORAGE_KEY_PREFIX + toHex(new Uint8Array(digest));
2137
+ };
2138
+ const subtleCryptoAvailable = () => typeof crypto !== "undefined" && crypto.subtle != null;
2139
+ const localStorageCachingProvider = (clientSdkKey) => {
2140
+ const storage = localStorageIfAvailable();
2141
+ if (!storage || !subtleCryptoAvailable()) return;
2142
+ const storageKey = storageKeyFor(clientSdkKey);
2143
+ return {
2144
+ read: async () => storage.getItem(await storageKey),
2145
+ write: async (value) => storage.setItem(await storageKey, value),
2146
+ remove: async () => storage.removeItem(await storageKey),
2147
+ sizeLimit: () => DEFAULT_CACHING_SIZE_LIMIT_BYTES
2148
+ };
2149
+ };
2150
+ //#endregion
1882
2151
  //#region ../js-browser-client/src/BrowserConfigDirectorClient.ts
1883
2152
  /**
1884
2153
  * Creates a browser client. With a client SDK key it connects to ConfigDirector; with
@@ -1901,13 +2170,26 @@ const createBrowserClient = (clientSdkKeyOrOptions, sdkOptions, clientOptions) =
1901
2170
  baseUrl,
1902
2171
  urlFactory: defaultUrlFactory,
1903
2172
  valueIdGenerator: generateValueId
1904
- }), clientSdkKeyOrOptions, sdkOptions, clientOptions);
2173
+ }), clientSdkKeyOrOptions, sdkOptions, clientOptions, { platformCachingProvider: localStorageCachingProvider });
1905
2174
  };
1906
2175
  //#endregion
1907
2176
  //#region ../js-client-core/src/installedTestClient.ts
1908
2177
  const INSTALLED_TEST_CLIENT_SLOT = Symbol.for("@configdirector/installed-test-client");
1909
2178
  const readInstalledTestClient = () => globalThis[INSTALLED_TEST_CLIENT_SLOT];
1910
2179
  //#endregion
2180
+ //#region ../shared/src/browser-half.ts
2181
+ const decideBrowserHalfSource = (browserHalf, serverConnectionMode) => {
2182
+ if (browserHalf.mode !== void 0 || browserHalf.values !== void 0) return "as-given";
2183
+ if (serverConnectionMode === "offline") return "inherit-offline";
2184
+ return "connect";
2185
+ };
2186
+ //#endregion
2187
+ //#region src/runtime/app/composables/useConfigDirectorClient.ts
2188
+ const useConfigDirectorClient = () => {
2189
+ const { $configDirectorClient } = useNuxtApp();
2190
+ return { client: $configDirectorClient };
2191
+ };
2192
+ //#endregion
1911
2193
  //#region ../shared/src/deepEqual.ts
1912
2194
  const deepEqual = (left, right) => {
1913
2195
  if (Object.is(left, right)) return true;
@@ -1933,17 +2215,17 @@ const decideMountAction = (client, context) => {
1933
2215
  return "none";
1934
2216
  };
1935
2217
  //#endregion
1936
- //#region ../shared/src/browser-half.ts
1937
- const decideBrowserHalfSource = (browserHalf, serverConnectionMode) => {
1938
- if (browserHalf.mode !== void 0 || browserHalf.values !== void 0) return "as-given";
1939
- if (serverConnectionMode === "offline") return "inherit-offline";
1940
- return "connect";
1941
- };
1942
- //#endregion
1943
- //#region src/runtime/app/composables/useConfigDirectorClient.ts
1944
- const useConfigDirectorClient = () => {
1945
- const { $configDirectorClient } = useNuxtApp();
1946
- return { client: $configDirectorClient };
2218
+ //#region src/runtime/client-startup.ts
2219
+ const decideStartupAction = (client, context, waitsForFirstContext) => {
2220
+ const mountAction = decideMountAction(client, context);
2221
+ if (!waitsForFirstContext) return mountAction;
2222
+ if (mountAction === "initialize" && context === void 0) return "waitForContext";
2223
+ if (mountAction === "none" && client.isInitializing) return "firstStartInFlight";
2224
+ return mountAction;
2225
+ };
2226
+ const decideContextUpdateAction = (client, context, waitsForFirstContext) => {
2227
+ if (waitsForFirstContext && decideMountAction(client, context) === "initialize") return "startClient";
2228
+ return "updateContext";
1947
2229
  };
1948
2230
  //#endregion
1949
2231
  //#region src/runtime/app/composables/useConfigDirectorContext.ts
@@ -1954,6 +2236,14 @@ const getEffectiveTimeout = (option) => {
1954
2236
  const useConfigDirectorContext = () => {
1955
2237
  const state = useState("configdirector-context");
1956
2238
  const { client } = useConfigDirectorClient();
2239
+ const { $configDirectorClientStartup } = useNuxtApp();
2240
+ const startup = $configDirectorClientStartup;
2241
+ const handContextToClient = (rawContext) => {
2242
+ switch (decideContextUpdateAction(client, rawContext, startup.waitsForFirstContext)) {
2243
+ case "startClient": return startup.start(rawContext);
2244
+ case "updateContext": return client.updateContext(rawContext);
2245
+ }
2246
+ };
1957
2247
  const updateContext = async (context, options) => {
1958
2248
  const rawContext = toRaw(context);
1959
2249
  const oldContext = JSON.stringify(state.value);
@@ -1963,7 +2253,7 @@ const useConfigDirectorContext = () => {
1963
2253
  const timeout = getEffectiveTimeout(options?.timeoutMilliseconds);
1964
2254
  let timer;
1965
2255
  try {
1966
- await Promise.race([client.updateContext(rawContext), new Promise((resolve) => {
2256
+ await Promise.race([handContextToClient(rawContext), new Promise((resolve) => {
1967
2257
  timer = setTimeout(() => resolve(), timeout);
1968
2258
  })]);
1969
2259
  } finally {
@@ -1990,28 +2280,51 @@ const OFFLINE_VALUES_ROUTE = "/_configdirector/offline-values";
1990
2280
  //#region src/runtime/plugin.client.ts
1991
2281
  const DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS = 2e3;
1992
2282
  var plugin_client_default = defineNuxtPlugin(async (nuxtApp) => {
1993
- const runtimeConfig = useRuntimeConfig();
1994
- const logger = createDefaultLogger(runtimeConfig.public?.configdirector?.logLevel);
2283
+ const configdirector = useRuntimeConfig().public?.configdirector;
2284
+ const logger = createDefaultLogger(configdirector?.logLevel);
1995
2285
  const installedTestClient = readInstalledTestClient();
1996
- const client = installedTestClient ?? await buildClient(runtimeConfig.public?.configdirector, logger);
2286
+ const { client, runsOffline } = installedTestClient ? {
2287
+ client: installedTestClient,
2288
+ runsOffline: false
2289
+ } : await buildClient(configdirector, logger);
1997
2290
  logger.debug(installedTestClient ? "Installed ConfigDirector Nuxt plugin with a test client" : "Installed ConfigDirector Nuxt plugin");
1998
2291
  const readyStatus = shallowRef(client.isReady ? "ready" : "loading");
1999
2292
  client.on("clientReady", () => {
2000
2293
  readyStatus.value = "ready";
2001
2294
  });
2295
+ const settleReadyStatus = () => {
2296
+ if (!client.isReady) readyStatus.value = "default";
2297
+ };
2298
+ const startup = {
2299
+ waitsForFirstContext: configdirector?.deferInitialization === true && !runsOffline,
2300
+ start: async (context) => {
2301
+ await client.initialize(context);
2302
+ settleReadyStatus();
2303
+ }
2304
+ };
2002
2305
  nuxtApp.hooks.hook("app:created", async () => {
2003
2306
  const { context } = useConfigDirectorContext();
2004
2307
  const rawContext = toRaw(context.value);
2005
- const mountAction = decideMountAction(client, rawContext);
2006
- logger.debug(`Browser client mount action: ${mountAction}`);
2007
- if (mountAction === "initialize") await client.initialize(rawContext);
2008
- else if (mountAction === "updateContext" && rawContext !== void 0) await client.updateContext(rawContext);
2009
- if (!client.isReady) readyStatus.value = "default";
2010
- logger.debug(`Browser client mount action awaited, ready status: ${client.isReady}`);
2308
+ const startupAction = decideStartupAction(client, rawContext, startup.waitsForFirstContext);
2309
+ logger.debug(`Browser client startup action: ${startupAction}`);
2310
+ switch (startupAction) {
2311
+ case "waitForContext": return;
2312
+ case "firstStartInFlight": return;
2313
+ case "initialize":
2314
+ await client.initialize(rawContext);
2315
+ break;
2316
+ case "updateContext":
2317
+ await client.updateContext(rawContext);
2318
+ break;
2319
+ case "none": break;
2320
+ }
2321
+ settleReadyStatus();
2322
+ logger.debug(`Browser client startup action awaited, ready status: ${client.isReady}`);
2011
2323
  });
2012
2324
  return { provide: {
2013
2325
  configDirectorClient: client,
2014
- configDirectorClientReadyStatus: readonly(readyStatus)
2326
+ configDirectorClientReadyStatus: readonly(readyStatus),
2327
+ configDirectorClientStartup: startup
2015
2328
  } };
2016
2329
  });
2017
2330
  const buildClient = async (configdirector, logger) => {
@@ -2021,23 +2334,26 @@ const buildClient = async (configdirector, logger) => {
2021
2334
  logger.error(message);
2022
2335
  throw new ConfigDirectorInitializationError(message);
2023
2336
  }
2024
- return createBrowserClient(configdirector?.clientSdkKey || void 0, {
2025
- sdkName: "nuxt-sdk",
2026
- sdkVersion: "1.11.0"
2027
- }, {
2028
- metadata: {
2029
- appName: configdirector?.appName,
2030
- appVersion: configdirector?.appVersion
2031
- },
2032
- connection: {
2033
- url: configdirector?.baseUrl || void 0,
2034
- mode: browserHalf.mode,
2035
- values: browserHalf.values,
2036
- pollingInterval: configdirector?.connection?.pollingInterval || void 0,
2037
- timeout: configdirector?.connection?.timeout || DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS
2038
- },
2039
- logger
2040
- });
2337
+ return {
2338
+ client: createBrowserClient(configdirector?.clientSdkKey || void 0, {
2339
+ sdkName: "nuxt-sdk",
2340
+ sdkVersion: "1.12.0"
2341
+ }, {
2342
+ metadata: {
2343
+ appName: configdirector?.appName,
2344
+ appVersion: configdirector?.appVersion
2345
+ },
2346
+ connection: {
2347
+ url: configdirector?.baseUrl || void 0,
2348
+ mode: browserHalf.mode,
2349
+ values: browserHalf.values,
2350
+ pollingInterval: configdirector?.connection?.pollingInterval || void 0,
2351
+ timeout: configdirector?.connection?.timeout || DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS
2352
+ },
2353
+ logger
2354
+ }),
2355
+ runsOffline: browserHalf.mode === "offline"
2356
+ };
2041
2357
  };
2042
2358
  const browserHalfConnectionOf = async (configdirector, logger) => {
2043
2359
  const givenConnection = {