@configdirector/nuxt-sdk 1.9.2 → 1.11.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 (40) hide show
  1. package/dist/module.d.mts +76 -15
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +13 -4
  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 +2 -1
  11. package/dist/runtime/app/composables/useConfigDirectorStatus.d.mts +1 -1
  12. package/dist/runtime/app/composables/useConfigDirectorStatus.d.ts +1 -1
  13. package/dist/runtime/app/composables/useConfigDirectorValue.d.mts +2 -2
  14. package/dist/runtime/app/composables/useConfigDirectorValue.d.ts +2 -2
  15. package/dist/runtime/context-kind-D6YBNij2.mjs +45 -0
  16. package/dist/runtime/native-values-BV2k_xR2.d.mts +83 -0
  17. package/dist/runtime/native-values-DZJjFBvk.mjs +116 -0
  18. package/dist/runtime/nitro/middleware.mjs +5 -1
  19. package/dist/runtime/nitro/offline-values.d.mts +7 -0
  20. package/dist/runtime/nitro/offline-values.d.ts +7 -0
  21. package/dist/runtime/nitro/offline-values.mjs +15 -0
  22. package/dist/runtime/nitro/plugin.d.mts +3 -1
  23. package/dist/runtime/nitro/plugin.d.ts +3 -1
  24. package/dist/runtime/nitro/plugin.mjs +329 -92
  25. package/dist/runtime/plugin.client.mjs +553 -118
  26. package/dist/runtime/plugin.server.mjs +50 -6
  27. package/dist/runtime/server/composables/useConfigDirectorServerClient.d.mts +6 -0
  28. package/dist/runtime/server/composables/useConfigDirectorServerClient.d.ts +6 -0
  29. package/dist/runtime/server/composables/{useConfigDirectorClient.mjs → useConfigDirectorServerClient.mjs} +3 -3
  30. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.mts +1 -1
  31. package/dist/runtime/server/composables/useConfigDirectorServerHooks.d.ts +1 -1
  32. package/dist/runtime/{types-Bgx2-3dY.d.mts → types-CS7AMNWe.d.mts} +1 -1
  33. package/dist/runtime/{types-CvmjergQ.d.mts → types-Dt3bENKf.d.mts} +13 -5
  34. package/dist/testing.d.mts +49 -27
  35. package/dist/testing.mjs +429 -185
  36. package/package.json +1 -1
  37. package/dist/runtime/server/composables/useConfigDirectorClient.d.mts +0 -6
  38. package/dist/runtime/server/composables/useConfigDirectorClient.d.ts +0 -6
  39. package/dist/runtime/types-CHmsCChN.d.mts +0 -60
  40. /package/dist/runtime/{types-CkzBXJwA.d.mts → types-C30VJ25l.d.mts} +0 -0
package/dist/testing.mjs CHANGED
@@ -49,6 +49,13 @@ var ConfigDirectorValidationError = class ConfigDirectorValidationError extends
49
49
  Object.setPrototypeOf(this, ConfigDirectorValidationError.prototype);
50
50
  }
51
51
  };
52
+ var ConfigDirectorInitializationError = class ConfigDirectorInitializationError extends Error {
53
+ name = "ConfigDirectorInitializationError";
54
+ constructor(message) {
55
+ super(message);
56
+ Object.setPrototypeOf(this, ConfigDirectorInitializationError.prototype);
57
+ }
58
+ };
52
59
  //#endregion
53
60
  //#region ../eventsource/src/errors.ts
54
61
  var StreamClosedError = class StreamClosedError extends Error {
@@ -383,6 +390,7 @@ var StreamingTransport = class {
383
390
  eventEmitter = new Emitter();
384
391
  url;
385
392
  timeoutTimer;
393
+ connectionGeneration = 0;
386
394
  constructor(options) {
387
395
  this.options = options;
388
396
  this.options = options;
@@ -390,6 +398,9 @@ var StreamingTransport = class {
390
398
  this.url = options.resolveUrl("client/sse/v1", options.baseUrl);
391
399
  }
392
400
  async connect(context, timeout) {
401
+ const generation = ++this.connectionGeneration;
402
+ const clientSession = await this.options.clientSession;
403
+ if (generation !== this.connectionGeneration) return this;
393
404
  if (this.eventSource) this.close();
394
405
  const eventSourcePromise = new Promise((resolve, reject) => {
395
406
  this.eventSource = createEventSourceClient({
@@ -401,7 +412,8 @@ var StreamingTransport = class {
401
412
  givenContext: context,
402
413
  metaContext: this.options.metaContext,
403
414
  clientSdkKey: this.options.clientSdkKey,
404
- instanceId: this.options.instanceId
415
+ instanceId: clientSession.instanceId,
416
+ ephemeral: clientSession.ephemeral
405
417
  }),
406
418
  onMessage: ({ data }) => {
407
419
  this.dispatchMessage(data);
@@ -466,6 +478,7 @@ var StreamingTransport = class {
466
478
  this.eventEmitter.clear();
467
479
  }
468
480
  close() {
481
+ this.connectionGeneration++;
469
482
  this.eventSource?.close();
470
483
  clearTimeout(this.timeoutTimer);
471
484
  }
@@ -475,6 +488,183 @@ var StreamingTransport = class {
475
488
  }
476
489
  };
477
490
  //#endregion
491
+ //#region ../shared/src/native-values.ts
492
+ const encodeNativeValue = (key, value) => {
493
+ if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a value cannot be empty.");
494
+ return encode(key, value);
495
+ };
496
+ const encode = (key, value) => {
497
+ switch (typeof value) {
498
+ case "boolean": return {
499
+ type: "boolean",
500
+ value: value ? "true" : "false"
501
+ };
502
+ case "number": return encodeNumber(key, value);
503
+ case "string": return {
504
+ type: "string",
505
+ value
506
+ };
507
+ case "object": if (value !== null) {
508
+ assertJsonContents(key, value, "");
509
+ return {
510
+ type: "json",
511
+ value: JSON.stringify(value)
512
+ };
513
+ }
514
+ }
515
+ throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)}. Use a boolean, a number, a string, a plain object, or an array.`);
516
+ };
517
+ const encodeNumber = (key, value) => {
518
+ if (!Number.isFinite(value)) throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)} is not a finite number.`);
519
+ if (Number.isInteger(value)) {
520
+ if (!Number.isSafeInteger(value)) throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${value} is not a safe integer, so the SDK could not read it back exactly.`);
521
+ return {
522
+ type: "integer",
523
+ value: String(value)
524
+ };
525
+ }
526
+ return {
527
+ type: "float",
528
+ value: toPlainDecimal(value)
529
+ };
530
+ };
531
+ const toPlainDecimal = (value) => {
532
+ const text = String(value);
533
+ const exponentIndex = text.indexOf("e-");
534
+ if (exponentIndex < 0) return text;
535
+ const sign = text.startsWith("-") ? "-" : "";
536
+ const [integerDigits = "", fractionDigits = ""] = text.slice(sign.length, exponentIndex).split(".");
537
+ const leadingZeros = Number(text.slice(exponentIndex + 2)) - integerDigits.length;
538
+ return `${sign}0.${"0".repeat(leadingZeros)}${integerDigits}${fractionDigits}`;
539
+ };
540
+ const assertJsonContents = (key, value, path) => {
541
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
542
+ if (typeof value === "number") {
543
+ if (!Number.isFinite(value)) throw invalidJsonContents(key, path, `${describe(value)} is not a finite number`);
544
+ return;
545
+ }
546
+ if (Array.isArray(value)) {
547
+ value.forEach((item, index) => assertJsonContents(key, item, `${path}[${index}]`));
548
+ return;
549
+ }
550
+ if (isPlainObject(value)) {
551
+ for (const [property, item] of Object.entries(value)) assertJsonContents(key, item, path ? `${path}.${property}` : property);
552
+ return;
553
+ }
554
+ throw invalidJsonContents(key, path, `${describe(value)} cannot be encoded as JSON`);
555
+ };
556
+ const invalidJsonContents = (key, path, problem) => new ConfigDirectorValidationError(`Invalid value for '${key}'${path ? ` at '${path}'` : ""}: ${problem}. JSON contents can hold booleans, finite numbers, strings, null, arrays, and plain objects.`);
557
+ const isPlainObject = (value) => {
558
+ if (typeof value !== "object" || value === null) return false;
559
+ const prototype = Object.getPrototypeOf(value);
560
+ return prototype === Object.prototype || prototype === null;
561
+ };
562
+ const describe = (value) => {
563
+ if (value === null) return "null";
564
+ if (typeof value === "function") return "a function";
565
+ if (typeof value === "object") return `a ${value.constructor?.name ?? "object"} instance`;
566
+ if (typeof value === "symbol") return "a symbol";
567
+ return String(value);
568
+ };
569
+ //#endregion
570
+ //#region ../js-client-core/src/native-values.ts
571
+ const nativeValueConfigState = (id, key, value, valueId) => {
572
+ const encoded = encodeNativeValue(key, value);
573
+ return {
574
+ id,
575
+ key,
576
+ type: encoded.type,
577
+ value: encoded.value,
578
+ valueId
579
+ };
580
+ };
581
+ //#endregion
582
+ //#region ../js-client-core/src/OfflineTransport.ts
583
+ const OFFLINE_ENVIRONMENT = {
584
+ environmentId: "offline",
585
+ projectId: "offline"
586
+ };
587
+ var OfflineTransport = class {
588
+ emitter = new Emitter();
589
+ constructor(source) {
590
+ this.source = source;
591
+ }
592
+ async connect() {
593
+ const configs = loadOfflineConfigs(this.source);
594
+ this.emitter.emit("configSetReceived", {
595
+ ...OFFLINE_ENVIRONMENT,
596
+ kind: "full",
597
+ configs
598
+ });
599
+ return this;
600
+ }
601
+ on = (name, handler) => this.emitter.on(name, handler);
602
+ off = (name, handler) => this.emitter.off(name, handler);
603
+ clear() {
604
+ this.emitter.clear();
605
+ }
606
+ close() {}
607
+ dispose() {
608
+ this.clear();
609
+ }
610
+ };
611
+ const loadOfflineConfigs = (source) => {
612
+ switch (source.kind) {
613
+ case "in-code-defaults": return {};
614
+ case "values": return encodeOfflineValues(source.values);
615
+ case "rejected": throw new ConfigDirectorInitializationError(source.message);
616
+ }
617
+ };
618
+ const encodeOfflineValues = (values) => {
619
+ try {
620
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, nativeValueConfigState(`offline:${key}`, key, value, void 0)]));
621
+ } catch (error) {
622
+ if (error instanceof ConfigDirectorValidationError) throw new ConfigDirectorInitializationError(`The offline values could not be loaded. ${error.message}`);
623
+ throw error;
624
+ }
625
+ };
626
+ //#endregion
627
+ //#region ../shared/src/constants.ts
628
+ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
629
+ //#endregion
630
+ //#region ../js-client-core/src/connection-plan.ts
631
+ const planConnection = (clientSdkKey, connection, urlFactory) => {
632
+ if (connection?.mode === "offline") return {
633
+ kind: "stay-offline",
634
+ source: offlineSourceOf(connection)
635
+ };
636
+ const mode = connection?.mode ?? "streaming";
637
+ if (!clientSdkKey || clientSdkKey.trim().length == 0) throw new ConfigDirectorValidationError("No client SDK key was provided, the client cannot be instantiated without a valid client SDK key");
638
+ if (connection?.values !== void 0) return {
639
+ kind: "stay-offline",
640
+ source: {
641
+ kind: "rejected",
642
+ message: `The connection option 'values' applies only when the connection mode is 'offline', but the mode is '${mode}'.`
643
+ }
644
+ };
645
+ return {
646
+ kind: "connect",
647
+ mode,
648
+ clientSdkKey,
649
+ baseUrl: baseUrlOf(connection?.url, urlFactory)
650
+ };
651
+ };
652
+ const baseUrlOf = (url, urlFactory) => {
653
+ if (!url) return CLIENT_BASE_URL;
654
+ try {
655
+ return urlFactory(url);
656
+ } catch (error) {
657
+ throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
658
+ }
659
+ };
660
+ const offlineSourceOf = (connection) => {
661
+ if (connection.values !== void 0) return {
662
+ kind: "values",
663
+ values: connection.values
664
+ };
665
+ return { kind: "in-code-defaults" };
666
+ };
667
+ //#endregion
478
668
  //#region ../shared/src/value-parser.ts
479
669
  const getRequestedType = (defaultValue) => {
480
670
  const baseType = typeof defaultValue;
@@ -694,6 +884,14 @@ const createDefaultLogger = (level, messageDecorator) => {
694
884
  return new DefaultConsoleLogger(level ?? "warn", messageDecorator ?? new LogMessageDecorator());
695
885
  };
696
886
  //#endregion
887
+ //#region ../js-client-core/src/telemetry/discarding.ts
888
+ const discardingTelemetryClient = {
889
+ updateContext: async () => {},
890
+ evaluatedConfig: () => {},
891
+ recordInvalidContext: () => {},
892
+ close: async () => {}
893
+ };
894
+ //#endregion
697
895
  //#region ../shared/src/url.ts
698
896
  const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
699
897
  //#endregion
@@ -722,10 +920,11 @@ const generateInstanceId = () => {
722
920
  });
723
921
  };
724
922
  //#endregion
725
- //#region ../js-client-core/src/instance-id.ts
923
+ //#region ../js-client-core/src/client-session.ts
726
924
  const STORAGE_KEY = "configdirector-sdk:instance-id";
727
925
  const RECORD_VERSION = 1;
728
926
  const MAX_AGE_MS = 360 * 60 * 1e3;
927
+ const STORAGE_DEADLINE_MS = 1e3;
729
928
  const CHECKSUM_SALT = "configdirector.instanceId.v1";
730
929
  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}$/;
731
930
  const fnv1a = (input, offsetBasis) => {
@@ -741,7 +940,7 @@ const checksumFor = (id, createdAt) => {
741
940
  const low = fnv1a(payload, 2166136261);
742
941
  return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
743
942
  };
744
- const getLocalStorage = () => {
943
+ const localStorageIfAvailable = () => {
745
944
  try {
746
945
  return globalThis.localStorage ?? void 0;
747
946
  } catch {
@@ -756,34 +955,37 @@ const isUsableRecord = (record) => {
756
955
  if (!UUID_V4_PATTERN.test(id)) return false;
757
956
  return checksum === checksumFor(id, createdAt);
758
957
  };
759
- const readStoredInstanceId = (storage, logger) => {
958
+ const readStoredInstanceId = async (storage, logger) => {
760
959
  let raw;
761
960
  try {
762
- raw = storage.getItem(STORAGE_KEY);
961
+ raw = await storage.getItem(STORAGE_KEY);
763
962
  } catch (error) {
764
- logger?.debug("[ConfigDirectorClient] Could not read the stored instance id, generating a new one", error);
765
- return;
963
+ logger.debug("[ConfigDirectorClient] Could not read the stored instance id, using an ephemeral one", error);
964
+ return { outcome: "failed" };
766
965
  }
767
- if (!raw) return;
966
+ if (typeof raw !== "string" || raw === "") return { outcome: "nothing usable" };
768
967
  let record;
769
968
  try {
770
969
  record = JSON.parse(raw);
771
970
  } catch {
772
- logger?.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
773
- return;
971
+ logger.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
972
+ return { outcome: "nothing usable" };
774
973
  }
775
974
  if (!isUsableRecord(record)) {
776
- logger?.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
777
- return;
975
+ logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
976
+ return { outcome: "nothing usable" };
778
977
  }
779
978
  const age = Date.now() - record.createdAt;
780
979
  if (age < 0 || age >= MAX_AGE_MS) {
781
- logger?.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
782
- return;
980
+ logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
981
+ return { outcome: "nothing usable" };
783
982
  }
784
- return record.id;
983
+ return {
984
+ outcome: "stored",
985
+ instanceId: record.id
986
+ };
785
987
  };
786
- const storeInstanceId = (storage, id, logger) => {
988
+ const persistInstanceId = async (storage, id, logger) => {
787
989
  const createdAt = Date.now();
788
990
  const record = {
789
991
  v: RECORD_VERSION,
@@ -792,19 +994,54 @@ const storeInstanceId = (storage, id, logger) => {
792
994
  checksum: checksumFor(id, createdAt)
793
995
  };
794
996
  try {
795
- storage.setItem(STORAGE_KEY, JSON.stringify(record));
997
+ await storage.setItem(STORAGE_KEY, JSON.stringify(record));
998
+ return true;
796
999
  } catch (error) {
797
- logger?.debug("[ConfigDirectorClient] Could not persist the instance id", error);
1000
+ logger.debug("[ConfigDirectorClient] Could not persist the instance id, using it as an ephemeral one", error);
1001
+ return false;
798
1002
  }
799
1003
  };
800
- const resolveInstanceId = (logger) => {
801
- const storage = getLocalStorage();
802
- if (!storage) return generateInstanceId();
803
- const storedInstanceId = readStoredInstanceId(storage, logger);
804
- if (storedInstanceId) return storedInstanceId;
805
- const instanceId = generateInstanceId();
806
- storeInstanceId(storage, instanceId, logger);
807
- return instanceId;
1004
+ const settleFromStorage = async (storage, provisionalId, deadline, logger) => {
1005
+ const read = await readStoredInstanceId(storage, logger);
1006
+ switch (read.outcome) {
1007
+ case "stored": return { instanceId: read.instanceId };
1008
+ case "failed": return {
1009
+ instanceId: provisionalId,
1010
+ ephemeral: true
1011
+ };
1012
+ case "nothing usable": break;
1013
+ }
1014
+ if (deadline.passed) return {
1015
+ instanceId: provisionalId,
1016
+ ephemeral: true
1017
+ };
1018
+ return await persistInstanceId(storage, provisionalId, logger) ? { instanceId: provisionalId } : {
1019
+ instanceId: provisionalId,
1020
+ ephemeral: true
1021
+ };
1022
+ };
1023
+ const resolveClientSession = (logger, storage = localStorageIfAvailable()) => {
1024
+ const provisionalId = generateInstanceId();
1025
+ if (!storage) {
1026
+ logger.debug("[ConfigDirectorClient] No instance id storage is available, using an ephemeral instance id");
1027
+ return Promise.resolve({
1028
+ instanceId: provisionalId,
1029
+ ephemeral: true
1030
+ });
1031
+ }
1032
+ const deadline = { passed: false };
1033
+ let deadlineTimer;
1034
+ const fallbackAtDeadline = new Promise((resolve) => {
1035
+ deadlineTimer = setTimeout(() => {
1036
+ deadline.passed = true;
1037
+ logger.warn(`[ConfigDirectorClient] The instance id storage did not respond within ${STORAGE_DEADLINE_MS}ms, using an ephemeral instance id`);
1038
+ resolve({
1039
+ instanceId: provisionalId,
1040
+ ephemeral: true
1041
+ });
1042
+ }, STORAGE_DEADLINE_MS);
1043
+ });
1044
+ return Promise.race([settleFromStorage(storage, provisionalId, deadline, logger), fallbackAtDeadline]).finally(() => clearTimeout(deadlineTimer));
808
1045
  };
809
1046
  //#endregion
810
1047
  //#region ../shared/src/transport/AbstractPollingTransport.ts
@@ -885,6 +1122,7 @@ var PollingTransport = class extends AbstractPollingTransport {
885
1122
  eventEmitter = new Emitter();
886
1123
  url;
887
1124
  lastUpdateTimestamp;
1125
+ connectionGeneration = 0;
888
1126
  constructor(options) {
889
1127
  super();
890
1128
  this.options = options;
@@ -894,19 +1132,26 @@ var PollingTransport = class extends AbstractPollingTransport {
894
1132
  this.pollingIntervalSeconds = options.pollingInterval ?? 60;
895
1133
  }
896
1134
  async connect(context, timeout) {
1135
+ const generation = ++this.connectionGeneration;
897
1136
  this.clearPollingInterval();
1137
+ const clientSession = await this.options.clientSession;
1138
+ if (generation !== this.connectionGeneration) return this;
898
1139
  try {
899
- await this.fetchConfigs(context, timeout);
1140
+ await this.fetchConfigs(context, timeout, clientSession);
900
1141
  } finally {
901
1142
  if (!this.fatalError) this.schedulePollingInterval(() => {
902
- this.fetchConfigs(context, timeout).catch((error) => {
1143
+ this.fetchConfigs(context, timeout, clientSession).catch((error) => {
903
1144
  this.logger.error("[PollingTransport] Error during polling:", error);
904
1145
  });
905
1146
  });
906
1147
  }
907
1148
  return this;
908
1149
  }
909
- async fetchConfigs(context, timeout) {
1150
+ close() {
1151
+ this.connectionGeneration++;
1152
+ super.close();
1153
+ }
1154
+ async fetchConfigs(context, timeout, clientSession) {
910
1155
  if (this.fatalError) {
911
1156
  this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
912
1157
  return;
@@ -919,7 +1164,8 @@ var PollingTransport = class extends AbstractPollingTransport {
919
1164
  givenContext: context,
920
1165
  metaContext: this.options.metaContext,
921
1166
  clientSdkKey: this.options.clientSdkKey,
922
- instanceId: this.options.instanceId,
1167
+ instanceId: clientSession.instanceId,
1168
+ ephemeral: clientSession.ephemeral,
923
1169
  lastUpdateTimestamp: this.lastUpdateTimestamp
924
1170
  })
925
1171
  }, this.logger);
@@ -960,9 +1206,49 @@ const read = (readValue) => {
960
1206
  const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
961
1207
  const readHost = () => read((globals) => globals.location?.host);
962
1208
  //#endregion
1209
+ //#region ../shared/src/context-kind.ts
1210
+ const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
1211
+ const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
1212
+ const isContextKindCharacter = (character) => character >= "a" && character <= "z" || character >= "0" && character <= "9" || character === "-" || character === "_";
1213
+ const isValidContextKind = (kind) => {
1214
+ if (kind.length < 1 || kind.length > CONTEXT_KIND_MAXIMUM_LENGTH) return false;
1215
+ for (const character of kind) if (!isContextKindCharacter(character)) return false;
1216
+ return true;
1217
+ };
1218
+ const isAcceptedKind = (kind) => kind === void 0 || kind === null || typeof kind === "string" && isValidContextKind(kind);
1219
+ const stringFormOf = (kind) => {
1220
+ if (typeof kind === "string") return kind;
1221
+ try {
1222
+ return String(kind);
1223
+ } catch {
1224
+ return Object.prototype.toString.call(kind);
1225
+ }
1226
+ };
1227
+ const firstCharactersOf = (text, maximumCharacters) => {
1228
+ let sample = "";
1229
+ let characterCount = 0;
1230
+ for (const character of text) {
1231
+ if (characterCount === maximumCharacters) break;
1232
+ sample += character;
1233
+ characterCount += 1;
1234
+ }
1235
+ return sample;
1236
+ };
1237
+ const admitContext = (context) => {
1238
+ const kind = context?.kind;
1239
+ if (isAcceptedKind(kind)) return {
1240
+ outcome: "admitted",
1241
+ context
1242
+ };
1243
+ return {
1244
+ outcome: "ignored",
1245
+ kindSample: firstCharactersOf(stringFormOf(kind), KIND_SAMPLE_MAXIMUM_CHARACTERS)
1246
+ };
1247
+ };
1248
+ //#endregion
963
1249
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
964
1250
  const MAX_EXPONENTIAL_DELAY = 9;
965
- var DefaultConfigDirectorClient = class {
1251
+ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
966
1252
  logger;
967
1253
  telemetryClient;
968
1254
  configSet;
@@ -977,18 +1263,20 @@ var DefaultConfigDirectorClient = class {
977
1263
  readyPromise;
978
1264
  readyResolve;
979
1265
  currentContext;
980
- connectionMode;
981
- instanceId;
982
- constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1266
+ connectionPlan;
1267
+ 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);
1271
+ }
1272
+ constructor(liveTelemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
983
1273
  this.logger = clientOptions?.logger ?? createDefaultLogger();
984
- this.instanceId = resolveInstanceId(this.logger);
1274
+ this.clientSession = resolveClientSession(this.logger, internalClientOptions?.instanceIdStorage);
985
1275
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
986
- this.validateSdkKeyPresence(clientSdkKey);
987
1276
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
988
- const baseUrl = this.parseUrl(clientOptions?.connection?.url, urlFactory) ?? "https://client-sdk-api.configdirector.com";
989
- this.connectionMode = clientOptions?.connection?.mode ?? "streaming";
990
- this.telemetryClient = telemetryClient;
991
- this.transport = internalClientOptions?.transport ?? this.buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions);
1277
+ this.connectionPlan = planConnection(clientSdkKey, clientOptions?.connection, urlFactory);
1278
+ this.telemetryClient = this.buildTelemetryClient(liveTelemetryClient, clientOptions);
1279
+ this.transport = internalClientOptions?.transport ?? this.buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions);
992
1280
  this.transport.on("configSetReceived", (configSet) => {
993
1281
  this.hasReceivedConfigSet = true;
994
1282
  this.readyResolve?.();
@@ -1025,8 +1313,8 @@ var DefaultConfigDirectorClient = class {
1025
1313
  const handlers = Array.isArray(handler) ? handler : [handler];
1026
1314
  for (const h of handlers) if (typeof h === "function") this.on(event, h);
1027
1315
  }
1028
- resolvePollingIntervalForMode(configuredSeconds) {
1029
- if (this.connectionMode !== "polling") return;
1316
+ pollingIntervalFor(mode, configuredSeconds) {
1317
+ if (mode !== "polling") return;
1030
1318
  const resolved = resolvePollingInterval(configuredSeconds, {
1031
1319
  defaultSeconds: 60,
1032
1320
  minimumSeconds: 30
@@ -1034,39 +1322,79 @@ var DefaultConfigDirectorClient = class {
1034
1322
  if (resolved.warning) this.logger.warn(`[ConfigDirectorClient] ${resolved.warning}`);
1035
1323
  return resolved.seconds;
1036
1324
  }
1037
- buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions) {
1038
- return new (this.getTransportConstructor(this.connectionMode))({
1039
- instanceId: this.instanceId,
1040
- clientSdkKey,
1041
- baseUrl,
1042
- resolveUrl: urlFactory,
1043
- metaContext: {
1044
- ...clientOptions?.metadata,
1045
- sdkName: sdkOptions.sdkName,
1046
- sdkVersion: sdkOptions.sdkVersion,
1047
- userAgent: readUserAgent(),
1048
- host: readHost()
1049
- },
1050
- logger: this.logger,
1051
- fetch: internalClientOptions?.fetch,
1052
- connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1053
- return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1054
- }),
1055
- pollingInterval: this.resolvePollingIntervalForMode(clientOptions?.connection?.pollingInterval)
1056
- });
1325
+ buildTelemetryClient(liveTelemetryClient, clientOptions) {
1326
+ const plan = this.connectionPlan;
1327
+ switch (plan.kind) {
1328
+ case "stay-offline": return discardingTelemetryClient;
1329
+ case "connect": return liveTelemetryClient({
1330
+ clientSdkKey: plan.clientSdkKey,
1331
+ baseUrl: plan.baseUrl,
1332
+ logger: this.logger,
1333
+ metadata: clientOptions?.metadata
1334
+ });
1335
+ }
1057
1336
  }
1058
- getTransportConstructor(connectionMode) {
1059
- switch (connectionMode) {
1337
+ buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions) {
1338
+ const plan = this.connectionPlan;
1339
+ switch (plan.kind) {
1340
+ case "stay-offline": return new OfflineTransport(plan.source);
1341
+ case "connect": return new (this.getTransportConstructor(plan.mode))({
1342
+ clientSession: this.clientSession,
1343
+ clientSdkKey: plan.clientSdkKey,
1344
+ baseUrl: plan.baseUrl,
1345
+ resolveUrl: urlFactory,
1346
+ metaContext: {
1347
+ ...clientOptions?.metadata,
1348
+ sdkName: sdkOptions.sdkName,
1349
+ sdkVersion: sdkOptions.sdkVersion,
1350
+ userAgent: readUserAgent(),
1351
+ host: readHost()
1352
+ },
1353
+ logger: this.logger,
1354
+ fetch: internalClientOptions?.fetch,
1355
+ connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1356
+ return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1357
+ }),
1358
+ pollingInterval: this.pollingIntervalFor(plan.mode, clientOptions?.connection?.pollingInterval)
1359
+ });
1360
+ }
1361
+ }
1362
+ getTransportConstructor(mode) {
1363
+ switch (mode) {
1060
1364
  case "polling": return PollingTransport;
1061
- default: return StreamingTransport;
1365
+ case "streaming": return StreamingTransport;
1062
1366
  }
1063
1367
  }
1064
1368
  async initialize(context) {
1065
1369
  if (!this.hasReceivedConfigSet) this.initializing = true;
1066
- await this.connectToTransport(context, "initialization");
1370
+ await this.submitContext(context, "initialization");
1067
1371
  }
1068
1372
  async updateContext(context) {
1069
- await this.connectToTransport(context, "context update");
1373
+ switch (this.connectionPlan.kind) {
1374
+ case "connect":
1375
+ await this.submitContext(context, "context update");
1376
+ return;
1377
+ case "stay-offline":
1378
+ this.storeContext(this.admittedContextFor(context, "context update"));
1379
+ return;
1380
+ }
1381
+ }
1382
+ storeContext(context) {
1383
+ this.currentContext = context;
1384
+ this.emit("contextUpdated", { context });
1385
+ }
1386
+ async submitContext(context, submission) {
1387
+ await this.connectToTransport(this.admittedContextFor(context, submission), submission);
1388
+ }
1389
+ admittedContextFor(suppliedContext, submission) {
1390
+ const admission = admitContext(suppliedContext);
1391
+ switch (admission.outcome) {
1392
+ case "admitted": return admission.context;
1393
+ case "ignored":
1394
+ this.logger.error(`[ConfigDirectorClient] Ignored the context supplied for ${submission} because its kind '${admission.kindSample}' is invalid; continuing as if no context was supplied. A context kind has 1 to 64 characters, using only lowercase letters, digits, '-' and '_'.`);
1395
+ this.telemetryClient.recordInvalidContext(admission.kindSample);
1396
+ return;
1397
+ }
1070
1398
  }
1071
1399
  get context() {
1072
1400
  return this.currentContext;
@@ -1095,11 +1423,21 @@ var DefaultConfigDirectorClient = class {
1095
1423
  if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
1096
1424
  this.timeoutTimer = setTimeout(() => resolve(), remainingTimeout);
1097
1425
  })]).finally(() => clearTimeout(this.timeoutTimer));
1098
- if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.`);
1426
+ if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. ${this.timeoutWarningDetails()}`);
1099
1427
  } catch (error) {
1428
+ if (error instanceof ConfigDirectorInitializationError) {
1429
+ this.initializing = false;
1430
+ throw error;
1431
+ }
1100
1432
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
1101
1433
  }
1102
1434
  }
1435
+ timeoutWarningDetails() {
1436
+ switch (this.connectionPlan.kind) {
1437
+ case "connect": return "The client will continue to retry since there were no fatal errors detected. Configs will return the default value until the connection succeeds.";
1438
+ case "stay-offline": return "The offline values were not delivered, so configs return the default value.";
1439
+ }
1440
+ }
1103
1441
  findKeysRemovedBy(configSet) {
1104
1442
  if (!this.configSet || configSet.kind != "full") return [];
1105
1443
  return Object.keys(this.configSet.configs).filter((key) => !Object.prototype.hasOwnProperty.call(configSet.configs, key));
@@ -1194,17 +1532,6 @@ var DefaultConfigDirectorClient = class {
1194
1532
  this.logger.error(`[ConfigDirectorClient] Error executing event handlers for '${name}'`, error);
1195
1533
  }
1196
1534
  }
1197
- parseUrl(url, urlFactory) {
1198
- if (!url) return;
1199
- try {
1200
- return urlFactory(url);
1201
- } catch (error) {
1202
- throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
1203
- }
1204
- }
1205
- validateSdkKeyPresence(sdkKey) {
1206
- if (!sdkKey || sdkKey.trim().length == 0) throw new ConfigDirectorValidationError("No client SDK key was provided, the client cannot be instantiated without a valid client SDK key");
1207
- }
1208
1535
  validateDefaultValue(defaultValue) {
1209
1536
  if (defaultValue === void 0 || defaultValue === null) throw new ConfigDirectorValidationError("Invalid default value. The default value for a config must be defined and non-null.");
1210
1537
  if (typeof defaultValue === "function") throw new ConfigDirectorValidationError("Invalid default value. The default value for a config cannot be a function.");
@@ -1230,14 +1557,24 @@ var DefaultConfigDirectorClient = class {
1230
1557
  this.handlersMap.clear();
1231
1558
  }
1232
1559
  pauseNetwork() {
1233
- this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1234
- clearTimeout(this.timeoutTimer);
1235
- this.timeoutTimer = void 0;
1236
- this.transport.close();
1237
- this.ready = false;
1560
+ switch (this.connectionPlan.kind) {
1561
+ case "stay-offline": return;
1562
+ case "connect":
1563
+ this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1564
+ clearTimeout(this.timeoutTimer);
1565
+ this.timeoutTimer = void 0;
1566
+ this.transport.close();
1567
+ this.ready = false;
1568
+ return;
1569
+ }
1238
1570
  }
1239
1571
  async resumeNetwork() {
1240
- await this.connectToTransport(this.currentContext, "network resume");
1572
+ switch (this.connectionPlan.kind) {
1573
+ case "stay-offline": return;
1574
+ case "connect":
1575
+ await this.connectToTransport(this.currentContext, "network resume");
1576
+ return;
1577
+ }
1241
1578
  }
1242
1579
  async close() {
1243
1580
  this.logger.debug("[ConfigDirectorClient] close() has been called, closing connection to server");
@@ -1254,97 +1591,6 @@ var DefaultConfigDirectorClient = class {
1254
1591
  }
1255
1592
  };
1256
1593
  //#endregion
1257
- //#region ../shared/src/testing/values.ts
1258
- const encodeTestValue$1 = (key, value) => {
1259
- if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a test value cannot be empty.");
1260
- return encode(key, value);
1261
- };
1262
- const encode = (key, value) => {
1263
- switch (typeof value) {
1264
- case "boolean": return {
1265
- type: "boolean",
1266
- value: value ? "true" : "false"
1267
- };
1268
- case "number": return encodeNumber(key, value);
1269
- case "string": return {
1270
- type: "string",
1271
- value
1272
- };
1273
- case "object": if (value !== null) {
1274
- assertJsonContents(key, value, "");
1275
- return {
1276
- type: "json",
1277
- value: JSON.stringify(value)
1278
- };
1279
- }
1280
- }
1281
- throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${describe(value)}. Use a boolean, a number, a string, a plain object, or an array.`);
1282
- };
1283
- const encodeNumber = (key, value) => {
1284
- if (!Number.isFinite(value)) throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${describe(value)} is not a finite number.`);
1285
- if (Number.isInteger(value)) {
1286
- if (!Number.isSafeInteger(value)) throw new ConfigDirectorValidationError(`Invalid test value for '${key}': ${value} is not a safe integer, so the SDK could not read it back exactly.`);
1287
- return {
1288
- type: "integer",
1289
- value: String(value)
1290
- };
1291
- }
1292
- return {
1293
- type: "float",
1294
- value: toPlainDecimal(value)
1295
- };
1296
- };
1297
- const toPlainDecimal = (value) => {
1298
- const text = String(value);
1299
- const exponentIndex = text.indexOf("e-");
1300
- if (exponentIndex < 0) return text;
1301
- const sign = text.startsWith("-") ? "-" : "";
1302
- const [integerDigits = "", fractionDigits = ""] = text.slice(sign.length, exponentIndex).split(".");
1303
- const leadingZeros = Number(text.slice(exponentIndex + 2)) - integerDigits.length;
1304
- return `${sign}0.${"0".repeat(leadingZeros)}${integerDigits}${fractionDigits}`;
1305
- };
1306
- const assertJsonContents = (key, value, path) => {
1307
- if (value === null || typeof value === "boolean" || typeof value === "string") return;
1308
- if (typeof value === "number") {
1309
- if (!Number.isFinite(value)) throw invalidJsonContents(key, path, `${describe(value)} is not a finite number`);
1310
- return;
1311
- }
1312
- if (Array.isArray(value)) {
1313
- value.forEach((item, index) => assertJsonContents(key, item, `${path}[${index}]`));
1314
- return;
1315
- }
1316
- if (isPlainObject(value)) {
1317
- for (const [property, item] of Object.entries(value)) assertJsonContents(key, item, path ? `${path}.${property}` : property);
1318
- return;
1319
- }
1320
- throw invalidJsonContents(key, path, `${describe(value)} cannot be encoded as JSON`);
1321
- };
1322
- const invalidJsonContents = (key, path, problem) => new ConfigDirectorValidationError(`Invalid test value for '${key}'${path ? ` at '${path}'` : ""}: ${problem}. JSON contents can hold booleans, finite numbers, strings, null, arrays, and plain objects.`);
1323
- const isPlainObject = (value) => {
1324
- if (typeof value !== "object" || value === null) return false;
1325
- const prototype = Object.getPrototypeOf(value);
1326
- return prototype === Object.prototype || prototype === null;
1327
- };
1328
- const describe = (value) => {
1329
- if (value === null) return "null";
1330
- if (typeof value === "function") return "a function";
1331
- if (typeof value === "object") return `a ${value.constructor?.name ?? "object"} instance`;
1332
- if (typeof value === "symbol") return "a symbol";
1333
- return String(value);
1334
- };
1335
- //#endregion
1336
- //#region ../js-client-core/src/testing/values.ts
1337
- const encodeTestValue = (key, value, revision) => {
1338
- const encoded = encodeTestValue$1(key, value);
1339
- return {
1340
- id: `test-config:${key}`,
1341
- key,
1342
- type: encoded.type,
1343
- value: encoded.value,
1344
- valueId: `test-value:${key}:${revision}`
1345
- };
1346
- };
1347
- //#endregion
1348
1594
  //#region ../js-client-core/src/testing/InMemoryConnection.ts
1349
1595
  const TEST_ENVIRONMENT = {
1350
1596
  environmentId: "test-environment",
@@ -1379,7 +1625,7 @@ var InMemoryConnection = class {
1379
1625
  return [...this.recordedContexts];
1380
1626
  }
1381
1627
  setValue(key, value) {
1382
- const configState = encodeTestValue(key, value, ++this.revision);
1628
+ const configState = this.encode(key, value);
1383
1629
  this.configs.set(key, configState);
1384
1630
  this.deliverIfConnected({
1385
1631
  ...TEST_ENVIRONMENT,
@@ -1524,18 +1770,16 @@ var InMemoryConnection = class {
1524
1770
  }
1525
1771
  encodeAll(values) {
1526
1772
  const encoded = /* @__PURE__ */ new Map();
1527
- for (const [key, value] of Object.entries(values)) encoded.set(key, encodeTestValue(key, value, ++this.revision));
1773
+ for (const [key, value] of Object.entries(values)) encoded.set(key, this.encode(key, value));
1528
1774
  return encoded;
1529
1775
  }
1776
+ encode(key, value) {
1777
+ return nativeValueConfigState(`test-config:${key}`, key, value, `test-value:${key}:${++this.revision}`);
1778
+ }
1530
1779
  };
1531
1780
  //#endregion
1532
1781
  //#region ../js-client-core/src/testing/TestClient.ts
1533
1782
  const TEST_CLIENT_SDK_KEY = "test-client";
1534
- const noopTelemetryClient = {
1535
- updateContext: async () => {},
1536
- evaluatedConfig: () => {},
1537
- close: async () => {}
1538
- };
1539
1783
  var InMemoryTestClient = class {
1540
1784
  constructor(client, connection) {
1541
1785
  this.client = client;
@@ -1575,7 +1819,7 @@ var InMemoryTestClient = class {
1575
1819
  const createTestClient$1 = (sdkOptions, options) => {
1576
1820
  const logger = options?.logger ?? createDefaultLogger();
1577
1821
  const connection = new InMemoryConnection(logger, options?.values ?? {});
1578
- return new InMemoryTestClient(new DefaultConfigDirectorClient(noopTelemetryClient, TEST_CLIENT_SDK_KEY, sdkOptions, {
1822
+ return new InMemoryTestClient(new DefaultConfigDirectorClient(() => discardingTelemetryClient, TEST_CLIENT_SDK_KEY, sdkOptions, {
1579
1823
  logger,
1580
1824
  connection: { timeout: options?.timeout }
1581
1825
  }, { transport: connection }), connection);
@@ -1638,7 +1882,7 @@ const installTestClient = (testClient) => {
1638
1882
  const createTestClient = (options) => {
1639
1883
  return createTestClient$1({
1640
1884
  sdkName: "nuxt-sdk",
1641
- sdkVersion: "1.9.2"
1885
+ sdkVersion: "1.11.0"
1642
1886
  }, options);
1643
1887
  };
1644
1888
  //#endregion