@configdirector/react-native-sdk 1.0.0 → 1.2.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.
@@ -78,7 +78,7 @@ type ConfigDirectorMetaContext = {
78
78
  appVersion?: string;
79
79
  appName?: string;
80
80
  };
81
- type ConnectionMode = "streaming" | "polling" | "one-time";
81
+ type ConnectionMode = "streaming" | "polling";
82
82
  //#endregion
83
83
  //#region ../js-client-core/src/types.d.ts
84
84
  /**
@@ -94,20 +94,18 @@ type ConfigDirectorClientOptions = {
94
94
  */
95
95
  connection?: {
96
96
  /**
97
- * The connection mode to be used, one of `streaming` (default), `polling`, or `one-time`.
97
+ * The connection mode to be used, one of `streaming` (default) or `polling`.
98
98
  * If set to `streaming`, the connection will remain open and receive updates whenever
99
99
  * config state is updated on the ConfigDirector dashboard.
100
100
  * When set to `polling`, there will be an initial request to retrieve config state during
101
101
  * initialization, and additional requests on a `pollingInterval`.
102
- * The `one-time` connection mode will only retrieve config state during initialization and
103
- * context updates. It will not poll for regular updates.
104
102
  *
105
103
  * Defaults to `streaming`
106
104
  */
107
105
  mode?: ConnectionMode;
108
106
  /**
109
107
  * The polling interval in _seconds_ when the `mode` is set to `polling`. This option has no
110
- * effect when the `mode` is set to `streaming` or `one-time`.
108
+ * effect when the `mode` is set to `streaming`.
111
109
  *
112
110
  * Defaults to 60 seconds
113
111
  */
@@ -162,6 +160,7 @@ type ClientHooks = {
162
160
  configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
163
161
  contextUpdated?: HookHandler<"contextUpdated"> | HookHandler<"contextUpdated">[];
164
162
  configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
163
+ connectionError?: HookHandler<"connectionError"> | HookHandler<"connectionError">[];
165
164
  };
166
165
  type ClientConnectAction = "initialization" | "context update" | "network resume";
167
166
  type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
@@ -178,6 +177,9 @@ type ClientEvents = {
178
177
  configEvaluated: {
179
178
  evaluation: ConfigEvaluation;
180
179
  };
180
+ connectionError: {
181
+ error: Error;
182
+ };
181
183
  };
182
184
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
183
185
  /**
@@ -255,6 +255,7 @@ var EventSourceClient = class {
255
255
  _defineProperty(this, "DEFAULT_CALCULATE_RECONNECT_DELAY", () => this.serverReconnectionTime);
256
256
  _defineProperty(this, "url", void 0);
257
257
  _defineProperty(this, "headers", void 0);
258
+ _defineProperty(this, "body", void 0);
258
259
  _defineProperty(this, "requestOptions", void 0);
259
260
  _defineProperty(this, "lastEventId", void 0);
260
261
  _defineProperty(this, "reconnectAttempt", 0);
@@ -276,8 +277,9 @@ var EventSourceClient = class {
276
277
  Accept: "text/event-stream",
277
278
  ...options.headers
278
279
  };
280
+ const body = options.body;
281
+ this.body = typeof body === "function" ? body : () => body;
279
282
  this.requestOptions = {
280
- body: options.body,
281
283
  method: (_options$method = options.method) !== null && _options$method !== void 0 ? _options$method : "GET",
282
284
  mode: options.mode,
283
285
  credentials: options.credentials,
@@ -333,6 +335,7 @@ var EventSourceClient = class {
333
335
  this._readyState = ReadyState.CONNECTING;
334
336
  this._fetch(this.url, {
335
337
  ...this.requestOptions,
338
+ body: this.body(),
336
339
  headers: this.buildRequestHeaders(),
337
340
  signal: this.abortController.signal,
338
341
  cache: "no-store"
@@ -466,7 +469,11 @@ var StreamingTransport = class {
466
469
  },
467
470
  shouldReconnect: (state) => {
468
471
  const reconnect = !this.isStatusFatal(state.status);
469
- if (!reconnect) reject(this.prepareFatalError(state.status, state.error));
472
+ if (!reconnect) {
473
+ const error = this.prepareFatalError(state.status, state.error);
474
+ this.eventEmitter.emit("connectionError", error);
475
+ reject(error);
476
+ }
470
477
  return reconnect;
471
478
  },
472
479
  calculateReconnectDelay: (state) => {
@@ -549,7 +556,7 @@ const isNumericNativeType = (requestedType) => {
549
556
  const parseConfigValue = (configState, defaultValue) => {
550
557
  const value = configState.value;
551
558
  const requestedType = getRequestedType(defaultValue);
552
- if (!value) return {
559
+ if (value == null) return {
553
560
  parsedValue: defaultValue,
554
561
  parsedValueId: void 0,
555
562
  requestedType,
@@ -564,7 +571,7 @@ const parseConfigValue = (configState, defaultValue) => {
564
571
  usedDefault: false,
565
572
  reason: "found-match"
566
573
  };
567
- if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
574
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string" || configState.type === "enum")) {
568
575
  const boolValue = parseConfigBoolean(value);
569
576
  const hasBoolean = typeof boolValue === "boolean";
570
577
  return {
@@ -598,11 +605,11 @@ const parseConfigValue = (configState, defaultValue) => {
598
605
  };
599
606
  }
600
607
  return {
601
- parsedValue: value,
602
- parsedValueId: configState.valueId,
608
+ parsedValue: defaultValue,
609
+ parsedValueId: void 0,
603
610
  requestedType,
604
- usedDefault: false,
605
- reason: "found-match"
611
+ usedDefault: true,
612
+ reason: "type-mismatch"
606
613
  };
607
614
  };
608
615
  const parseConfigBoolean = (value) => {
@@ -612,17 +619,22 @@ const parseConfigBoolean = (value) => {
612
619
  return lowerValue === "true";
613
620
  };
614
621
  const parseConfigInteger = (value) => {
615
- if (!value) return;
616
- const num = Number.parseInt(value);
617
- if (isNaN(num) || !isFinite(num)) return;
618
- return num;
622
+ const num = parseStrictNumber(value);
623
+ return num == null ? void 0 : Math.trunc(num);
619
624
  };
620
625
  const parseConfigFloat = (value) => {
621
- if (!value) return;
622
- const num = Number.parseFloat(value);
626
+ return parseStrictNumber(value);
627
+ };
628
+ const parseStrictNumber = (value) => {
629
+ if (!value || !isDecimalCharacters(value)) return;
630
+ const num = Number(value);
623
631
  if (isNaN(num) || !isFinite(num)) return;
624
632
  return num;
625
633
  };
634
+ const isDecimalCharacters = (value) => {
635
+ for (const character of value) if (!(character >= "0" && character <= "9") && !"+-.eE".includes(character)) return false;
636
+ return true;
637
+ };
626
638
  const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
627
639
  if (typeof defaultValue === "string") return {
628
640
  parsedValue: jsonString,
@@ -632,8 +644,16 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
632
644
  reason: "found-match"
633
645
  };
634
646
  try {
647
+ const parsed = JSON.parse(jsonString);
648
+ if (!isJsonTypeCompatible(parsed, defaultValue)) return {
649
+ parsedValue: defaultValue,
650
+ parsedValueId: void 0,
651
+ requestedType,
652
+ usedDefault: true,
653
+ reason: "type-mismatch"
654
+ };
635
655
  return {
636
- parsedValue: JSON.parse(jsonString),
656
+ parsedValue: parsed,
637
657
  parsedValueId: valueId,
638
658
  requestedType,
639
659
  usedDefault: false,
@@ -649,6 +669,12 @@ const parseJson = (jsonString, defaultValue, requestedType, valueId) => {
649
669
  };
650
670
  }
651
671
  };
672
+ const isJsonTypeCompatible = (parsed, defaultValue) => {
673
+ if (parsed === null) return false;
674
+ const defaultType = typeof defaultValue;
675
+ if (defaultType === "bigint") return typeof parsed === "number";
676
+ return typeof parsed === defaultType;
677
+ };
652
678
  //#endregion
653
679
  //#region ../shared/src/logger.ts
654
680
  const LEVELS = {
@@ -716,6 +742,21 @@ const createDefaultLogger = (level, messageDecorator) => {
716
742
  return new DefaultConsoleLogger(level !== null && level !== void 0 ? level : "warn", messageDecorator !== null && messageDecorator !== void 0 ? messageDecorator : new LogMessageDecorator());
717
743
  };
718
744
  //#endregion
745
+ //#region ../shared/src/url.ts
746
+ const defaultUrlFactory = (input, base) => new URL(input, base === null || base === void 0 ? void 0 : base.toString());
747
+ //#endregion
748
+ //#region ../shared/src/constants.ts
749
+ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
750
+ //#endregion
751
+ //#region ../shared/src/instance-id.ts
752
+ const generateInstanceId = () => {
753
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
754
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (placeholder) => {
755
+ const random = Math.random() * 16 | 0;
756
+ return (placeholder === "x" ? random : random & 3 | 8).toString(16);
757
+ });
758
+ };
759
+ //#endregion
719
760
  //#region ../shared/src/transport/AbstractPollingTransport.ts
720
761
  var AbstractPollingTransport = class {
721
762
  constructor() {
@@ -734,6 +775,7 @@ var AbstractPollingTransport = class {
734
775
  async handleNonOkResponse(response) {
735
776
  if (!response.ok) if (this.isStatusFatal(response.status)) {
736
777
  this.fatalError = true;
778
+ this.close();
737
779
  throw this.prepareFatalResponseStatusError(response.status, await response.text());
738
780
  } else throw new ConfigDirectorConnectionError(`Connection failed with status: ${response.status}`, response.status);
739
781
  }
@@ -754,9 +796,7 @@ var AbstractPollingTransport = class {
754
796
  return !!status && status >= 400 && status < 500;
755
797
  }
756
798
  isFetchErrorFatal(fetchError) {
757
- if ((fetchError === null || fetchError === void 0 ? void 0 : fetchError.name) === "NotAllowedError") return true;
758
- else if (fetchError instanceof TypeError) return true;
759
- return false;
799
+ return (fetchError === null || fetchError === void 0 ? void 0 : fetchError.name) === "NotAllowedError";
760
800
  }
761
801
  get isConnected() {
762
802
  return this.pollingInterval !== void 0;
@@ -844,7 +884,12 @@ var PollingTransport = class extends AbstractPollingTransport {
844
884
  this.eventEmitter.emit("configSetReceived", json);
845
885
  }
846
886
  } catch (fetchError) {
847
- this.handleFetchError(fetchError);
887
+ try {
888
+ this.handleFetchError(fetchError);
889
+ } catch (handledError) {
890
+ if (this.fatalError) this.eventEmitter.emit("connectionError", handledError);
891
+ throw handledError;
892
+ }
848
893
  }
849
894
  }
850
895
  on(name, handler) {
@@ -858,34 +903,11 @@ var PollingTransport = class extends AbstractPollingTransport {
858
903
  }
859
904
  };
860
905
  //#endregion
861
- //#region ../js-client-core/src/OneTimeTransport.ts
862
- var OneTimeTransport = class extends PollingTransport {
863
- constructor(options) {
864
- super(options);
865
- this.pollingIntervalSeconds = 0;
866
- }
867
- };
868
- //#endregion
869
- //#region ../shared/src/url.ts
870
- const defaultUrlFactory = (input, base) => new URL(input, base === null || base === void 0 ? void 0 : base.toString());
871
- //#endregion
872
- //#region ../shared/src/constants.ts
873
- const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
874
- //#endregion
875
- //#region ../shared/src/instance-id.ts
876
- const generateInstanceId = () => {
877
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
878
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (placeholder) => {
879
- const random = Math.random() * 16 | 0;
880
- return (placeholder === "x" ? random : random & 3 | 8).toString(16);
881
- });
882
- };
883
- //#endregion
884
906
  //#region ../js-client-core/src/DefaultConfigDirectorClient.ts
885
907
  const MAX_EXPONENTIAL_DELAY = 9;
886
908
  var DefaultConfigDirectorClient = class {
887
909
  constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
888
- var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _clientOptions$connec5, _navigator, _internalClientOption2, _clientOptions$connec6, _clientOptions$hooks, _clientOptions$hooks2, _clientOptions$hooks3, _clientOptions$hooks4;
910
+ var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _clientOptions$connec5, _navigator, _internalClientOption2, _clientOptions$connec6, _clientOptions$hooks, _clientOptions$hooks2, _clientOptions$hooks3, _clientOptions$hooks4, _clientOptions$hooks5;
889
911
  _defineProperty(this, "logger", void 0);
890
912
  _defineProperty(this, "telemetryClient", void 0);
891
913
  _defineProperty(this, "configSet", void 0);
@@ -946,10 +968,15 @@ var DefaultConfigDirectorClient = class {
946
968
  }
947
969
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
948
970
  });
971
+ this.transport.on("connectionError", (error) => {
972
+ this.logger.error("[ConfigDirectorClient] The connection encountered an unrecoverable error and will not be retried. Configs will no longer receive updates.", error);
973
+ this.emit("connectionError", { error });
974
+ });
949
975
  this.registerHandler("clientReady", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks = clientOptions.hooks) === null || _clientOptions$hooks === void 0 ? void 0 : _clientOptions$hooks.clientReady);
950
976
  this.registerHandler("configEvaluated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks2 = clientOptions.hooks) === null || _clientOptions$hooks2 === void 0 ? void 0 : _clientOptions$hooks2.configEvaluated);
951
977
  this.registerHandler("configsUpdated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks3 = clientOptions.hooks) === null || _clientOptions$hooks3 === void 0 ? void 0 : _clientOptions$hooks3.configsUpdated);
952
978
  this.registerHandler("contextUpdated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks4 = clientOptions.hooks) === null || _clientOptions$hooks4 === void 0 ? void 0 : _clientOptions$hooks4.contextUpdated);
979
+ this.registerHandler("connectionError", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks5 = clientOptions.hooks) === null || _clientOptions$hooks5 === void 0 ? void 0 : _clientOptions$hooks5.connectionError);
953
980
  }
954
981
  registerHandler(event, handler) {
955
982
  if (!handler) return;
@@ -958,7 +985,6 @@ var DefaultConfigDirectorClient = class {
958
985
  }
959
986
  getTransportConstructor(connectionMode) {
960
987
  switch (connectionMode) {
961
- case "one-time": return OneTimeTransport;
962
988
  case "polling": return PollingTransport;
963
989
  default: return StreamingTransport;
964
990
  }
@@ -978,7 +1004,8 @@ var DefaultConfigDirectorClient = class {
978
1004
  this.ready = false;
979
1005
  this.readyPromise = new Promise((resolve) => {
980
1006
  this.readyResolve = resolve;
981
- }).then(() => {
1007
+ }).then((cancelled) => {
1008
+ if (cancelled) return;
982
1009
  this.ready = true;
983
1010
  this.initializing = false;
984
1011
  this.emit("clientReady", { action: caller });
@@ -1143,10 +1170,11 @@ var DefaultConfigDirectorClient = class {
1143
1170
  var _this$readyResolve2;
1144
1171
  this.logger.debug("[ConfigDirectorClient] close() has been called, closing connection to server");
1145
1172
  clearTimeout(this.timeoutTimer);
1146
- (_this$readyResolve2 = this.readyResolve) === null || _this$readyResolve2 === void 0 || _this$readyResolve2.call(this);
1173
+ (_this$readyResolve2 = this.readyResolve) === null || _this$readyResolve2 === void 0 || _this$readyResolve2.call(this, true);
1147
1174
  this.telemetryClient.close();
1148
1175
  this.transport.close();
1149
1176
  this.ready = false;
1177
+ this.initializing = false;
1150
1178
  }
1151
1179
  dispose() {
1152
1180
  this.clear();
@@ -1368,11 +1396,25 @@ var TelemetryEventCollector = class {
1368
1396
  this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), initialDelay);
1369
1397
  }
1370
1398
  async flushAndScheduleNext() {
1371
- if ((await this.flush()).fatalError) {
1399
+ if (!this.collectEvents) return;
1400
+ let response;
1401
+ try {
1402
+ response = await this.flush();
1403
+ } catch (error) {
1404
+ this.logger.warn("[TelemetryEventCollector] Error flushing telemetry events: ", error);
1405
+ response = {
1406
+ success: false,
1407
+ fatalError: false
1408
+ };
1409
+ }
1410
+ if (response.fatalError) {
1372
1411
  this.collectEvents = false;
1373
1412
  this.close();
1374
1413
  this.logger.warn("[TelemetryEventCollector] Received a fatal error from telemetry collection. No longer collecting events.");
1375
- } else this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
1414
+ } else if (this.collectEvents) {
1415
+ this.cancelScheduledFlush();
1416
+ this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.flushIntervalDelay);
1417
+ }
1376
1418
  }
1377
1419
  cancelScheduledFlush() {
1378
1420
  clearTimeout(this.flushTimeout);
@@ -1380,7 +1422,11 @@ var TelemetryEventCollector = class {
1380
1422
  async close() {
1381
1423
  this.collectEvents = false;
1382
1424
  clearTimeout(this.flushTimeout);
1383
- await this.flush();
1425
+ try {
1426
+ await this.flush();
1427
+ } catch (error) {
1428
+ this.logger.warn("[TelemetryEventCollector] Error flushing telemetry events during close: ", error);
1429
+ }
1384
1430
  this.evaluationEventQueue.clear();
1385
1431
  }
1386
1432
  dispose() {
@@ -1649,7 +1695,7 @@ const createClient = (clientSdkKey, clientOptions) => {
1649
1695
  var _clientOptions$logger, _clientOptions$connec;
1650
1696
  const sdkIdentity = {
1651
1697
  sdkName: "react-native-sdk",
1652
- sdkVersion: "1.0.0"
1698
+ sdkVersion: "1.2.0"
1653
1699
  };
1654
1700
  const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
1655
1701
  return new DefaultConfigDirectorClient(new ReactNativeTelemetryClient({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/react-native-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "React Native client SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",