@configdirector/react-native-sdk 0.8.0 → 0.9.1

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.
@@ -26,6 +26,15 @@ interface ConfigDirectorLogger {
26
26
  interface ConfigDirectorLogMessageDecorator {
27
27
  decorateMessage(message: string): string;
28
28
  }
29
+ type EvaluationReason = "found-match" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean";
30
+ type ConfigEvaluation = {
31
+ key: string;
32
+ value: ConfigValueType;
33
+ valueId?: string;
34
+ isDefaultValue: boolean;
35
+ reason: EvaluationReason;
36
+ context?: ConfigDirectorContext;
37
+ };
29
38
  /**
30
39
  * The user's context to be sent to ConfigDirector. This context will be used for targeting
31
40
  * rules evaluation.
@@ -131,8 +140,31 @@ type ConfigDirectorClientOptions = {
131
140
  * );
132
141
  */
133
142
  logger?: ConfigDirectorLogger;
143
+ /**
144
+ * Handlers for hooks/events emitted by the client. Each hook accepts either a single event handler or
145
+ * an array of event handlers.
146
+ *
147
+ * @example
148
+ * import { createClient, createConsoleLogger } from "@configdirector/client-sdk";
149
+ * const client = createClient(
150
+ * "YOUR-SDK-KEY",
151
+ * {
152
+ * hooks: {
153
+ * configEvaluated: (event) => { console.log(event.evaluation); }
154
+ * },
155
+ * },
156
+ * );
157
+ */
158
+ hooks?: ClientHooks;
159
+ };
160
+ type ClientHooks = {
161
+ clientReady?: HookHandler<"clientReady"> | HookHandler<"clientReady">[];
162
+ configsUpdated?: HookHandler<"configsUpdated"> | HookHandler<"configsUpdated">[];
163
+ contextUpdated?: HookHandler<"contextUpdated"> | HookHandler<"contextUpdated">[];
164
+ configEvaluated?: HookHandler<"configEvaluated"> | HookHandler<"configEvaluated">[];
134
165
  };
135
166
  type ClientConnectAction = "initialization" | "context update" | "network resume";
167
+ type HookHandler<TEvent extends keyof ClientEvents> = (payload: ClientEvents[TEvent]) => void;
136
168
  type ClientEvents = {
137
169
  configsUpdated: {
138
170
  keys: string[];
@@ -143,6 +175,9 @@ type ClientEvents = {
143
175
  contextUpdated: {
144
176
  context: ConfigDirectorContext | undefined;
145
177
  };
178
+ configEvaluated: {
179
+ evaluation: ConfigEvaluation;
180
+ };
146
181
  };
147
182
  type WatchHandler<T extends ConfigValueType> = (message: T) => void;
148
183
  /**
@@ -297,6 +332,7 @@ type ConfigDirectorProviderOptions = {
297
332
  timeout?: number;
298
333
  context?: ConfigDirectorContext;
299
334
  logger?: ConfigDirectorLogger;
335
+ hooks?: ClientHooks;
300
336
  netInfoSubscribe?: NetInfoSubscribe;
301
337
  };
302
338
  //#endregion
@@ -564,7 +564,7 @@ const parseConfigValue = (configState, defaultValue) => {
564
564
  usedDefault: false,
565
565
  reason: "found-match"
566
566
  };
567
- if (typeof defaultValue === "boolean" && configState.type === "boolean") {
567
+ if (typeof defaultValue === "boolean" && (configState.type === "boolean" || configState.type === "string")) {
568
568
  const boolValue = parseConfigBoolean(value);
569
569
  const hasBoolean = typeof boolValue === "boolean";
570
570
  return {
@@ -586,7 +586,7 @@ const parseConfigValue = (configState, defaultValue) => {
586
586
  reason: hasNumber ? "found-match" : "invalid-number"
587
587
  };
588
588
  }
589
- if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum")) {
589
+ if (isNumericNativeType(typeof defaultValue) && (configState.type === "float" || configState.type === "enum" || configState.type === "string")) {
590
590
  const numValue = parseConfigFloat(value);
591
591
  const hasNumber = typeof numValue === "number";
592
592
  return {
@@ -873,7 +873,7 @@ const CLIENT_BASE_URL = "https://client-sdk-api.configdirector.com";
873
873
  const MAX_EXPONENTIAL_DELAY = 9;
874
874
  var DefaultConfigDirectorClient = class {
875
875
  constructor(telemetryClient, clientSdkKey, sdkOptions, clientOptions, internalClientOptions) {
876
- var _clientOptions$logger, _clientOptions$connec, _clientOptions$connec2, _internalClientOption, _this$parseUrl, _clientOptions$connec3, _clientOptions$connec4, _clientOptions$connec5, _navigator, _internalClientOption2, _clientOptions$connec6;
876
+ 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;
877
877
  _defineProperty(this, "logger", void 0);
878
878
  _defineProperty(this, "telemetryClient", void 0);
879
879
  _defineProperty(this, "configSet", void 0);
@@ -892,6 +892,7 @@ var DefaultConfigDirectorClient = class {
892
892
  this.instanceId = crypto.randomUUID();
893
893
  this.logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createDefaultLogger();
894
894
  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;
895
+ this.validateSdkKeyPresence(clientSdkKey);
895
896
  const urlFactory = (_internalClientOption = internalClientOptions === null || internalClientOptions === void 0 ? void 0 : internalClientOptions.urlFactory) !== null && _internalClientOption !== void 0 ? _internalClientOption : defaultUrlFactory;
896
897
  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;
897
898
  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";
@@ -921,18 +922,27 @@ var DefaultConfigDirectorClient = class {
921
922
  const configKeys = Object.keys(configSet.configs);
922
923
  if (!this.configSet || configSet.kind == "full") {
923
924
  this.configSet = configSet;
924
- this.eventEmitter.emit("configsUpdated", { keys: configKeys });
925
+ this.emit("configsUpdated", { keys: configKeys });
925
926
  this.updateWatchers(configSet.configs);
926
927
  } else {
927
928
  this.configSet.configs = {
928
929
  ...this.configSet.configs,
929
930
  ...configSet.configs
930
931
  };
931
- this.eventEmitter.emit("configsUpdated", { keys: configKeys });
932
+ this.emit("configsUpdated", { keys: configKeys });
932
933
  this.updateWatchers(configSet.configs);
933
934
  }
934
935
  this.logger.debug("[ConfigDirectorClient] ConfigSet updated from server:", { keys: configKeys });
935
936
  });
937
+ this.registerHandler("clientReady", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks = clientOptions.hooks) === null || _clientOptions$hooks === void 0 ? void 0 : _clientOptions$hooks.clientReady);
938
+ this.registerHandler("configEvaluated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks2 = clientOptions.hooks) === null || _clientOptions$hooks2 === void 0 ? void 0 : _clientOptions$hooks2.configEvaluated);
939
+ this.registerHandler("configsUpdated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks3 = clientOptions.hooks) === null || _clientOptions$hooks3 === void 0 ? void 0 : _clientOptions$hooks3.configsUpdated);
940
+ this.registerHandler("contextUpdated", clientOptions === null || clientOptions === void 0 || (_clientOptions$hooks4 = clientOptions.hooks) === null || _clientOptions$hooks4 === void 0 ? void 0 : _clientOptions$hooks4.contextUpdated);
941
+ }
942
+ registerHandler(event, handler) {
943
+ if (!handler) return;
944
+ const handlers = Array.isArray(handler) ? handler : [handler];
945
+ for (const h of handlers) if (typeof h === "function") this.on(event, h);
936
946
  }
937
947
  getTransportConstructor(connectionMode) {
938
948
  switch (connectionMode) {
@@ -959,14 +969,14 @@ var DefaultConfigDirectorClient = class {
959
969
  }).then(() => {
960
970
  this.ready = true;
961
971
  this.initializing = false;
962
- this.eventEmitter.emit("clientReady", { action: caller });
972
+ this.emit("clientReady", { action: caller });
963
973
  this.logger.debug("[ConfigDirectorClient] Received initial payload from the server, client is ready");
964
974
  });
965
975
  const startTime = (/* @__PURE__ */ new Date()).getTime();
966
976
  await this.transport.connect(context !== null && context !== void 0 ? context : {}, this.timeout);
967
977
  this.currentContext = context;
968
978
  this.telemetryClient.updateContext(context);
969
- this.eventEmitter.emit("contextUpdated", { context });
979
+ this.emit("contextUpdated", { context });
970
980
  const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - startTime;
971
981
  const remainingTimeout = this.timeout - elapsedTime;
972
982
  clearTimeout(this.timeoutTimer);
@@ -1016,10 +1026,11 @@ var DefaultConfigDirectorClient = class {
1016
1026
  return this.getValueFromConfigState(configKey, configState, defaultValue);
1017
1027
  }
1018
1028
  getValueFromConfigState(configKey, configState, defaultValue) {
1019
- var _this$currentContext2;
1029
+ var _this$currentContext2, _parseResult$parsedVa;
1020
1030
  if (!configState) {
1021
1031
  var _this$currentContext;
1022
1032
  this.logger.debug(`[ConfigDirectorClient] No config state found for '${configKey}', returning default value '${defaultValue}'`);
1033
+ const reason = this.isReady ? "config-state-missing" : "client-not-ready";
1023
1034
  this.telemetryClient.evaluatedConfig({
1024
1035
  contextId: (_this$currentContext = this.currentContext) === null || _this$currentContext === void 0 ? void 0 : _this$currentContext.id,
1025
1036
  key: configKey,
@@ -1027,7 +1038,14 @@ var DefaultConfigDirectorClient = class {
1027
1038
  requestedType: getRequestedType(defaultValue),
1028
1039
  evaluatedValue: defaultValue,
1029
1040
  usedDefault: true,
1030
- evaluationReason: "config-state-missing"
1041
+ evaluationReason: reason
1042
+ });
1043
+ this.dispatchEvaluationEvent({
1044
+ key: configKey,
1045
+ value: defaultValue,
1046
+ isDefaultValue: true,
1047
+ reason,
1048
+ context: this.context
1031
1049
  });
1032
1050
  return defaultValue;
1033
1051
  }
@@ -1043,9 +1061,27 @@ var DefaultConfigDirectorClient = class {
1043
1061
  usedDefault: parseResult.usedDefault,
1044
1062
  evaluationReason: parseResult.reason
1045
1063
  });
1064
+ this.dispatchEvaluationEvent({
1065
+ key: configKey,
1066
+ value: parseResult.parsedValue,
1067
+ valueId: (_parseResult$parsedVa = parseResult.parsedValueId) !== null && _parseResult$parsedVa !== void 0 ? _parseResult$parsedVa : void 0,
1068
+ isDefaultValue: parseResult.usedDefault,
1069
+ reason: parseResult.reason,
1070
+ context: this.context
1071
+ });
1046
1072
  this.logger.debug(`[ConfigDirectorClient] Evaluated '${configKey}' to '${parseResult.parsedValue}'`);
1047
1073
  return parseResult.parsedValue;
1048
1074
  }
1075
+ dispatchEvaluationEvent(event) {
1076
+ setTimeout(() => this.emit("configEvaluated", { evaluation: event }), 0);
1077
+ }
1078
+ emit(name, payload) {
1079
+ try {
1080
+ this.eventEmitter.emit(name, payload);
1081
+ } catch (error) {
1082
+ this.logger.error(`[ConfigDirectorClient] Error executing event handlers for '${name}'`, error);
1083
+ }
1084
+ }
1049
1085
  parseUrl(url, urlFactory) {
1050
1086
  if (!url) return;
1051
1087
  try {
@@ -1054,6 +1090,9 @@ var DefaultConfigDirectorClient = class {
1054
1090
  throw new ConfigDirectorValidationError(`Invalid base URL '${url}'. Parsing failed: ${error}`);
1055
1091
  }
1056
1092
  }
1093
+ validateSdkKeyPresence(sdkKey) {
1094
+ 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");
1095
+ }
1057
1096
  validateDefaultValue(defaultValue) {
1058
1097
  if (defaultValue === void 0 || defaultValue === null) throw new ConfigDirectorValidationError("Invalid default value. The default value for a config must be defined and non-null.");
1059
1098
  if (typeof defaultValue === "function") throw new ConfigDirectorValidationError("Invalid default value. The default value for a config cannot be a function.");
@@ -1530,13 +1569,12 @@ const toBase62 = (buf, length) => {
1530
1569
  }
1531
1570
  return result.padStart(length, "0");
1532
1571
  };
1572
+ const BASE62_LENGTH = Math.ceil(128 / Math.log2(62));
1533
1573
  //#endregion
1534
1574
  //#region src/value-id-generator.ts
1535
- const DIGEST_BYTES = 16;
1536
- const BASE62_LENGTH = Math.ceil(DIGEST_BYTES * 8 / Math.log2(62));
1537
1575
  const generateValueId = (v) => {
1538
1576
  var _v$toString;
1539
- return toBase62(sha256(encodeUtf8((_v$toString = v === null || v === void 0 ? void 0 : v.toString()) !== null && _v$toString !== void 0 ? _v$toString : "")).subarray(0, DIGEST_BYTES), BASE62_LENGTH);
1577
+ return toBase62(sha256(encodeUtf8((_v$toString = v === null || v === void 0 ? void 0 : v.toString()) !== null && _v$toString !== void 0 ? _v$toString : "")).subarray(0, 16), BASE62_LENGTH);
1540
1578
  };
1541
1579
  //#endregion
1542
1580
  //#region src/ReactNativeTelemetryClient.ts
@@ -1597,7 +1635,7 @@ const createClient = (clientSdkKey, clientOptions) => {
1597
1635
  const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
1598
1636
  return new DefaultConfigDirectorClient(new ReactNativeTelemetryClient(clientSdkKey, (clientOptions === null || clientOptions === void 0 || (_clientOptions$connec = clientOptions.connection) === null || _clientOptions$connec === void 0 ? void 0 : _clientOptions$connec.url) ? urlFactory(clientOptions.connection.url) : CLIENT_BASE_URL, logger, urlFactory), clientSdkKey, {
1599
1637
  sdkName: "react-native-sdk",
1600
- sdkVersion: "0.8.0"
1638
+ sdkVersion: "0.9.1"
1601
1639
  }, {
1602
1640
  ...clientOptions,
1603
1641
  logger
@@ -1647,7 +1685,8 @@ var ConfigDirectorProvider = class extends Component {
1647
1685
  appName: props.appName,
1648
1686
  appVersion: props.appVersion
1649
1687
  },
1650
- logger: (_props$logger = props.logger) !== null && _props$logger !== void 0 ? _props$logger : createConsoleLogger("warn")
1688
+ logger: (_props$logger = props.logger) !== null && _props$logger !== void 0 ? _props$logger : createConsoleLogger("warn"),
1689
+ hooks: props.hooks
1651
1690
  }),
1652
1691
  status: "loading"
1653
1692
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/react-native-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
@@ -68,7 +68,7 @@
68
68
  "@types/wtfnode": "^0",
69
69
  "babel-jest": "^30.3.0",
70
70
  "del-cli": "^7.0.0",
71
- "eslint": "^10.2.0",
71
+ "eslint": "^10.7.0",
72
72
  "jest": "^30.3.0",
73
73
  "prettier": "^3.8.2",
74
74
  "react": "19.1.0",