@configdirector/react-native-sdk 1.6.1 → 1.8.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.
@@ -89,6 +89,13 @@ var ConfigDirectorValidationError = class ConfigDirectorValidationError extends
89
89
  Object.setPrototypeOf(this, ConfigDirectorValidationError.prototype);
90
90
  }
91
91
  };
92
+ var ConfigDirectorInitializationError = class ConfigDirectorInitializationError extends Error {
93
+ constructor(message) {
94
+ super(message);
95
+ _defineProperty(this, "name", "ConfigDirectorInitializationError");
96
+ Object.setPrototypeOf(this, ConfigDirectorInitializationError.prototype);
97
+ }
98
+ };
92
99
  //#endregion
93
100
  //#region ../js-client-core/src/errors.ts
94
101
  const isFetchErrorFatal = (fetchError) => {
@@ -438,11 +445,15 @@ var StreamingTransport = class {
438
445
  _defineProperty(this, "eventEmitter", new Emitter());
439
446
  _defineProperty(this, "url", void 0);
440
447
  _defineProperty(this, "timeoutTimer", void 0);
448
+ _defineProperty(this, "connectionGeneration", 0);
441
449
  this.options = options;
442
450
  this.logger = options.logger;
443
451
  this.url = options.resolveUrl("client/sse/v1", options.baseUrl);
444
452
  }
445
453
  async connect(context, timeout) {
454
+ const generation = ++this.connectionGeneration;
455
+ const clientSession = await this.options.clientSession;
456
+ if (generation !== this.connectionGeneration) return this;
446
457
  if (this.eventSource) this.close();
447
458
  const eventSourcePromise = new Promise((resolve, reject) => {
448
459
  this.eventSource = createEventSourceClient({
@@ -454,7 +465,8 @@ var StreamingTransport = class {
454
465
  givenContext: context,
455
466
  metaContext: this.options.metaContext,
456
467
  clientSdkKey: this.options.clientSdkKey,
457
- instanceId: this.options.instanceId
468
+ instanceId: clientSession.instanceId,
469
+ ephemeral: clientSession.ephemeral
458
470
  }),
459
471
  onMessage: ({ data }) => {
460
472
  this.dispatchMessage(data);
@@ -520,6 +532,7 @@ var StreamingTransport = class {
520
532
  }
521
533
  close() {
522
534
  var _this$eventSource;
535
+ this.connectionGeneration++;
523
536
  (_this$eventSource = this.eventSource) === null || _this$eventSource === void 0 || _this$eventSource.close();
524
537
  clearTimeout(this.timeoutTimer);
525
538
  }
@@ -529,6 +542,187 @@ var StreamingTransport = class {
529
542
  }
530
543
  };
531
544
  //#endregion
545
+ //#region ../shared/src/native-values.ts
546
+ const encodeNativeValue = (key, value) => {
547
+ if (key.trim().length == 0) throw new ConfigDirectorValidationError("Invalid config key. The key of a value cannot be empty.");
548
+ return encode(key, value);
549
+ };
550
+ const encode = (key, value) => {
551
+ switch (typeof value) {
552
+ case "boolean": return {
553
+ type: "boolean",
554
+ value: value ? "true" : "false"
555
+ };
556
+ case "number": return encodeNumber(key, value);
557
+ case "string": return {
558
+ type: "string",
559
+ value
560
+ };
561
+ case "object": if (value !== null) {
562
+ assertJsonContents(key, value, "");
563
+ return {
564
+ type: "json",
565
+ value: JSON.stringify(value)
566
+ };
567
+ }
568
+ }
569
+ throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)}. Use a boolean, a number, a string, a plain object, or an array.`);
570
+ };
571
+ const encodeNumber = (key, value) => {
572
+ if (!Number.isFinite(value)) throw new ConfigDirectorValidationError(`Invalid value for '${key}': ${describe(value)} is not a finite number.`);
573
+ if (Number.isInteger(value)) {
574
+ 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.`);
575
+ return {
576
+ type: "integer",
577
+ value: String(value)
578
+ };
579
+ }
580
+ return {
581
+ type: "float",
582
+ value: toPlainDecimal(value)
583
+ };
584
+ };
585
+ const toPlainDecimal = (value) => {
586
+ const text = String(value);
587
+ const exponentIndex = text.indexOf("e-");
588
+ if (exponentIndex < 0) return text;
589
+ const sign = text.startsWith("-") ? "-" : "";
590
+ const [integerDigits = "", fractionDigits = ""] = text.slice(sign.length, exponentIndex).split(".");
591
+ const leadingZeros = Number(text.slice(exponentIndex + 2)) - integerDigits.length;
592
+ return `${sign}0.${"0".repeat(leadingZeros)}${integerDigits}${fractionDigits}`;
593
+ };
594
+ const assertJsonContents = (key, value, path) => {
595
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
596
+ if (typeof value === "number") {
597
+ if (!Number.isFinite(value)) throw invalidJsonContents(key, path, `${describe(value)} is not a finite number`);
598
+ return;
599
+ }
600
+ if (Array.isArray(value)) {
601
+ value.forEach((item, index) => assertJsonContents(key, item, `${path}[${index}]`));
602
+ return;
603
+ }
604
+ if (isPlainObject(value)) {
605
+ for (const [property, item] of Object.entries(value)) assertJsonContents(key, item, path ? `${path}.${property}` : property);
606
+ return;
607
+ }
608
+ throw invalidJsonContents(key, path, `${describe(value)} cannot be encoded as JSON`);
609
+ };
610
+ 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.`);
611
+ const isPlainObject = (value) => {
612
+ if (typeof value !== "object" || value === null) return false;
613
+ const prototype = Object.getPrototypeOf(value);
614
+ return prototype === Object.prototype || prototype === null;
615
+ };
616
+ const describe = (value) => {
617
+ if (value === null) return "null";
618
+ if (typeof value === "function") return "a function";
619
+ if (typeof value === "object") {
620
+ var _value$constructor$na, _value$constructor;
621
+ return `a ${(_value$constructor$na = (_value$constructor = value.constructor) === null || _value$constructor === void 0 ? void 0 : _value$constructor.name) !== null && _value$constructor$na !== void 0 ? _value$constructor$na : "object"} instance`;
622
+ }
623
+ if (typeof value === "symbol") return "a symbol";
624
+ return String(value);
625
+ };
626
+ //#endregion
627
+ //#region ../js-client-core/src/native-values.ts
628
+ const nativeValueConfigState = (id, key, value, valueId) => {
629
+ const encoded = encodeNativeValue(key, value);
630
+ return {
631
+ id,
632
+ key,
633
+ type: encoded.type,
634
+ value: encoded.value,
635
+ valueId
636
+ };
637
+ };
638
+ //#endregion
639
+ //#region ../js-client-core/src/OfflineTransport.ts
640
+ const OFFLINE_ENVIRONMENT = {
641
+ environmentId: "offline",
642
+ projectId: "offline"
643
+ };
644
+ var OfflineTransport = class {
645
+ constructor(source) {
646
+ this.source = source;
647
+ _defineProperty(this, "emitter", new Emitter());
648
+ _defineProperty(this, "on", (name, handler) => this.emitter.on(name, handler));
649
+ _defineProperty(this, "off", (name, handler) => this.emitter.off(name, handler));
650
+ }
651
+ async connect() {
652
+ const configs = loadOfflineConfigs(this.source);
653
+ this.emitter.emit("configSetReceived", {
654
+ ...OFFLINE_ENVIRONMENT,
655
+ kind: "full",
656
+ configs
657
+ });
658
+ return this;
659
+ }
660
+ clear() {
661
+ this.emitter.clear();
662
+ }
663
+ close() {}
664
+ dispose() {
665
+ this.clear();
666
+ }
667
+ };
668
+ const loadOfflineConfigs = (source) => {
669
+ switch (source.kind) {
670
+ case "in-code-defaults": return {};
671
+ case "values": return encodeOfflineValues(source.values);
672
+ case "rejected": throw new ConfigDirectorInitializationError(source.message);
673
+ }
674
+ };
675
+ const encodeOfflineValues = (values) => {
676
+ try {
677
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, nativeValueConfigState(`offline:${key}`, key, value, void 0)]));
678
+ } catch (error) {
679
+ if (error instanceof ConfigDirectorValidationError) throw new ConfigDirectorInitializationError(`The offline values could not be loaded. ${error.message}`);
680
+ throw error;
681
+ }
682
+ };
683
+ //#endregion
684
+ //#region ../shared/src/constants.ts
685
+ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
686
+ //#endregion
687
+ //#region ../js-client-core/src/connection-plan.ts
688
+ const planConnection = (clientSdkKey, connection, urlFactory) => {
689
+ var _connection$mode;
690
+ if ((connection === null || connection === void 0 ? void 0 : connection.mode) === "offline") return {
691
+ kind: "stay-offline",
692
+ source: offlineSourceOf(connection)
693
+ };
694
+ const mode = (_connection$mode = connection === null || connection === void 0 ? void 0 : connection.mode) !== null && _connection$mode !== void 0 ? _connection$mode : "streaming";
695
+ 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");
696
+ if ((connection === null || connection === void 0 ? void 0 : connection.values) !== void 0) return {
697
+ kind: "stay-offline",
698
+ source: {
699
+ kind: "rejected",
700
+ message: `The connection option 'values' applies only when the connection mode is 'offline', but the mode is '${mode}'.`
701
+ }
702
+ };
703
+ return {
704
+ kind: "connect",
705
+ mode,
706
+ clientSdkKey,
707
+ baseUrl: baseUrlOf(connection === null || connection === void 0 ? void 0 : connection.url, urlFactory)
708
+ };
709
+ };
710
+ const baseUrlOf = (url, urlFactory) => {
711
+ if (!url) return CLIENT_BASE_URL;
712
+ try {
713
+ return urlFactory(url);
714
+ } catch (error) {
715
+ throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
716
+ }
717
+ };
718
+ const offlineSourceOf = (connection) => {
719
+ if (connection.values !== void 0) return {
720
+ kind: "values",
721
+ values: connection.values
722
+ };
723
+ return { kind: "in-code-defaults" };
724
+ };
725
+ //#endregion
532
726
  //#region ../shared/src/value-parser.ts
533
727
  const getRequestedType = (defaultValue) => {
534
728
  const baseType = typeof defaultValue;
@@ -752,12 +946,17 @@ const createDefaultLogger = (level, messageDecorator) => {
752
946
  return new DefaultConsoleLogger(level !== null && level !== void 0 ? level : "warn", messageDecorator !== null && messageDecorator !== void 0 ? messageDecorator : new LogMessageDecorator());
753
947
  };
754
948
  //#endregion
949
+ //#region ../js-client-core/src/telemetry/discarding.ts
950
+ const discardingTelemetryClient = {
951
+ updateContext: async () => {},
952
+ evaluatedConfig: () => {},
953
+ recordInvalidContext: () => {},
954
+ close: async () => {}
955
+ };
956
+ //#endregion
755
957
  //#region ../shared/src/url.ts
756
958
  const defaultUrlFactory = (input, base) => new URL(input, base === null || base === void 0 ? void 0 : base.toString());
757
959
  //#endregion
758
- //#region ../shared/src/constants.ts
759
- const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
760
- //#endregion
761
960
  //#region ../shared/src/transport/pollingInterval.ts
762
961
  const resolvePollingInterval = (configuredSeconds, defaults) => {
763
962
  if (configuredSeconds === void 0) return {
@@ -783,10 +982,11 @@ const generateInstanceId = () => {
783
982
  });
784
983
  };
785
984
  //#endregion
786
- //#region ../js-client-core/src/instance-id.ts
985
+ //#region ../js-client-core/src/client-session.ts
787
986
  const STORAGE_KEY = "configdirector-sdk:instance-id";
788
987
  const RECORD_VERSION = 1;
789
988
  const MAX_AGE_MS = 360 * 60 * 1e3;
989
+ const STORAGE_DEADLINE_MS = 1e3;
790
990
  const CHECKSUM_SALT = "configdirector.instanceId.v1";
791
991
  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}$/;
792
992
  const fnv1a = (input, offsetBasis) => {
@@ -802,7 +1002,7 @@ const checksumFor = (id, createdAt) => {
802
1002
  const low = fnv1a(payload, 2166136261);
803
1003
  return fnv1a(`${low}|${payload}`, 16777619).toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
804
1004
  };
805
- const getLocalStorage = () => {
1005
+ const localStorageIfAvailable = () => {
806
1006
  try {
807
1007
  var _localStorage;
808
1008
  return (_localStorage = globalThis.localStorage) !== null && _localStorage !== void 0 ? _localStorage : void 0;
@@ -818,34 +1018,37 @@ const isUsableRecord = (record) => {
818
1018
  if (!UUID_V4_PATTERN.test(id)) return false;
819
1019
  return checksum === checksumFor(id, createdAt);
820
1020
  };
821
- const readStoredInstanceId = (storage, logger) => {
1021
+ const readStoredInstanceId = async (storage, logger) => {
822
1022
  let raw;
823
1023
  try {
824
- raw = storage.getItem(STORAGE_KEY);
1024
+ raw = await storage.getItem(STORAGE_KEY);
825
1025
  } catch (error) {
826
- logger === null || logger === void 0 || logger.debug("[ConfigDirectorClient] Could not read the stored instance id, generating a new one", error);
827
- return;
1026
+ logger.debug("[ConfigDirectorClient] Could not read the stored instance id, using an ephemeral one", error);
1027
+ return { outcome: "failed" };
828
1028
  }
829
- if (!raw) return;
1029
+ if (typeof raw !== "string" || raw === "") return { outcome: "nothing usable" };
830
1030
  let record;
831
1031
  try {
832
1032
  record = JSON.parse(raw);
833
1033
  } catch {
834
- logger === null || logger === void 0 || logger.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
835
- return;
1034
+ logger.debug("[ConfigDirectorClient] The stored instance id is not valid JSON, generating a new one");
1035
+ return { outcome: "nothing usable" };
836
1036
  }
837
1037
  if (!isUsableRecord(record)) {
838
- logger === null || logger === void 0 || logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
839
- return;
1038
+ logger.debug("[ConfigDirectorClient] The stored instance id failed validation, generating a new one");
1039
+ return { outcome: "nothing usable" };
840
1040
  }
841
1041
  const age = Date.now() - record.createdAt;
842
1042
  if (age < 0 || age >= MAX_AGE_MS) {
843
- logger === null || logger === void 0 || logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
844
- return;
1043
+ logger.debug("[ConfigDirectorClient] The stored instance id has expired, generating a new one");
1044
+ return { outcome: "nothing usable" };
845
1045
  }
846
- return record.id;
1046
+ return {
1047
+ outcome: "stored",
1048
+ instanceId: record.id
1049
+ };
847
1050
  };
848
- const storeInstanceId = (storage, id, logger) => {
1051
+ const persistInstanceId = async (storage, id, logger) => {
849
1052
  const createdAt = Date.now();
850
1053
  const record = {
851
1054
  v: RECORD_VERSION,
@@ -854,19 +1057,54 @@ const storeInstanceId = (storage, id, logger) => {
854
1057
  checksum: checksumFor(id, createdAt)
855
1058
  };
856
1059
  try {
857
- storage.setItem(STORAGE_KEY, JSON.stringify(record));
1060
+ await storage.setItem(STORAGE_KEY, JSON.stringify(record));
1061
+ return true;
858
1062
  } catch (error) {
859
- logger === null || logger === void 0 || logger.debug("[ConfigDirectorClient] Could not persist the instance id", error);
1063
+ logger.debug("[ConfigDirectorClient] Could not persist the instance id, using it as an ephemeral one", error);
1064
+ return false;
1065
+ }
1066
+ };
1067
+ const settleFromStorage = async (storage, provisionalId, deadline, logger) => {
1068
+ const read = await readStoredInstanceId(storage, logger);
1069
+ switch (read.outcome) {
1070
+ case "stored": return { instanceId: read.instanceId };
1071
+ case "failed": return {
1072
+ instanceId: provisionalId,
1073
+ ephemeral: true
1074
+ };
1075
+ case "nothing usable": break;
860
1076
  }
1077
+ if (deadline.passed) return {
1078
+ instanceId: provisionalId,
1079
+ ephemeral: true
1080
+ };
1081
+ return await persistInstanceId(storage, provisionalId, logger) ? { instanceId: provisionalId } : {
1082
+ instanceId: provisionalId,
1083
+ ephemeral: true
1084
+ };
861
1085
  };
862
- const resolveInstanceId = (logger) => {
863
- const storage = getLocalStorage();
864
- if (!storage) return generateInstanceId();
865
- const storedInstanceId = readStoredInstanceId(storage, logger);
866
- if (storedInstanceId) return storedInstanceId;
867
- const instanceId = generateInstanceId();
868
- storeInstanceId(storage, instanceId, logger);
869
- return instanceId;
1086
+ const resolveClientSession = (logger, storage = localStorageIfAvailable()) => {
1087
+ const provisionalId = generateInstanceId();
1088
+ if (!storage) {
1089
+ logger.debug("[ConfigDirectorClient] No instance id storage is available, using an ephemeral instance id");
1090
+ return Promise.resolve({
1091
+ instanceId: provisionalId,
1092
+ ephemeral: true
1093
+ });
1094
+ }
1095
+ const deadline = { passed: false };
1096
+ let deadlineTimer;
1097
+ const fallbackAtDeadline = new Promise((resolve) => {
1098
+ deadlineTimer = setTimeout(() => {
1099
+ deadline.passed = true;
1100
+ logger.warn(`[ConfigDirectorClient] The instance id storage did not respond within ${STORAGE_DEADLINE_MS}ms, using an ephemeral instance id`);
1101
+ resolve({
1102
+ instanceId: provisionalId,
1103
+ ephemeral: true
1104
+ });
1105
+ }, STORAGE_DEADLINE_MS);
1106
+ });
1107
+ return Promise.race([settleFromStorage(storage, provisionalId, deadline, logger), fallbackAtDeadline]).finally(() => clearTimeout(deadlineTimer));
870
1108
  };
871
1109
  //#endregion
872
1110
  //#region ../shared/src/transport/AbstractPollingTransport.ts
@@ -954,25 +1192,33 @@ var PollingTransport = class extends AbstractPollingTransport {
954
1192
  _defineProperty(this, "eventEmitter", new Emitter());
955
1193
  _defineProperty(this, "url", void 0);
956
1194
  _defineProperty(this, "lastUpdateTimestamp", void 0);
1195
+ _defineProperty(this, "connectionGeneration", 0);
957
1196
  this.options = options;
958
1197
  this.logger = options.logger;
959
1198
  this.url = options.resolveUrl("client/polling/v1", options.baseUrl);
960
1199
  this.pollingIntervalSeconds = (_options$pollingInter = options.pollingInterval) !== null && _options$pollingInter !== void 0 ? _options$pollingInter : 60;
961
1200
  }
962
1201
  async connect(context, timeout) {
1202
+ const generation = ++this.connectionGeneration;
963
1203
  this.clearPollingInterval();
1204
+ const clientSession = await this.options.clientSession;
1205
+ if (generation !== this.connectionGeneration) return this;
964
1206
  try {
965
- await this.fetchConfigs(context, timeout);
1207
+ await this.fetchConfigs(context, timeout, clientSession);
966
1208
  } finally {
967
1209
  if (!this.fatalError) this.schedulePollingInterval(() => {
968
- this.fetchConfigs(context, timeout).catch((error) => {
1210
+ this.fetchConfigs(context, timeout, clientSession).catch((error) => {
969
1211
  this.logger.error("[PollingTransport] Error during polling:", error);
970
1212
  });
971
1213
  });
972
1214
  }
973
1215
  return this;
974
1216
  }
975
- async fetchConfigs(context, timeout) {
1217
+ close() {
1218
+ this.connectionGeneration++;
1219
+ super.close();
1220
+ }
1221
+ async fetchConfigs(context, timeout, clientSession) {
976
1222
  if (this.fatalError) {
977
1223
  this.logger.warn("[PollingTransport] There was a prior unrecoverable error. Ignoring attempt to reconnect.");
978
1224
  return;
@@ -985,7 +1231,8 @@ var PollingTransport = class extends AbstractPollingTransport {
985
1231
  givenContext: context,
986
1232
  metaContext: this.options.metaContext,
987
1233
  clientSdkKey: this.options.clientSdkKey,
988
- instanceId: this.options.instanceId,
1234
+ instanceId: clientSession.instanceId,
1235
+ ephemeral: clientSession.ephemeral,
989
1236
  lastUpdateTimestamp: this.lastUpdateTimestamp
990
1237
  })
991
1238
  }, this.logger);
@@ -1032,11 +1279,55 @@ const readHost = () => read((globals) => {
1032
1279
  return (_globals$location = globals.location) === null || _globals$location === void 0 ? void 0 : _globals$location.host;
1033
1280
  });
1034
1281
  //#endregion
1282
+ //#region ../shared/src/context-kind.ts
1283
+ const CONTEXT_KIND_MAXIMUM_LENGTH = 64;
1284
+ const KIND_SAMPLE_MAXIMUM_CHARACTERS = 128;
1285
+ const isContextKindCharacter = (character) => character >= "a" && character <= "z" || character >= "0" && character <= "9" || character === "-" || character === "_";
1286
+ const isValidContextKind = (kind) => {
1287
+ if (kind.length < 1 || kind.length > CONTEXT_KIND_MAXIMUM_LENGTH) return false;
1288
+ for (const character of kind) if (!isContextKindCharacter(character)) return false;
1289
+ return true;
1290
+ };
1291
+ const isAcceptedKind = (kind) => kind === void 0 || kind === null || typeof kind === "string" && isValidContextKind(kind);
1292
+ const stringFormOf = (kind) => {
1293
+ if (typeof kind === "string") return kind;
1294
+ try {
1295
+ return String(kind);
1296
+ } catch {
1297
+ return Object.prototype.toString.call(kind);
1298
+ }
1299
+ };
1300
+ const firstCharactersOf = (text, maximumCharacters) => {
1301
+ let sample = "";
1302
+ let characterCount = 0;
1303
+ for (const character of text) {
1304
+ if (characterCount === maximumCharacters) break;
1305
+ sample += character;
1306
+ characterCount += 1;
1307
+ }
1308
+ return sample;
1309
+ };
1310
+ const admitContext = (context) => {
1311
+ const kind = context === null || context === void 0 ? void 0 : context.kind;
1312
+ if (isAcceptedKind(kind)) return {
1313
+ outcome: "admitted",
1314
+ context
1315
+ };
1316
+ return {
1317
+ outcome: "ignored",
1318
+ kindSample: firstCharactersOf(stringFormOf(kind), KIND_SAMPLE_MAXIMUM_CHARACTERS)
1319
+ };
1320
+ };
1321
+ //#endregion
1035
1322
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
1036
1323
  const MAX_EXPONENTIAL_DELAY = 9;
1037
- var DefaultConfigDirectorClient = class {
1038
- constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1039
- var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _clientOptions$connec5, _internalClientOption2, _clientOptions$hooks, _clientOptions$hooks2, _clientOptions$hooks3, _clientOptions$hooks4, _clientOptions$hooks5;
1324
+ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1325
+ static from(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions) {
1326
+ if (typeof clientSdkKeyOrOptions === "object") return new DefaultConfigDirectorClient(liveTelemetryClient, void 0, sdkOptions, clientSdkKeyOrOptions);
1327
+ return new DefaultConfigDirectorClient(liveTelemetryClient, clientSdkKeyOrOptions, sdkOptions, clientOptions);
1328
+ }
1329
+ constructor(liveTelemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
1330
+ var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _internalClientOption2, _clientOptions$hooks, _clientOptions$hooks2, _clientOptions$hooks3, _clientOptions$hooks4, _clientOptions$hooks5;
1040
1331
  _defineProperty(this, "logger", void 0);
1041
1332
  _defineProperty(this, "telemetryClient", void 0);
1042
1333
  _defineProperty(this, "configSet", void 0);
@@ -1051,17 +1342,15 @@ var DefaultConfigDirectorClient = class {
1051
1342
  _defineProperty(this, "readyPromise", void 0);
1052
1343
  _defineProperty(this, "readyResolve", void 0);
1053
1344
  _defineProperty(this, "currentContext", void 0);
1054
- _defineProperty(this, "connectionMode", void 0);
1055
- _defineProperty(this, "instanceId", void 0);
1345
+ _defineProperty(this, "connectionPlan", void 0);
1346
+ _defineProperty(this, "clientSession", void 0);
1056
1347
  this.logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createDefaultLogger();
1057
- this.instanceId = resolveInstanceId(this.logger);
1348
+ this.clientSession = resolveClientSession(this.logger, internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.instanceIdStorage);
1058
1349
  this.timeout = (_clientOptions$connec = clientOptions === null || clientOptions === void 0 || (_clientOptions$connec2 = clientOptions.connection) === null || _clientOptions$connec2 === void 0 ? void 0 : _clientOptions$connec2.timeout) !== null && _clientOptions$connec !== void 0 ? _clientOptions$connec : 3e3;
1059
- this.validateSdkKeyPresence(clientSdkKey);
1060
1350
  const urlFactory = (_internalClientOption = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.urlFactory) !== null && _internalClientOption !== void 0 ? _internalClientOption : defaultUrlFactory;
1061
- const baseUrl = (_this$parseUrl = this.parseUrl(clientOptions === null || clientOptions === void 0 || (_clientOptions$connec3 = clientOptions.connection) === null || _clientOptions$connec3 === void 0 ? void 0 : _clientOptions$connec3.url, urlFactory)) !== null && _this$parseUrl !== void 0 ? _this$parseUrl : CLIENT_BASE_URL;
1062
- this.connectionMode = (_clientOptions$connec4 = clientOptions === null || clientOptions === void 0 || (_clientOptions$connec5 = clientOptions.connection) === null || _clientOptions$connec5 === void 0 ? void 0 : _clientOptions$connec5.mode) !== null && _clientOptions$connec4 !== void 0 ? _clientOptions$connec4 : "streaming";
1063
- this.telemetryClient = telemetryClient;
1064
- this.transport = (_internalClientOption2 = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.transport) !== null && _internalClientOption2 !== void 0 ? _internalClientOption2 : this.buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions);
1351
+ this.connectionPlan = planConnection(clientSdkKey, clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.connection, urlFactory);
1352
+ this.telemetryClient = this.buildTelemetryClient(liveTelemetryClient, clientOptions);
1353
+ this.transport = (_internalClientOption2 = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.transport) !== null && _internalClientOption2 !== void 0 ? _internalClientOption2 : this.buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions);
1065
1354
  this.transport.on("configSetReceived", (configSet) => {
1066
1355
  var _this$readyResolve;
1067
1356
  this.hasReceivedConfigSet = true;
@@ -1099,8 +1388,8 @@ var DefaultConfigDirectorClient = class {
1099
1388
  const handlers = Array.isArray(handler) ? handler : [handler];
1100
1389
  for (const h of handlers) if (typeof h === "function") this.on(event, h);
1101
1390
  }
1102
- resolvePollingIntervalForMode(configuredSeconds) {
1103
- if (this.connectionMode !== "polling") return;
1391
+ pollingIntervalFor(mode, configuredSeconds) {
1392
+ if (mode !== "polling") return;
1104
1393
  const resolved = resolvePollingInterval(configuredSeconds, {
1105
1394
  defaultSeconds: 60,
1106
1395
  minimumSeconds: 30
@@ -1108,40 +1397,81 @@ var DefaultConfigDirectorClient = class {
1108
1397
  if (resolved.warning) this.logger.warn(`[ConfigDirectorClient] ${resolved.warning}`);
1109
1398
  return resolved.seconds;
1110
1399
  }
1111
- buildTransport(clientSdkKey, sdkOptions, baseUrl, urlFactory, clientOptions, internalClientOptions) {
1112
- var _internalClientOption3, _clientOptions$connec6;
1113
- return new (this.getTransportConstructor(this.connectionMode))({
1114
- instanceId: this.instanceId,
1115
- clientSdkKey,
1116
- baseUrl,
1117
- resolveUrl: urlFactory,
1118
- metaContext: {
1119
- ...clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.metadata,
1120
- sdkName: sdkOptions.sdkName,
1121
- sdkVersion: sdkOptions.sdkVersion,
1122
- userAgent: readUserAgent(),
1123
- host: readHost()
1124
- },
1125
- logger: this.logger,
1126
- fetch: internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.fetch,
1127
- connectionRetryDelay: (_internalClientOption3 = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.connectionRetryDelay) !== null && _internalClientOption3 !== void 0 ? _internalClientOption3 : ((attempt) => {
1128
- return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1129
- }),
1130
- pollingInterval: this.resolvePollingIntervalForMode(clientOptions === null || clientOptions === void 0 || (_clientOptions$connec6 = clientOptions.connection) === null || _clientOptions$connec6 === void 0 ? void 0 : _clientOptions$connec6.pollingInterval)
1131
- });
1400
+ buildTelemetryClient(liveTelemetryClient, clientOptions) {
1401
+ const plan = this.connectionPlan;
1402
+ switch (plan.kind) {
1403
+ case "stay-offline": return discardingTelemetryClient;
1404
+ case "connect": return liveTelemetryClient({
1405
+ clientSdkKey: plan.clientSdkKey,
1406
+ baseUrl: plan.baseUrl,
1407
+ logger: this.logger,
1408
+ metadata: clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.metadata
1409
+ });
1410
+ }
1132
1411
  }
1133
- getTransportConstructor(connectionMode) {
1134
- switch (connectionMode) {
1412
+ buildTransport(sdkOptions, clientOptions, urlFactory, internalClientOptions) {
1413
+ const plan = this.connectionPlan;
1414
+ switch (plan.kind) {
1415
+ case "stay-offline": return new OfflineTransport(plan.source);
1416
+ case "connect":
1417
+ var _internalClientOption3, _clientOptions$connec3;
1418
+ return new (this.getTransportConstructor(plan.mode))({
1419
+ clientSession: this.clientSession,
1420
+ clientSdkKey: plan.clientSdkKey,
1421
+ baseUrl: plan.baseUrl,
1422
+ resolveUrl: urlFactory,
1423
+ metaContext: {
1424
+ ...clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.metadata,
1425
+ sdkName: sdkOptions.sdkName,
1426
+ sdkVersion: sdkOptions.sdkVersion,
1427
+ userAgent: readUserAgent(),
1428
+ host: readHost()
1429
+ },
1430
+ logger: this.logger,
1431
+ fetch: internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.fetch,
1432
+ connectionRetryDelay: (_internalClientOption3 = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.connectionRetryDelay) !== null && _internalClientOption3 !== void 0 ? _internalClientOption3 : ((attempt) => {
1433
+ return Math.pow(2, Math.min(attempt, MAX_EXPONENTIAL_DELAY)) * 1e3;
1434
+ }),
1435
+ pollingInterval: this.pollingIntervalFor(plan.mode, clientOptions === null || clientOptions === void 0 || (_clientOptions$connec3 = clientOptions.connection) === null || _clientOptions$connec3 === void 0 ? void 0 : _clientOptions$connec3.pollingInterval)
1436
+ });
1437
+ }
1438
+ }
1439
+ getTransportConstructor(mode) {
1440
+ switch (mode) {
1135
1441
  case "polling": return PollingTransport;
1136
- default: return StreamingTransport;
1442
+ case "streaming": return StreamingTransport;
1137
1443
  }
1138
1444
  }
1139
1445
  async initialize(context) {
1140
1446
  if (!this.hasReceivedConfigSet) this.initializing = true;
1141
- await this.connectToTransport(context, "initialization");
1447
+ await this.submitContext(context, "initialization");
1142
1448
  }
1143
1449
  async updateContext(context) {
1144
- await this.connectToTransport(context, "context update");
1450
+ switch (this.connectionPlan.kind) {
1451
+ case "connect":
1452
+ await this.submitContext(context, "context update");
1453
+ return;
1454
+ case "stay-offline":
1455
+ this.storeContext(this.admittedContextFor(context, "context update"));
1456
+ return;
1457
+ }
1458
+ }
1459
+ storeContext(context) {
1460
+ this.currentContext = context;
1461
+ this.emit("contextUpdated", { context });
1462
+ }
1463
+ async submitContext(context, submission) {
1464
+ await this.connectToTransport(this.admittedContextFor(context, submission), submission);
1465
+ }
1466
+ admittedContextFor(suppliedContext, submission) {
1467
+ const admission = admitContext(suppliedContext);
1468
+ switch (admission.outcome) {
1469
+ case "admitted": return admission.context;
1470
+ case "ignored":
1471
+ 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 '_'.`);
1472
+ this.telemetryClient.recordInvalidContext(admission.kindSample);
1473
+ return;
1474
+ }
1145
1475
  }
1146
1476
  get context() {
1147
1477
  return this.currentContext;
@@ -1170,11 +1500,21 @@ var DefaultConfigDirectorClient = class {
1170
1500
  if (remainingTimeout > 0) await Promise.race([this.readyPromise, new Promise((resolve) => {
1171
1501
  this.timeoutTimer = setTimeout(() => resolve(), remainingTimeout);
1172
1502
  })]).finally(() => clearTimeout(this.timeoutTimer));
1173
- 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.`);
1503
+ if (!this.ready) this.logger.warn(`[ConfigDirectorClient] Timed out waiting for ${caller} after ${this.timeout}ms. ${this.timeoutWarningDetails()}`);
1174
1504
  } catch (error) {
1505
+ if (error instanceof ConfigDirectorInitializationError) {
1506
+ this.initializing = false;
1507
+ throw error;
1508
+ }
1175
1509
  this.logger.error(`[ConfigDirectorClient] An error occurred during ${caller}: `, error);
1176
1510
  }
1177
1511
  }
1512
+ timeoutWarningDetails() {
1513
+ switch (this.connectionPlan.kind) {
1514
+ 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.";
1515
+ case "stay-offline": return "The offline values were not delivered, so configs return the default value.";
1516
+ }
1517
+ }
1178
1518
  findKeysRemovedBy(configSet) {
1179
1519
  if (!this.configSet || configSet.kind != "full") return [];
1180
1520
  return Object.keys(this.configSet.configs).filter((key) => !Object.prototype.hasOwnProperty.call(configSet.configs, key));
@@ -1273,17 +1613,6 @@ var DefaultConfigDirectorClient = class {
1273
1613
  this.logger.error(`[ConfigDirectorClient] Error executing event handlers for '${name}'`, error);
1274
1614
  }
1275
1615
  }
1276
- parseUrl(url, urlFactory) {
1277
- if (!url) return;
1278
- try {
1279
- return urlFactory(url);
1280
- } catch (error) {
1281
- throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
1282
- }
1283
- }
1284
- validateSdkKeyPresence(sdkKey) {
1285
- 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");
1286
- }
1287
1616
  validateDefaultValue(defaultValue) {
1288
1617
  if (defaultValue === void 0 || defaultValue === null) throw new ConfigDirectorValidationError("Invalid default value. The default value for a config must be defined and non-null.");
1289
1618
  if (typeof defaultValue === "function") throw new ConfigDirectorValidationError("Invalid default value. The default value for a config cannot be a function.");
@@ -1309,14 +1638,24 @@ var DefaultConfigDirectorClient = class {
1309
1638
  this.handlersMap.clear();
1310
1639
  }
1311
1640
  pauseNetwork() {
1312
- this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1313
- clearTimeout(this.timeoutTimer);
1314
- this.timeoutTimer = void 0;
1315
- this.transport.close();
1316
- this.ready = false;
1641
+ switch (this.connectionPlan.kind) {
1642
+ case "stay-offline": return;
1643
+ case "connect":
1644
+ this.logger.debug("[ConfigDirectorClient] pauseNetwork() called, pausing transport connection");
1645
+ clearTimeout(this.timeoutTimer);
1646
+ this.timeoutTimer = void 0;
1647
+ this.transport.close();
1648
+ this.ready = false;
1649
+ return;
1650
+ }
1317
1651
  }
1318
1652
  async resumeNetwork() {
1319
- await this.connectToTransport(this.currentContext, "network resume");
1653
+ switch (this.connectionPlan.kind) {
1654
+ case "stay-offline": return;
1655
+ case "connect":
1656
+ await this.connectToTransport(this.currentContext, "network resume");
1657
+ return;
1658
+ }
1320
1659
  }
1321
1660
  async close() {
1322
1661
  var _this$readyResolve2;
@@ -1338,4 +1677,4 @@ var DefaultConfigDirectorClient = class {
1338
1677
  const INSTALLED_TEST_CLIENT_SLOT = Symbol.for("@configdirector/installed-test-client");
1339
1678
  const readInstalledTestClient = () => globalThis[INSTALLED_TEST_CLIENT_SLOT];
1340
1679
  //#endregion
1341
- export { CLIENT_BASE_URL as a, ConfigDirectorConnectionError as c, _defineProperty as d, fetchWithTimeout as i, ConfigDirectorValidationError as l, readInstalledTestClient as n, createDefaultLogger as o, DefaultConfigDirectorClient as r, isFetchErrorFatal as s, INSTALLED_TEST_CLIENT_SLOT as t, Emitter as u };
1680
+ export { discardingTelemetryClient as a, isFetchErrorFatal as c, Emitter as d, _defineProperty as f, fetchWithTimeout as i, ConfigDirectorConnectionError as l, readInstalledTestClient as n, createDefaultLogger as o, DefaultConfigDirectorClient as r, nativeValueConfigState as s, INSTALLED_TEST_CLIENT_SLOT as t, ConfigDirectorValidationError as u };