@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
@@ -393,6 +393,7 @@ var StreamingTransport = class {
393
393
  eventEmitter = new Emitter();
394
394
  url;
395
395
  timeoutTimer;
396
+ connectionGeneration = 0;
396
397
  constructor(options) {
397
398
  this.options = options;
398
399
  this.options = options;
@@ -400,6 +401,9 @@ var StreamingTransport = class {
400
401
  this.url = options.resolveUrl("client/sse/v1", options.baseUrl);
401
402
  }
402
403
  async connect(context, timeout) {
404
+ const generation = ++this.connectionGeneration;
405
+ const clientSession = await this.options.clientSession;
406
+ if (generation !== this.connectionGeneration) return this;
403
407
  if (this.eventSource) this.close();
404
408
  const eventSourcePromise = new Promise((resolve, reject) => {
405
409
  this.eventSource = createEventSourceClient({
@@ -411,7 +415,8 @@ var StreamingTransport = class {
411
415
  givenContext: context,
412
416
  metaContext: this.options.metaContext,
413
417
  clientSdkKey: this.options.clientSdkKey,
414
- instanceId: this.options.instanceId
418
+ instanceId: clientSession.instanceId,
419
+ ephemeral: clientSession.ephemeral
415
420
  }),
416
421
  onMessage: ({ data }) => {
417
422
  this.dispatchMessage(data);
@@ -476,6 +481,7 @@ var StreamingTransport = class {
476
481
  this.eventEmitter.clear();
477
482
  }
478
483
  close() {
484
+ this.connectionGeneration++;
479
485
  this.eventSource?.close();
480
486
  clearTimeout(this.timeoutTimer);
481
487
  }
@@ -485,6 +491,183 @@ var StreamingTransport = class {
485
491
  }
486
492
  };
487
493
  //#endregion
494
+ //#region ../shared/src/native-values.ts
495
+ const encodeNativeValue = (key, value) => {
496
+ if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a value cannot be empty.");
497
+ return encode(key, value);
498
+ };
499
+ const encode = (key, value) => {
500
+ switch (typeof value) {
501
+ case "boolean": return {
502
+ type: "boolean",
503
+ value: value ? "true" : "false"
504
+ };
505
+ case "number": return encodeNumber(key, value);
506
+ case "string": return {
507
+ type: "string",
508
+ value
509
+ };
510
+ case "object": if (value !== null) {
511
+ assertJsonContents(key, value, "");
512
+ return {
513
+ type: "json",
514
+ value: JSON.stringify(value)
515
+ };
516
+ }
517
+ }
518
+ throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)}. Use a boolean, a number, a string, a plain object, or an array.`);
519
+ };
520
+ const encodeNumber = (key, value) => {
521
+ if (!Number.isFinite(value)) throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)} is not a finite number.`);
522
+ if (Number.isInteger(value)) {
523
+ 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.`);
524
+ return {
525
+ type: "integer",
526
+ value: String(value)
527
+ };
528
+ }
529
+ return {
530
+ type: "float",
531
+ value: toPlainDecimal(value)
532
+ };
533
+ };
534
+ const toPlainDecimal = (value) => {
535
+ const text = String(value);
536
+ const exponentIndex = text.indexOf("e-");
537
+ if (exponentIndex < 0) return text;
538
+ const sign = text.startsWith("-") ? "-" : "";
539
+ const [integerDigits = "", fractionDigits = ""] = text.slice(sign.length, exponentIndex).split(".");
540
+ const leadingZeros = Number(text.slice(exponentIndex + 2)) - integerDigits.length;
541
+ return `${sign}0.${"0".repeat(leadingZeros)}${integerDigits}${fractionDigits}`;
542
+ };
543
+ const assertJsonContents = (key, value, path) => {
544
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
545
+ if (typeof value === "number") {
546
+ if (!Number.isFinite(value)) throw invalidJsonContents(key, path, `${describe(value)} is not a finite number`);
547
+ return;
548
+ }
549
+ if (Array.isArray(value)) {
550
+ value.forEach((item, index) => assertJsonContents(key, item, `${path}[${index}]`));
551
+ return;
552
+ }
553
+ if (isPlainObject$1(value)) {
554
+ for (const [property, item] of Object.entries(value)) assertJsonContents(key, item, path ? `${path}.${property}` : property);
555
+ return;
556
+ }
557
+ throw invalidJsonContents(key, path, `${describe(value)} cannot be encoded as JSON`);
558
+ };
559
+ 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.`);
560
+ const isPlainObject$1 = (value) => {
561
+ if (typeof value !== "object" || value === null) return false;
562
+ const prototype = Object.getPrototypeOf(value);
563
+ return prototype === Object.prototype || prototype === null;
564
+ };
565
+ const describe = (value) => {
566
+ if (value === null) return "null";
567
+ if (typeof value === "function") return "a function";
568
+ if (typeof value === "object") return `a ${value.constructor?.name ?? "object"} instance`;
569
+ if (typeof value === "symbol") return "a symbol";
570
+ return String(value);
571
+ };
572
+ //#endregion
573
+ //#region ../js-client-core/src/native-values.ts
574
+ const nativeValueConfigState = (id, key, value, valueId) => {
575
+ const encoded = encodeNativeValue(key, value);
576
+ return {
577
+ id,
578
+ key,
579
+ type: encoded.type,
580
+ value: encoded.value,
581
+ valueId
582
+ };
583
+ };
584
+ //#endregion
585
+ //#region ../js-client-core/src/OfflineTransport.ts
586
+ const OFFLINE_ENVIRONMENT = {
587
+ environmentId: "offline",
588
+ projectId: "offline"
589
+ };
590
+ var OfflineTransport = class {
591
+ emitter = new Emitter();
592
+ constructor(source) {
593
+ this.source = source;
594
+ }
595
+ async connect() {
596
+ const configs = loadOfflineConfigs(this.source);
597
+ this.emitter.emit("configSetReceived", {
598
+ ...OFFLINE_ENVIRONMENT,
599
+ kind: "full",
600
+ configs
601
+ });
602
+ return this;
603
+ }
604
+ on = (name, handler) => this.emitter.on(name, handler);
605
+ off = (name, handler) => this.emitter.off(name, handler);
606
+ clear() {
607
+ this.emitter.clear();
608
+ }
609
+ close() {}
610
+ dispose() {
611
+ this.clear();
612
+ }
613
+ };
614
+ const loadOfflineConfigs = (source) => {
615
+ switch (source.kind) {
616
+ case "in-code-defaults": return {};
617
+ case "values": return encodeOfflineValues(source.values);
618
+ case "rejected": throw new ConfigDirectorInitializationError(source.message);
619
+ }
620
+ };
621
+ const encodeOfflineValues = (values) => {
622
+ try {
623
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, nativeValueConfigState(`offline:${key}`, key, value, void 0)]));
624
+ } catch (error) {
625
+ if (error instanceof ConfigDirectorValidationError) throw new ConfigDirectorInitializationError(`The offline values could not be loaded. ${error.message}`);
626
+ throw error;
627
+ }
628
+ };
629
+ //#endregion
630
+ //#region ../shared/src/constants.ts
631
+ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
632
+ //#endregion
633
+ //#region ../js-client-core/src/connection-plan.ts
634
+ const planConnection = (clientSdkKey, connection, urlFactory) => {
635
+ if (connection?.mode === "offline") return {
636
+ kind: "stay-offline",
637
+ source: offlineSourceOf(connection)
638
+ };
639
+ const mode = connection?.mode ?? "streaming";
640
+ 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");
641
+ if (connection?.values !== void 0) return {
642
+ kind: "stay-offline",
643
+ source: {
644
+ kind: "rejected",
645
+ message: `The connection option 'values' applies only when the connection mode is 'offline', but the mode is '${mode}'.`
646
+ }
647
+ };
648
+ return {
649
+ kind: "connect",
650
+ mode,
651
+ clientSdkKey,
652
+ baseUrl: baseUrlOf(connection?.url, urlFactory)
653
+ };
654
+ };
655
+ const baseUrlOf = (url, urlFactory) => {
656
+ if (!url) return CLIENT_BASE_URL;
657
+ try {
658
+ return urlFactory(url);
659
+ } catch (error) {
660
+ throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
661
+ }
662
+ };
663
+ const offlineSourceOf = (connection) => {
664
+ if (connection.values !== void 0) return {
665
+ kind: "values",
666
+ values: connection.values
667
+ };
668
+ return { kind: "in-code-defaults" };
669
+ };
670
+ //#endregion
488
671
  //#region ../shared/src/value-parser.ts
489
672
  const getRequestedType = (defaultValue) => {
490
673
  const baseType = typeof defaultValue;
@@ -704,16 +887,16 @@ const createDefaultLogger$1 = (level, messageDecorator) => {
704
887
  return new DefaultConsoleLogger(level ?? "warn", messageDecorator ?? new LogMessageDecorator());
705
888
  };
706
889
  //#endregion
890
+ //#region ../js-client-core/src/telemetry/discarding.ts
891
+ const discardingTelemetryClient = {
892
+ updateContext: async () => {},
893
+ evaluatedConfig: () => {},
894
+ recordInvalidContext: () => {},
895
+ close: async () => {}
896
+ };
897
+ //#endregion
707
898
  //#region ../shared/src/url.ts
708
899
  const defaultUrlFactory = (input, base) => new URL(input, base?.toString());
709
- const parseUrl = (url) => {
710
- if (!url) return;
711
- try {
712
- return new URL(url);
713
- } catch (error) {
714
- throw new ConfigDirectorValidationError(`Invalid URL '${url}'. Parsing failed: ${error}`);
715
- }
716
- };
717
900
  //#endregion
718
901
  //#region ../shared/src/transport/pollingInterval.ts
719
902
  const resolvePollingInterval = (configuredSeconds, defaults) => {
@@ -740,10 +923,11 @@ const generateInstanceId = () => {
740
923
  });
741
924
  };
742
925
  //#endregion
743
- //#region ../js-client-core/src/instance-id.ts
926
+ //#region ../js-client-core/src/client-session.ts
744
927
  const STORAGE_KEY = "configdirector-sdk:instance-id";
745
928
  const RECORD_VERSION = 1;
746
929
  const MAX_AGE_MS = 360 * 60 * 1e3;
930
+ const STORAGE_DEADLINE_MS = 1e3;
747
931
  const CHECKSUM_SALT = "configdirector.instanceId.v1";
748
932
  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}$/;
749
933
  const fnv1a = (input, offsetBasis) => {
@@ -759,7 +943,7 @@ const checksumFor = (id, createdAt) => {
759
943
  const low = fnv1a(payload, 2166136261);
760
944
  return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
761
945
  };
762
- const getLocalStorage = () => {
946
+ const localStorageIfAvailable = () => {
763
947
  try {
764
948
  return globalThis.localStorage ?? void 0;
765
949
  } catch {
@@ -774,34 +958,37 @@ const isUsableRecord = (record) => {
774
958
  if (!UUID_V4_PATTERN.test(id)) return false;
775
959
  return checksum === checksumFor(id, createdAt);
776
960
  };
777
- const readStoredInstanceId = (storage, logger) => {
961
+ const readStoredInstanceId = async (storage, logger) => {
778
962
  let raw;
779
963
  try {
780
- raw = storage.getItem(STORAGE_KEY);
964
+ raw = await storage.getItem(STORAGE_KEY);
781
965
  } catch (error) {
782
- logger?.debug("[ConfigDirectorClient] Could not read the stored instance id, generating a new one", error);
783
- return;
966
+ logger.debug("[ConfigDirectorClient] Could not read the stored instance id, using an ephemeral one", error);
967
+ return { outcome: "failed" };
784
968
  }
785
- if (!raw) return;
969
+ if (typeof raw !== "string" || raw === "") return { outcome: "nothing usable" };
786
970
  let record;
787
971
  try {
788
972
  record = JSON.parse(raw);
789
973
  } catch {
790
- logger?.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
791
- return;
974
+ logger.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
975
+ return { outcome: "nothing usable" };
792
976
  }
793
977
  if (!isUsableRecord(record)) {
794
- logger?.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
795
- return;
978
+ logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
979
+ return { outcome: "nothing usable" };
796
980
  }
797
981
  const age = Date.now() - record.createdAt;
798
982
  if (age < 0 || age >= MAX_AGE_MS) {
799
- logger?.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
800
- return;
983
+ logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
984
+ return { outcome: "nothing usable" };
801
985
  }
802
- return record.id;
986
+ return {
987
+ outcome: "stored",
988
+ instanceId: record.id
989
+ };
803
990
  };
804
- const storeInstanceId = (storage, id, logger) => {
991
+ const persistInstanceId = async (storage, id, logger) => {
805
992
  const createdAt = Date.now();
806
993
  const record = {
807
994
  v: RECORD_VERSION,
@@ -810,19 +997,54 @@ const storeInstanceId = (storage, id, logger) => {
810
997
  checksum: checksumFor(id, createdAt)
811
998
  };
812
999
  try {
813
- storage.setItem(STORAGE_KEY, JSON.stringify(record));
1000
+ await storage.setItem(STORAGE_KEY, JSON.stringify(record));
1001
+ return true;
814
1002
  } catch (error) {
815
- logger?.debug("[ConfigDirectorClient] Could not persist the instance id", error);
1003
+ logger.debug("[ConfigDirectorClient] Could not persist the instance id, using it as an ephemeral one", error);
1004
+ return false;
1005
+ }
1006
+ };
1007
+ const settleFromStorage = async (storage, provisionalId, deadline, logger) => {
1008
+ const read = await readStoredInstanceId(storage, logger);
1009
+ switch (read.outcome) {
1010
+ case "stored": return { instanceId: read.instanceId };
1011
+ case "failed": return {
1012
+ instanceId: provisionalId,
1013
+ ephemeral: true
1014
+ };
1015
+ case "nothing usable": break;
816
1016
  }
1017
+ if (deadline.passed) return {
1018
+ instanceId: provisionalId,
1019
+ ephemeral: true
1020
+ };
1021
+ return await persistInstanceId(storage, provisionalId, logger) ? { instanceId: provisionalId } : {
1022
+ instanceId: provisionalId,
1023
+ ephemeral: true
1024
+ };
817
1025
  };
818
- const resolveInstanceId = (logger) => {
819
- const storage = getLocalStorage();
820
- if (!storage) return generateInstanceId();
821
- const storedInstanceId = readStoredInstanceId(storage, logger);
822
- if (storedInstanceId) return storedInstanceId;
823
- const instanceId = generateInstanceId();
824
- storeInstanceId(storage, instanceId, logger);
825
- return instanceId;
1026
+ const resolveClientSession = (logger, storage = localStorageIfAvailable()) => {
1027
+ const provisionalId = generateInstanceId();
1028
+ if (!storage) {
1029
+ logger.debug("[ConfigDirectorClient] No instance id storage is available, using an ephemeral instance id");
1030
+ return Promise.resolve({
1031
+ instanceId: provisionalId,
1032
+ ephemeral: true
1033
+ });
1034
+ }
1035
+ const deadline = { passed: false };
1036
+ let deadlineTimer;
1037
+ const fallbackAtDeadline = new Promise((resolve) => {
1038
+ deadlineTimer = setTimeout(() => {
1039
+ deadline.passed = true;
1040
+ logger.warn(`[ConfigDirectorClient] The instance id storage did not respond within ${STORAGE_DEADLINE_MS}ms, using an ephemeral instance id`);
1041
+ resolve({
1042
+ instanceId: provisionalId,
1043
+ ephemeral: true
1044
+ });
1045
+ }, STORAGE_DEADLINE_MS);
1046
+ });
1047
+ return Promise.race([settleFromStorage(storage, provisionalId, deadline, logger), fallbackAtDeadline]).finally(() => clearTimeout(deadlineTimer));
826
1048
  };
827
1049
  //#endregion
828
1050
  //#region ../shared/src/transport/AbstractPollingTransport.ts
@@ -903,6 +1125,7 @@ var PollingTransport = class extends AbstractPollingTransport {
903
1125
  eventEmitter = new Emitter();
904
1126
  url;
905
1127
  lastUpdateTimestamp;
1128
+ connectionGeneration = 0;
906
1129
  constructor(options) {
907
1130
  super();
908
1131
  this.options = options;
@@ -912,19 +1135,26 @@ var PollingTransport = class extends AbstractPollingTransport {
912
1135
  this.pollingIntervalSeconds = options.pollingInterval ?? 60;
913
1136
  }
914
1137
  async connect(context, timeout) {
1138
+ const generation = ++this.connectionGeneration;
915
1139
  this.clearPollingInterval();
1140
+ const clientSession = await this.options.clientSession;
1141
+ if (generation !== this.connectionGeneration) return this;
916
1142
  try {
917
- await this.fetchConfigs(context, timeout);
1143
+ await this.fetchConfigs(context, timeout, clientSession);
918
1144
  } finally {
919
1145
  if (!this.fatalError) this.schedulePollingInterval(() => {
920
- this.fetchConfigs(context, timeout).catch((error) => {
1146
+ this.fetchConfigs(context, timeout, clientSession).catch((error) => {
921
1147
  this.logger.error("[PollingTransport] Error during polling:", error);
922
1148
  });
923
1149
  });
924
1150
  }
925
1151
  return this;
926
1152
  }
927
- async fetchConfigs(context, timeout) {
1153
+ close() {
1154
+ this.connectionGeneration++;
1155
+ super.close();
1156
+ }
1157
+ async fetchConfigs(context, timeout, clientSession) {
928
1158
  if (this.fatalError) {
929
1159
  this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
930
1160
  return;
@@ -937,7 +1167,8 @@ var PollingTransport = class extends AbstractPollingTransport {
937
1167
  givenContext: context,
938
1168
  metaContext: this.options.metaContext,
939
1169
  clientSdkKey: this.options.clientSdkKey,
940
- instanceId: this.options.instanceId,
1170
+ instanceId: clientSession.instanceId,
1171
+ ephemeral: clientSession.ephemeral,
941
1172
  lastUpdateTimestamp: this.lastUpdateTimestamp
942
1173
  })
943
1174
  }, this.logger);
@@ -978,9 +1209,53 @@ const read = (readValue) => {
978
1209
  const readUserAgent = () => read((globals) => globals.navigator?.userAgent);
979
1210
  const readHost = () => read((globals) => globals.location?.host);
980
1211
  //#endregion
1212
+ //#region ../shared/src/context-kind.ts
1213
+ const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
1214
+ const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
1215
+ const isContextKindCharacter = (character) => character >= "a" && character <= "z" || character >= "0" && character <= "9" || character === "-" || character === "_";
1216
+ const isValidContextKind = (kind) => {
1217
+ if (kind.length < 1 || kind.length > CONTEXT_KIND_MAXIMUM_LENGTH) return false;
1218
+ for (const character of kind) if (!isContextKindCharacter(character)) return false;
1219
+ return true;
1220
+ };
1221
+ const isAcceptedKind = (kind) => kind === void 0 || kind === null || typeof kind === "string" && isValidContextKind(kind);
1222
+ const stringFormOf = (kind) => {
1223
+ if (typeof kind === "string") return kind;
1224
+ try {
1225
+ return String(kind);
1226
+ } catch {
1227
+ return Object.prototype.toString.call(kind);
1228
+ }
1229
+ };
1230
+ const firstCharactersOf = (text, maximumCharacters) => {
1231
+ let sample = "";
1232
+ let characterCount = 0;
1233
+ for (const character of text) {
1234
+ if (characterCount === maximumCharacters) break;
1235
+ sample += character;
1236
+ characterCount += 1;
1237
+ }
1238
+ return sample;
1239
+ };
1240
+ const admittedContextOrNone = (context) => {
1241
+ const admission = admitContext(context);
1242
+ return admission.outcome === "admitted" ? admission.context : void 0;
1243
+ };
1244
+ const admitContext = (context) => {
1245
+ const kind = context?.kind;
1246
+ if (isAcceptedKind(kind)) return {
1247
+ outcome: "admitted",
1248
+ context
1249
+ };
1250
+ return {
1251
+ outcome: "ignored",
1252
+ kindSample: firstCharactersOf(stringFormOf(kind), KIND_SAMPLE_MAXIMUM_CHARACTERS)
1253
+ };
1254
+ };
1255
+ //#endregion
981
1256
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
982
1257
  const MAX_EXPONENTIAL_DELAY = 9;
983
- var DefaultConfigDirectorClient = class {
1258
+ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
984
1259
  logger;
985
1260
  telemetryClient;
986
1261
  configSet;
@@ -995,18 +1270,20 @@ var DefaultConfigDirectorClient = class {
995
1270
  readyPromise;
996
1271
  readyResolve;
997
1272
  currentContext;
998
- connectionMode;
999
- instanceId;
1000
- constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1273
+ connectionPlan;
1274
+ 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);
1278
+ }
1279
+ constructor(liveTelemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1001
1280
  this.logger = clientOptions?.logger ?? createDefaultLogger$1();
1002
- this.instanceId = resolveInstanceId(this.logger);
1281
+ this.clientSession = resolveClientSession(this.logger, internalClientOptions?.instanceIdStorage);
1003
1282
  this.timeout = clientOptions?.connection?.timeout ?? 3e3;
1004
- this.validateSdkKeyPresence(clientSdkKey);
1005
1283
  const urlFactory = internalClientOptions?.urlFactory ?? defaultUrlFactory;
1006
- const baseUrl = this.parseUrl(clientOptions?.connection?.url, urlFactory) ?? "https://client-sdk-api.configdirector.com";
1007
- this.connectionMode = clientOptions?.connection?.mode ?? "streaming";
1008
- this.telemetryClient = telemetryClient;
1009
- this.transport = internalClientOptions?.transport ?? this.buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions);
1284
+ this.connectionPlan = planConnection(clientSdkKey, clientOptions?.connection, urlFactory);
1285
+ this.telemetryClient = this.buildTelemetryClient(liveTelemetryClient, clientOptions);
1286
+ this.transport = internalClientOptions?.transport ?? this.buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions);
1010
1287
  this.transport.on("configSetReceived", (configSet) => {
1011
1288
  this.hasReceivedConfigSet = true;
1012
1289
  this.readyResolve?.();
@@ -1043,8 +1320,8 @@ var DefaultConfigDirectorClient = class {
1043
1320
  const handlers = Array.isArray(handler) ? handler : [handler];
1044
1321
  for (const h of handlers) if (typeof h === "function") this.on(event, h);
1045
1322
  }
1046
- resolvePollingIntervalForMode(configuredSeconds) {
1047
- if (this.connectionMode !== "polling") return;
1323
+ pollingIntervalFor(mode, configuredSeconds) {
1324
+ if (mode !== "polling") return;
1048
1325
  const resolved = resolvePollingInterval(configuredSeconds, {
1049
1326
  defaultSeconds: 60,
1050
1327
  minimumSeconds: 30
@@ -1052,39 +1329,79 @@ var DefaultConfigDirectorClient = class {
1052
1329
  if (resolved.warning) this.logger.warn(`[ConfigDirectorClient] ${resolved.warning}`);
1053
1330
  return resolved.seconds;
1054
1331
  }
1055
- buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions) {
1056
- return new (this.getTransportConstructor(this.connectionMode))({
1057
- instanceId: this.instanceId,
1058
- clientSdkKey,
1059
- baseUrl,
1060
- resolveUrl: urlFactory,
1061
- metaContext: {
1062
- ...clientOptions?.metadata,
1063
- sdkName: sdkOptions.sdkName,
1064
- sdkVersion: sdkOptions.sdkVersion,
1065
- userAgent: readUserAgent(),
1066
- host: readHost()
1067
- },
1068
- logger: this.logger,
1069
- fetch: internalClientOptions?.fetch,
1070
- connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1071
- return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1072
- }),
1073
- pollingInterval: this.resolvePollingIntervalForMode(clientOptions?.connection?.pollingInterval)
1074
- });
1332
+ buildTelemetryClient(liveTelemetryClient, clientOptions) {
1333
+ const plan = this.connectionPlan;
1334
+ switch (plan.kind) {
1335
+ case "stay-offline": return discardingTelemetryClient;
1336
+ case "connect": return liveTelemetryClient({
1337
+ clientSdkKey: plan.clientSdkKey,
1338
+ baseUrl: plan.baseUrl,
1339
+ logger: this.logger,
1340
+ metadata: clientOptions?.metadata
1341
+ });
1342
+ }
1343
+ }
1344
+ buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions) {
1345
+ const plan = this.connectionPlan;
1346
+ switch (plan.kind) {
1347
+ case "stay-offline": return new OfflineTransport(plan.source);
1348
+ case "connect": return new (this.getTransportConstructor(plan.mode))({
1349
+ clientSession: this.clientSession,
1350
+ clientSdkKey: plan.clientSdkKey,
1351
+ baseUrl: plan.baseUrl,
1352
+ resolveUrl: urlFactory,
1353
+ metaContext: {
1354
+ ...clientOptions?.metadata,
1355
+ sdkName: sdkOptions.sdkName,
1356
+ sdkVersion: sdkOptions.sdkVersion,
1357
+ userAgent: readUserAgent(),
1358
+ host: readHost()
1359
+ },
1360
+ logger: this.logger,
1361
+ fetch: internalClientOptions?.fetch,
1362
+ connectionRetryDelay: internalClientOptions?.connectionRetryDelay ?? ((attempt) => {
1363
+ return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1364
+ }),
1365
+ pollingInterval: this.pollingIntervalFor(plan.mode, clientOptions?.connection?.pollingInterval)
1366
+ });
1367
+ }
1075
1368
  }
1076
- getTransportConstructor(connectionMode) {
1077
- switch (connectionMode) {
1369
+ getTransportConstructor(mode) {
1370
+ switch (mode) {
1078
1371
  case "polling": return PollingTransport;
1079
- default: return StreamingTransport;
1372
+ case "streaming": return StreamingTransport;
1080
1373
  }
1081
1374
  }
1082
1375
  async initialize(context) {
1083
1376
  if (!this.hasReceivedConfigSet) this.initializing = true;
1084
- await this.connectToTransport(context, "initialization");
1377
+ await this.submitContext(context, "initialization");
1085
1378
  }
1086
1379
  async updateContext(context) {
1087
- await this.connectToTransport(context, "context update");
1380
+ switch (this.connectionPlan.kind) {
1381
+ case "connect":
1382
+ await this.submitContext(context, "context update");
1383
+ return;
1384
+ case "stay-offline":
1385
+ this.storeContext(this.admittedContextFor(context, "context update"));
1386
+ return;
1387
+ }
1388
+ }
1389
+ storeContext(context) {
1390
+ this.currentContext = context;
1391
+ this.emit("contextUpdated", { context });
1392
+ }
1393
+ async submitContext(context, submission) {
1394
+ await this.connectToTransport(this.admittedContextFor(context, submission), submission);
1395
+ }
1396
+ admittedContextFor(suppliedContext, submission) {
1397
+ const admission = admitContext(suppliedContext);
1398
+ switch (admission.outcome) {
1399
+ case "admitted": return admission.context;
1400
+ case "ignored":
1401
+ 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 '_'.`);
1402
+ this.telemetryClient.recordInvalidContext(admission.kindSample);
1403
+ return;
1404
+ }
1088
1405
  }
1089
1406
  get context() {
1090
1407
  return this.currentContext;
@@ -1113,11 +1430,21 @@ var DefaultConfigDirectorClient = class {
1113
1430
  if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
1114
1431
  this.timeoutTimer = setTimeout(() => resolve(), remainingTimeout);
1115
1432
  })]).finally(() => clearTimeout(this.timeoutTimer));
1116
- 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.`);
1433
+ if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. ${this.timeoutWarningDetails()}`);
1117
1434
  } catch (error) {
1435
+ if (error instanceof ConfigDirectorInitializationError) {
1436
+ this.initializing = false;
1437
+ throw error;
1438
+ }
1118
1439
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
1119
1440
  }
1120
1441
  }
1442
+ timeoutWarningDetails() {
1443
+ switch (this.connectionPlan.kind) {
1444
+ 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.";
1445
+ case "stay-offline": return "The offline values were not delivered, so configs return the default value.";
1446
+ }
1447
+ }
1121
1448
  findKeysRemovedBy(configSet) {
1122
1449
  if (!this.configSet || configSet.kind != "full") return [];
1123
1450
  return Object.keys(this.configSet.configs).filter((key) => !Object.prototype.hasOwnProperty.call(configSet.configs, key));
@@ -1212,17 +1539,6 @@ var DefaultConfigDirectorClient = class {
1212
1539
  this.logger.error(`[ConfigDirectorClient] Error executing event handlers for '${name}'`, error);
1213
1540
  }
1214
1541
  }
1215
- parseUrl(url, urlFactory) {
1216
- if (!url) return;
1217
- try {
1218
- return urlFactory(url);
1219
- } catch (error) {
1220
- throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
1221
- }
1222
- }
1223
- validateSdkKeyPresence(sdkKey) {
1224
- 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");
1225
- }
1226
1542
  validateDefaultValue(defaultValue) {
1227
1543
  if (defaultValue === void 0 || defaultValue === null) throw new ConfigDirectorValidationError("Invalid default value. The default value for a config must be defined and non-null.");
1228
1544
  if (typeof defaultValue === "function") throw new ConfigDirectorValidationError("Invalid default value. The default value for a config cannot be a function.");
@@ -1248,14 +1564,24 @@ var DefaultConfigDirectorClient = class {
1248
1564
  this.handlersMap.clear();
1249
1565
  }
1250
1566
  pauseNetwork() {
1251
- this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1252
- clearTimeout(this.timeoutTimer);
1253
- this.timeoutTimer = void 0;
1254
- this.transport.close();
1255
- this.ready = false;
1567
+ switch (this.connectionPlan.kind) {
1568
+ case "stay-offline": return;
1569
+ case "connect":
1570
+ this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1571
+ clearTimeout(this.timeoutTimer);
1572
+ this.timeoutTimer = void 0;
1573
+ this.transport.close();
1574
+ this.ready = false;
1575
+ return;
1576
+ }
1256
1577
  }
1257
1578
  async resumeNetwork() {
1258
- await this.connectToTransport(this.currentContext, "network resume");
1579
+ switch (this.connectionPlan.kind) {
1580
+ case "stay-offline": return;
1581
+ case "connect":
1582
+ await this.connectToTransport(this.currentContext, "network resume");
1583
+ return;
1584
+ }
1259
1585
  }
1260
1586
  async close() {
1261
1587
  this.logger.debug("[ConfigDirectorClient] close() has been called, closing connection to server");
@@ -1296,6 +1622,59 @@ const mapToTelemetryValue = (ev) => {
1296
1622
  if (stringValue && stringValue.length <= CONFIG_VALUE_MAX_LENGTH) return { value: stringValue };
1297
1623
  return ev.valueId ? { valueId: ev.valueId } : { value: stringValue };
1298
1624
  };
1625
+ var InvalidContextSubmissions = class {
1626
+ countsByKindSample = /* @__PURE__ */ new Map();
1627
+ countBeyondSampleLimit = 0;
1628
+ startTime;
1629
+ record(kindSample) {
1630
+ this.startTime ??= /* @__PURE__ */ new Date();
1631
+ return this.add(kindSample, 1);
1632
+ }
1633
+ absorb(entries) {
1634
+ for (const entry of entries) {
1635
+ if (this.startTime === void 0 || entry.startTime < this.startTime) this.startTime = entry.startTime;
1636
+ this.add(entry.event.kindSample, entry.count);
1637
+ }
1638
+ }
1639
+ add(kindSample, count) {
1640
+ if (kindSample !== void 0) {
1641
+ const knownCount = this.countsByKindSample.get(kindSample);
1642
+ if (knownCount !== void 0) {
1643
+ this.countsByKindSample.set(kindSample, knownCount + count);
1644
+ return "known-kind-sample";
1645
+ }
1646
+ if (this.countsByKindSample.size < 20) {
1647
+ this.countsByKindSample.set(kindSample, count);
1648
+ return "new-kind-sample";
1649
+ }
1650
+ }
1651
+ const firstBeyondSampleLimit = this.countBeyondSampleLimit === 0;
1652
+ this.countBeyondSampleLimit += count;
1653
+ return firstBeyondSampleLimit ? "first-beyond-sample-limit" : "beyond-sample-limit";
1654
+ }
1655
+ takeSnapshot() {
1656
+ const endTime = /* @__PURE__ */ new Date();
1657
+ const startTime = this.startTime ?? endTime;
1658
+ const entryOf = (count, kindSample) => ({
1659
+ startTime,
1660
+ endTime,
1661
+ count,
1662
+ event: {
1663
+ reason: "invalid-context-kind",
1664
+ kindSample
1665
+ }
1666
+ });
1667
+ const sampledEntries = Array.from(this.countsByKindSample, ([kindSample, count]) => entryOf(count, kindSample));
1668
+ const entries = this.countBeyondSampleLimit > 0 ? [...sampledEntries, entryOf(this.countBeyondSampleLimit)] : sampledEntries;
1669
+ this.clear();
1670
+ return entries;
1671
+ }
1672
+ clear() {
1673
+ this.countsByKindSample.clear();
1674
+ this.countBeyondSampleLimit = 0;
1675
+ this.startTime = void 0;
1676
+ }
1677
+ };
1299
1678
  //#endregion
1300
1679
  //#region ../js-browser-client/src/telemetry/WebWorkerTelemetryClient.ts
1301
1680
  const DEFAULT_WORKER_CLOSE_TIMEOUT = 6e3;
@@ -1306,6 +1685,7 @@ var WebWorkerTelemetryClient = class {
1306
1685
  turnedOff = false;
1307
1686
  pendingEvaluations = [];
1308
1687
  pendingEvaluationLimit;
1688
+ pendingInvalidContextSubmissions = new InvalidContextSubmissions();
1309
1689
  closePromise;
1310
1690
  closeResolve;
1311
1691
  closeTimer;
@@ -1337,7 +1717,7 @@ var WebWorkerTelemetryClient = class {
1337
1717
  return;
1338
1718
  }
1339
1719
  try {
1340
- return new Worker(URL.createObjectURL(new Blob(["(function(){let e=async(e,t)=>e.valueId?{valueId:e.valueId}:e.value?e.value.length<=500&&e.type!=`json`?{value:e.value}:{valueId:await t(e.value)}:e,t=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},n=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},r=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},i=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var a=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.sdkIdentity=e.sdkIdentity,this.metaContext=e.metaContext,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,metaContext:{sdkName:this.sdkIdentity.sdkName,sdkVersion:this.sdkIdentity.sdkVersion,appName:this.metaContext.appName,appVersion:this.metaContext.appVersion},context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return t(e.discreteEvents)&&t(e.aggregatedEvents)&&n(e.droppedEvents)}async sendReport(e){try{let t=await r(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=i(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500&&e!=429}},o=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},s=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},c=class{constructor(e){this.aggregator=new o,this.collectEvents=!0,this.started=!1,this.logger=e.logger,this.evaluationEventQueue=new s(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4,this.initialFlushIntervalDelay=e.initialFlushIntervalDelay??5e3}start(){this.started||!this.collectEvents||(this.started=!0,this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.initialFlushIntervalDelay))}async flushAndScheduleNext(){if(!this.collectEvents)return;let e;try{e=await this.flush()}catch(t){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events: `,t),e={success:!1,fatalError:!1}}e.fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.collectEvents&&(this.cancelScheduledFlush(),this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay))}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout);try{await this.flush()}catch(e){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events during close: `,e)}this.evaluationEventQueue.clear()}dispose(){this.close()}},l=class{constructor(e){this.valueIdGenerator=e,this.valueIdGenerator=e}async process(t){let n=await Promise.all(t.events.map(async t=>({...t,defaultValue:await e(t.defaultValue,this.valueIdGenerator),evaluatedValue:await e(t.evaluatedValue,this.valueIdGenerator)})));return{...t,events:n}}},u=class extends c{constructor(e){super(e),this.reporter=new a(e),this.evaluationEventSnapshotPreprocessor=new l(e.valueIdGenerator)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e)},droppedEvents:{evaluatedConfig:e.droppedCount}})}};let d=(e,t)=>new URL(e,t?.toString()),f=(e,t)=>{let n=BigInt(0),r=BigInt(62),i=Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),a=BigInt(`0x`+i),o=``;for(;a>n;)o=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`[Number(a%r)]+o,a/=r;return o.padStart(t,`0`)},p=Math.ceil(128/Math.log2(62)),m=new TextEncoder,h=async e=>{if(typeof crypto>`u`||crypto.subtle==null)return;let t=await crypto.subtle.digest(`SHA-256`,m.encode(e?.toString()??``));return f(new Uint8Array(t,0,16),p)},g,_=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),v=e=>{try{self.postMessage(e)}catch{self.postMessage(_(e.payload.level,e.payload.message))}},y={debug:function(e,...t){v(_(`debug`,e,t))},info:function(e,...t){v(_(`info`,e,t))},warn:function(e,...t){v(_(`warn`,e,t))},error:function(e,...t){v(_(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!g){y.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(y.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:if(g){y.warn(`[TelemetryWorker] Received a duplicate Initialize event, ignoring it`);break}g=new u({...t.payload,baseUrl:d(t.payload.baseUrl),urlFactory:d,valueIdGenerator:h,logger:y}),g.start();break;case`EvaluatedConfigEvent`:g?.evaluatedConfig(t.payload);break;case`UpdateContext`:g?.updateContext(t.payload.context);break;case`Flush`:g?.forceFlush();break;case`Close`:(g?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1720
+ return new Worker(URL.createObjectURL(new Blob(["(function(){let e=async(e,t)=>e.valueId?{valueId:e.valueId}:e.value?e.value.length<=500&&e.type!=`json`?{value:e.value}:{valueId:await t(e.value)}:e,t=e=>{let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]?.length??0)>0)return!1;return!0},n=e=>{if(!e)return!0;let t=Object.keys(e);if(t.length==0)return!0;for(let n of t)if((e[n]??0)>0)return!1;return!0},r=async(e,t,n,r)=>{let i=new AbortController,a=setTimeout(()=>{r.debug(`[fetchWithTimeout] Reached timeout, aborting request`),i.abort()},e);try{let e=await fetch(typeof t==`string`||t instanceof Request?t:t.toString(),{...n,signal:i.signal});return clearTimeout(a),e}catch(e){throw r.warn(`[fetchWithTimeout] Fetch error: `,e),clearTimeout(a),e}},i=e=>e?.name===`NotAllowedError`?!0:e instanceof TypeError;var a=class{constructor(e){this.executeRequests=!0,this.sdkKey=e.sdkKey,this.logger=e.logger,this.sdkIdentity=e.sdkIdentity,this.metaContext=e.metaContext,this.url=e.urlFactory(`client/telemetry/v1`,e.baseUrl)}async report({context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r}){if(!this.executeRequests)return{success:!1,fatalError:!0};let i={clientSdkKey:this.sdkKey,metaContext:{sdkName:this.sdkIdentity.sdkName,sdkVersion:this.sdkIdentity.sdkVersion,appName:this.metaContext.appName,appVersion:this.metaContext.appVersion},context:e,discreteEvents:t,aggregatedEvents:n,droppedEvents:r};if(this.isReportEmpty(i))return{success:!0,fatalError:!1};let a=await this.sendReport(i);return a.fatalError&&(this.executeRequests=!1),a}isReportEmpty(e){return t(e.discreteEvents)&&t(e.aggregatedEvents)&&n(e.droppedEvents)}async sendReport(e){try{let t=await r(5e3,this.url,{method:`POST`,body:JSON.stringify(e),keepalive:!0},this.logger),n=this.isStatusFatal(t.status);return n&&this.logger.warn(`[EventReporter] Received a fatal status response (${t.status}) from the telemetry endpoint. No more telemetry data will be sent.`),{success:t.ok,fatalError:n}}catch(e){let t=i(e);return this.logger.warn(`[EventReporter] Error attempting to send telemetry data: ${e}`,{error:e}),{success:!1,fatalError:t}}}isStatusFatal(e){return!!e&&e>=400&&e<500&&e!=429}},o=class{aggregate(e){if(e.events.length==0)return[];let t=new Map;for(let n of e.events){let e=JSON.stringify(n),r=t.get(e)?.count||0;t.set(e,{event:n,count:r+1})}return Array.from(t).map(([,t])=>({startTime:e.startTime,endTime:e.endTime,count:t.count,event:t.event}))}},s=class{constructor(e){this._events=[],this._droppedEventCount=0,this.limit=e??1e3}get events(){return this._events}get reachedLimit(){return this._events.length>=this.limit}get droppedEventCount(){return this._droppedEventCount}push(...e){this.startTime||=new Date;let t=Math.max(0,e.length-this.limit),n=e.slice(t,e.length);if(this._droppedEventCount+=t,this._events.length+n.length>this.limit){let e=this._events.length+n.length-this.limit;this._events.splice(0,e),this._droppedEventCount+=e}return this._events.push(...n)}takeSnapshot(){let e=new Date,t=this.startTime??e,n=this._events.splice(0),r=this._droppedEventCount;return this.startTime=void 0,this._droppedEventCount=0,{startTime:t,endTime:e,events:n,droppedCount:r}}clear(){this._events=[],this.startTime=void 0,this._droppedEventCount=0}},c=class{constructor(e){this.aggregator=new o,this.collectEvents=!0,this.started=!1,this.logger=e.logger,this.evaluationEventQueue=new s(e.evaluationQueueLimit??1e3),this.flushIntervalDelay=e.flushIntervalDelay??3e4,this.initialFlushIntervalDelay=e.initialFlushIntervalDelay??5e3}start(){this.started||!this.collectEvents||(this.started=!0,this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.initialFlushIntervalDelay))}async flushAndScheduleNext(){if(!this.collectEvents)return;let e;try{e=await this.flush()}catch(t){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events: `,t),e={success:!1,fatalError:!1}}e.fatalError?(this.collectEvents=!1,this.close(),this.logger.warn(`[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.`)):this.collectEvents&&(this.cancelScheduledFlush(),this.flushTimeout=setTimeout(()=>this.flushAndScheduleNext(),this.flushIntervalDelay))}cancelScheduledFlush(){clearTimeout(this.flushTimeout)}async close(){this.collectEvents=!1,clearTimeout(this.flushTimeout);try{await this.flush()}catch(e){this.logger.warn(`[TelemetryEventCollector] Error flushing telemetry events during close: `,e)}this.evaluationEventQueue.clear()}dispose(){this.close()}},l=class{constructor(e){this.valueIdGenerator=e,this.valueIdGenerator=e}async process(t){let n=await Promise.all(t.events.map(async t=>({...t,defaultValue:await e(t.defaultValue,this.valueIdGenerator),evaluatedValue:await e(t.evaluatedValue,this.valueIdGenerator)})));return{...t,events:n}}},u=class{constructor(){this.countsByKindSample=new Map,this.countBeyondSampleLimit=0}record(e){return this.startTime??=new Date,this.add(e,1)}absorb(e){for(let t of e)(this.startTime===void 0||t.startTime<this.startTime)&&(this.startTime=t.startTime),this.add(t.event.kindSample,t.count)}add(e,t){if(e!==void 0){let n=this.countsByKindSample.get(e);if(n!==void 0)return this.countsByKindSample.set(e,n+t),`known-kind-sample`;if(this.countsByKindSample.size<20)return this.countsByKindSample.set(e,t),`new-kind-sample`}let n=this.countBeyondSampleLimit===0;return this.countBeyondSampleLimit+=t,n?`first-beyond-sample-limit`:`beyond-sample-limit`}takeSnapshot(){let e=new Date,t=this.startTime??e,n=(n,r)=>({startTime:t,endTime:e,count:n,event:{reason:`invalid-context-kind`,kindSample:r}}),r=Array.from(this.countsByKindSample,([e,t])=>n(t,e)),i=this.countBeyondSampleLimit>0?[...r,n(this.countBeyondSampleLimit)]:r;return this.clear(),i}clear(){this.countsByKindSample.clear(),this.countBeyondSampleLimit=0,this.startTime=void 0}},d=class extends c{constructor(e){super(e),this.invalidContextSubmissions=new u,this.reporter=new a(e),this.evaluationEventSnapshotPreprocessor=new l(e.valueIdGenerator)}async updateContext(e){this.cancelScheduledFlush(),await this.flush(),this._context=e,await this.flushAndScheduleNext()}evaluatedConfig(e){this.collectEvents&&this.evaluationEventQueue.push(e)}recordInvalidContext(e){this.invalidContextSubmissions.record(e)}absorbInvalidContextSubmissions(e){this.invalidContextSubmissions.absorb(e)}async forceFlush(){this.cancelScheduledFlush(),await this.flushAndScheduleNext()}async flush(){if(!this.reporter)return{success:!1,fatalError:!1};let e=await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());return await this.reporter?.report({context:this._context,discreteEvents:{},aggregatedEvents:{evaluatedConfig:this.aggregator.aggregate(e),invalidContext:this.invalidContextSubmissions.takeSnapshot()},droppedEvents:{evaluatedConfig:e.droppedCount}})}};let f=(e,t)=>new URL(e,t?.toString()),p=(e,t)=>{let n=BigInt(0),r=BigInt(62),i=Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),a=BigInt(`0x`+i),o=``;for(;a>n;)o=`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`[Number(a%r)]+o,a/=r;return o.padStart(t,`0`)},m=Math.ceil(128/Math.log2(62)),h=new TextEncoder,g=async e=>{if(typeof crypto>`u`||crypto.subtle==null)return;let t=await crypto.subtle.digest(`SHA-256`,h.encode(e?.toString()??``));return p(new Uint8Array(t,0,16),m)},_,v=(e,t,...n)=>({type:`Log`,payload:{level:e,message:t,args:n}}),y=e=>{try{self.postMessage(e)}catch{self.postMessage(v(e.payload.level,e.payload.message))}},b={debug:function(e,...t){y(v(`debug`,e,t))},info:function(e,...t){y(v(`info`,e,t))},warn:function(e,...t){y(v(`warn`,e,t))},error:function(e,...t){y(v(`error`,e,t))}};addEventListener(`message`,e=>{let t=e.data;if(t.type!=`Initialize`&&!_){b.info(`[TelemetryWorker] Ignoring ${t.type} because no collector is configured`);return}switch(b.debug(`[TelemetryWorker] Received '${t.type}' event:`,t),t.type){case`Initialize`:if(_){b.warn(`[TelemetryWorker] Received a duplicate Initialize event, ignoring it`);break}_=new d({...t.payload,baseUrl:f(t.payload.baseUrl),urlFactory:f,valueIdGenerator:g,logger:b}),_.start();break;case`EvaluatedConfigEvent`:_?.evaluatedConfig(t.payload);break;case`InvalidContextSubmissions`:_?.absorbInvalidContextSubmissions(t.payload.entries);break;case`UpdateContext`:_?.updateContext(t.payload.context);break;case`Flush`:_?.forceFlush();break;case`Close`:(_?.close()??Promise.resolve()).then(()=>{self.postMessage({type:`Closed`}),self.close()});break}})})();"], { type: "application/javascript" })));
1341
1721
  } catch (error) {
1342
1722
  this.turnOff("[WebWorkerTelemetryClient] Telemetry is off because the telemetry Web Worker could not be created: ", error);
1343
1723
  return;
@@ -1364,8 +1744,19 @@ var WebWorkerTelemetryClient = class {
1364
1744
  }
1365
1745
  for (const evaluation of this.pendingEvaluations) worker.postMessage(evaluation);
1366
1746
  this.pendingEvaluations.length = 0;
1747
+ this.forwardInvalidContextSubmissions(worker);
1367
1748
  return worker;
1368
1749
  }
1750
+ forwardInvalidContextSubmissions(worker) {
1751
+ if (!worker) return;
1752
+ const entries = this.pendingInvalidContextSubmissions.takeSnapshot();
1753
+ if (entries.length === 0) return;
1754
+ const submissionsMessage = {
1755
+ type: "InvalidContextSubmissions",
1756
+ payload: { entries }
1757
+ };
1758
+ worker.postMessage(submissionsMessage);
1759
+ }
1369
1760
  async updateContext(value) {
1370
1761
  if (this.closePromise) return;
1371
1762
  const updateContextMessage = {
@@ -1387,6 +1778,10 @@ var WebWorkerTelemetryClient = class {
1387
1778
  if (this.pendingEvaluations.length >= this.pendingEvaluationLimit) this.pendingEvaluations.shift();
1388
1779
  this.pendingEvaluations.push(evaluatedConfigMessage);
1389
1780
  }
1781
+ recordInvalidContext(kindSample) {
1782
+ this.pendingInvalidContextSubmissions.record(kindSample);
1783
+ this.forwardInvalidContextSubmissions(this.worker);
1784
+ }
1390
1785
  sanitizeEvaluatedConfigEvent(event) {
1391
1786
  return {
1392
1787
  ...event,
@@ -1485,27 +1880,28 @@ const generateValueId = async (v) => {
1485
1880
  };
1486
1881
  //#endregion
1487
1882
  //#region ../js-browser-client/src/BrowserConfigDirectorClient.ts
1488
- const createBrowserClient = (clientSdkKey, sdkOptions, clientOptions) => {
1489
- const logger = clientOptions?.logger ?? createDefaultLogger$1();
1490
- const baseUrl = parseUrl(clientOptions?.connection?.url) ?? "https://client-sdk-api.configdirector.com";
1491
- return new DefaultConfigDirectorClient(new WebWorkerTelemetryClient({
1883
+ /**
1884
+ * Creates a browser client. With a client SDK key it connects to ConfigDirector; with
1885
+ * {@link OfflineClientOptions} alone it runs in `offline` mode and serves `connection.values`. An
1886
+ * absent key with `clientOptions` is the same as the options alone when their mode is `offline`,
1887
+ * and fails like a missing key otherwise.
1888
+ */
1889
+ const createBrowserClient = (clientSdkKeyOrOptions, sdkOptions, clientOptions) => {
1890
+ return DefaultConfigDirectorClient.from(({ clientSdkKey, baseUrl, logger, metadata }) => new WebWorkerTelemetryClient({
1492
1891
  sdkKey: clientSdkKey,
1493
1892
  sdkIdentity: {
1494
1893
  sdkName: sdkOptions.sdkName,
1495
1894
  sdkVersion: sdkOptions.sdkVersion
1496
1895
  },
1497
1896
  metaContext: {
1498
- appName: clientOptions?.metadata?.appName,
1499
- appVersion: clientOptions?.metadata?.appVersion
1897
+ appName: metadata?.appName,
1898
+ appVersion: metadata?.appVersion
1500
1899
  },
1501
1900
  logger,
1502
1901
  baseUrl,
1503
1902
  urlFactory: defaultUrlFactory,
1504
1903
  valueIdGenerator: generateValueId
1505
- }), clientSdkKey, sdkOptions, {
1506
- ...clientOptions,
1507
- logger
1508
- });
1904
+ }), clientSdkKeyOrOptions, sdkOptions, clientOptions);
1509
1905
  };
1510
1906
  //#endregion
1511
1907
  //#region ../js-client-core/src/installedTestClient.ts
@@ -1537,6 +1933,13 @@ const decideMountAction = (client, context) => {
1537
1933
  return "none";
1538
1934
  };
1539
1935
  //#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
1540
1943
  //#region src/runtime/app/composables/useConfigDirectorClient.ts
1541
1944
  const useConfigDirectorClient = () => {
1542
1945
  const { $configDirectorClient } = useNuxtApp();
@@ -1555,7 +1958,7 @@ const useConfigDirectorContext = () => {
1555
1958
  const rawContext = toRaw(context);
1556
1959
  const oldContext = JSON.stringify(state.value);
1557
1960
  const newContext = JSON.stringify(rawContext);
1558
- state.value = rawContext;
1961
+ state.value = admittedContextOrNone(rawContext);
1559
1962
  if (oldContext != newContext) {
1560
1963
  const timeout = getEffectiveTimeout(options?.timeoutMilliseconds);
1561
1964
  let timer;
@@ -1581,13 +1984,16 @@ const createDefaultLogger = (level) => {
1581
1984
  return logger;
1582
1985
  };
1583
1986
  //#endregion
1987
+ //#region src/runtime/offline-values-route.ts
1988
+ const OFFLINE_VALUES_ROUTE = "/_configdirector/offline-values";
1989
+ //#endregion
1584
1990
  //#region src/runtime/plugin.client.ts
1585
1991
  const DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS = 2e3;
1586
- var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1992
+ var plugin_client_default = defineNuxtPlugin(async (nuxtApp) => {
1587
1993
  const runtimeConfig = useRuntimeConfig();
1588
1994
  const logger = createDefaultLogger(runtimeConfig.public?.configdirector?.logLevel);
1589
1995
  const installedTestClient = readInstalledTestClient();
1590
- const client = installedTestClient ?? buildClient(runtimeConfig.public?.configdirector, logger);
1996
+ const client = installedTestClient ?? await buildClient(runtimeConfig.public?.configdirector, logger);
1591
1997
  logger.debug(installedTestClient ? "Installed ConfigDirector Nuxt plugin with a test client" : "Installed ConfigDirector Nuxt plugin");
1592
1998
  const readyStatus = shallowRef(client.isReady ? "ready" : "loading");
1593
1999
  client.on("clientReady", () => {
@@ -1608,29 +2014,58 @@ var plugin_client_default = defineNuxtPlugin((nuxtApp) => {
1608
2014
  configDirectorClientReadyStatus: readonly(readyStatus)
1609
2015
  } };
1610
2016
  });
1611
- const buildClient = (configdirector, logger) => {
1612
- if (!configdirector?.clientSdkKey) {
2017
+ const buildClient = async (configdirector, logger) => {
2018
+ const browserHalf = await browserHalfConnectionOf(configdirector, logger);
2019
+ if (browserHalf.mode !== "offline" && !configdirector?.clientSdkKey) {
1613
2020
  const message = "The ConfigDirector clientSdkKey must be configured for the plugin to initialize. You can provide it in nuxt.config.ts under 'runtimeConfig.public.configdirector.clientSdkKey' or as a runtime environment variable named NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY";
1614
2021
  logger.error(message);
1615
2022
  throw new ConfigDirectorInitializationError(message);
1616
2023
  }
1617
- const { clientSdkKey, appName, appVersion, baseUrl, connection } = configdirector;
1618
- return createBrowserClient(clientSdkKey, {
2024
+ return createBrowserClient(configdirector?.clientSdkKey || void 0, {
1619
2025
  sdkName: "nuxt-sdk",
1620
- sdkVersion: "1.9.2"
2026
+ sdkVersion: "1.11.0"
1621
2027
  }, {
1622
2028
  metadata: {
1623
- appName,
1624
- appVersion
2029
+ appName: configdirector?.appName,
2030
+ appVersion: configdirector?.appVersion
1625
2031
  },
1626
2032
  connection: {
1627
- url: baseUrl || void 0,
1628
- mode: connection?.mode || void 0,
1629
- pollingInterval: connection?.pollingInterval || void 0,
1630
- timeout: connection?.timeout || DEFAULT_INITIALIZATION_TIMEOUT_MILLISECONDS
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
1631
2038
  },
1632
2039
  logger
1633
2040
  });
1634
2041
  };
2042
+ const browserHalfConnectionOf = async (configdirector, logger) => {
2043
+ const givenConnection = {
2044
+ mode: configdirector?.connection?.mode || void 0,
2045
+ values: configdirector?.connection?.values
2046
+ };
2047
+ const source = decideBrowserHalfSource(givenConnection, configdirector?.serverConnectionMode);
2048
+ logger.debug(`Browser half source: ${source}`);
2049
+ switch (source) {
2050
+ case "as-given": return givenConnection;
2051
+ case "inherit-offline": return {
2052
+ mode: "offline",
2053
+ values: await fetchServerOfflineValues(logger)
2054
+ };
2055
+ case "connect": return {
2056
+ mode: void 0,
2057
+ values: void 0
2058
+ };
2059
+ }
2060
+ };
2061
+ const fetchServerOfflineValues = async (logger) => {
2062
+ try {
2063
+ return await $fetch(OFFLINE_VALUES_ROUTE);
2064
+ } catch (error) {
2065
+ const message = `The server runs ConfigDirector in offline mode, but its values could not be fetched from ${OFFLINE_VALUES_ROUTE}: ${error instanceof Error ? error.message : String(error)}. A statically generated site has no server to serve them; set 'runtimeConfig.public.configdirector.connection' to { mode: "offline", values } in nuxt.config.ts to give the browser its own values.`;
2066
+ logger.error(message);
2067
+ throw new ConfigDirectorInitializationError(message);
2068
+ }
2069
+ };
1635
2070
  //#endregion
1636
2071
  export { plugin_client_default as default };