@schematichq/schematic-react 1.4.1 → 1.5.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.
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  UsagePeriod: () => UsagePeriod,
38
38
  useSchematic: () => useSchematic,
39
39
  useSchematicContext: () => useSchematicContext,
40
+ useSchematicCreditBalance: () => useSchematicCreditBalance,
40
41
  useSchematicEntitlement: () => useSchematicEntitlement,
41
42
  useSchematicEvents: () => useSchematicEvents,
42
43
  useSchematicFlag: () => useSchematicFlag,
@@ -53,7 +54,11 @@ var __getOwnPropNames2 = Object.getOwnPropertyNames;
53
54
  var __getProtoOf2 = Object.getPrototypeOf;
54
55
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
55
56
  var __commonJS = (cb, mod) => function __require() {
56
- return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
57
+ try {
58
+ return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
59
+ } catch (e) {
60
+ throw mod = 0, e;
61
+ }
57
62
  };
58
63
  var __copyProps2 = (to, from, except, desc) => {
59
64
  if (from && typeof from === "object" || typeof from === "function") {
@@ -612,19 +617,16 @@ for (let i = 0; i < 256; ++i) {
612
617
  function unsafeStringify(arr, offset = 0) {
613
618
  return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
614
619
  }
615
- var getRandomValues;
616
620
  var rnds8 = new Uint8Array(16);
617
621
  function rng() {
618
- if (!getRandomValues) {
619
- if (typeof crypto === "undefined" || !crypto.getRandomValues) {
620
- throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
621
- }
622
- getRandomValues = crypto.getRandomValues.bind(crypto);
622
+ return crypto.getRandomValues(rnds8);
623
+ }
624
+ function v4(options, buf, offset) {
625
+ if (!buf && !options && crypto.randomUUID) {
626
+ return crypto.randomUUID();
623
627
  }
624
- return getRandomValues(rnds8);
628
+ return _v4(options, buf, offset);
625
629
  }
626
- var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
627
- var native_default = { randomUUID };
628
630
  function _v4(options, buf, offset) {
629
631
  options = options || {};
630
632
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -645,12 +647,6 @@ function _v4(options, buf, offset) {
645
647
  }
646
648
  return unsafeStringify(rnds);
647
649
  }
648
- function v4(options, buf, offset) {
649
- if (native_default.randomUUID && !buf && !options) {
650
- return native_default.randomUUID();
651
- }
652
- return _v4(options, buf, offset);
653
- }
654
650
  var v4_default = v4;
655
651
  var import_polyfill = __toESM2(require_browser_polyfill());
656
652
  function EntitlementValueTypeFromJSON(json) {
@@ -659,6 +655,18 @@ function EntitlementValueTypeFromJSON(json) {
659
655
  function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
660
656
  return json;
661
657
  }
658
+ function MetricPeriodMonthResetFromJSON(json) {
659
+ return MetricPeriodMonthResetFromJSONTyped(json, false);
660
+ }
661
+ function MetricPeriodMonthResetFromJSONTyped(json, ignoreDiscriminator) {
662
+ return json;
663
+ }
664
+ function MetricPeriodFromJSON(json) {
665
+ return MetricPeriodFromJSONTyped(json, false);
666
+ }
667
+ function MetricPeriodFromJSONTyped(json, ignoreDiscriminator) {
668
+ return json;
669
+ }
662
670
  function FeatureEntitlementFromJSON(json) {
663
671
  return FeatureEntitlementFromJSONTyped(json, false);
664
672
  }
@@ -668,21 +676,31 @@ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
668
676
  }
669
677
  return {
670
678
  allocation: json["allocation"] == null ? void 0 : json["allocation"],
679
+ consumptionRate: json["consumption_rate"] == null ? void 0 : json["consumption_rate"],
671
680
  creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
672
681
  creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
682
+ creditReserved: json["credit_reserved"] == null ? void 0 : json["credit_reserved"],
683
+ creditSettled: json["credit_settled"] == null ? void 0 : json["credit_settled"],
673
684
  creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
674
685
  creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
675
686
  eventName: json["event_name"] == null ? void 0 : json["event_name"],
687
+ eventSubtype: json["event_subtype"] == null ? void 0 : json["event_subtype"],
676
688
  featureId: json["feature_id"],
677
689
  featureKey: json["feature_key"],
678
- metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
690
+ metricPeriod: json["metric_period"] == null ? void 0 : MetricPeriodFromJSON(json["metric_period"]),
679
691
  metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
680
- monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
692
+ monthReset: json["month_reset"] == null ? void 0 : MetricPeriodMonthResetFromJSON(json["month_reset"]),
681
693
  softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
682
694
  usage: json["usage"] == null ? void 0 : json["usage"],
683
695
  valueType: EntitlementValueTypeFromJSON(json["value_type"])
684
696
  };
685
697
  }
698
+ function RuleTypeFromJSON(json) {
699
+ return RuleTypeFromJSONTyped(json, false);
700
+ }
701
+ function RuleTypeFromJSONTyped(json, ignoreDiscriminator) {
702
+ return json;
703
+ }
686
704
  function CheckFlagResponseDataFromJSON(json) {
687
705
  return CheckFlagResponseDataFromJSONTyped(json, false);
688
706
  }
@@ -697,13 +715,13 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
697
715
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
698
716
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
699
717
  featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
700
- featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
718
+ featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : MetricPeriodFromJSON(json["feature_usage_period"]),
701
719
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
702
720
  flag: json["flag"],
703
721
  flagId: json["flag_id"] == null ? void 0 : json["flag_id"],
704
722
  reason: json["reason"],
705
723
  ruleId: json["rule_id"] == null ? void 0 : json["rule_id"],
706
- ruleType: json["rule_type"] == null ? void 0 : json["rule_type"],
724
+ ruleType: json["rule_type"] == null ? void 0 : RuleTypeFromJSON(json["rule_type"]),
707
725
  userId: json["user_id"] == null ? void 0 : json["user_id"],
708
726
  value: json["value"]
709
727
  };
@@ -720,6 +738,268 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
720
738
  params: json["params"]
721
739
  };
722
740
  }
741
+ var BASE_PATH = "https://api.schematichq.com".replace(/\/+$/, "");
742
+ var Configuration = class {
743
+ constructor(configuration = {}) {
744
+ this.configuration = configuration;
745
+ }
746
+ configuration;
747
+ set config(configuration) {
748
+ this.configuration = configuration;
749
+ }
750
+ get basePath() {
751
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
752
+ }
753
+ get fetchApi() {
754
+ return this.configuration.fetchApi;
755
+ }
756
+ get middleware() {
757
+ return this.configuration.middleware || [];
758
+ }
759
+ get queryParamsStringify() {
760
+ return this.configuration.queryParamsStringify || querystring;
761
+ }
762
+ get username() {
763
+ return this.configuration.username;
764
+ }
765
+ get password() {
766
+ return this.configuration.password;
767
+ }
768
+ get apiKey() {
769
+ const apiKey = this.configuration.apiKey;
770
+ if (apiKey) {
771
+ return typeof apiKey === "function" ? apiKey : () => apiKey;
772
+ }
773
+ return void 0;
774
+ }
775
+ get accessToken() {
776
+ const accessToken = this.configuration.accessToken;
777
+ if (accessToken) {
778
+ return typeof accessToken === "function" ? accessToken : async () => accessToken;
779
+ }
780
+ return void 0;
781
+ }
782
+ get headers() {
783
+ return this.configuration.headers;
784
+ }
785
+ get credentials() {
786
+ return this.configuration.credentials;
787
+ }
788
+ };
789
+ var DefaultConfig = new Configuration();
790
+ var BaseAPI = class _BaseAPI {
791
+ constructor(configuration = DefaultConfig) {
792
+ this.configuration = configuration;
793
+ this.middleware = configuration.middleware;
794
+ }
795
+ configuration;
796
+ static jsonRegex = new RegExp(
797
+ "^(:?application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(:?;.*)?$",
798
+ "i"
799
+ );
800
+ middleware;
801
+ withMiddleware(...middlewares) {
802
+ const next = this.clone();
803
+ next.middleware = next.middleware.concat(...middlewares);
804
+ return next;
805
+ }
806
+ withPreMiddleware(...preMiddlewares) {
807
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
808
+ return this.withMiddleware(...middlewares);
809
+ }
810
+ withPostMiddleware(...postMiddlewares) {
811
+ const middlewares = postMiddlewares.map((post) => ({ post }));
812
+ return this.withMiddleware(...middlewares);
813
+ }
814
+ /**
815
+ * Check if the given MIME is a JSON MIME.
816
+ * JSON MIME examples:
817
+ * application/json
818
+ * application/json; charset=UTF8
819
+ * APPLICATION/JSON
820
+ * application/vnd.company+json
821
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
822
+ * @return True if the given MIME is JSON, false otherwise.
823
+ */
824
+ isJsonMime(mime) {
825
+ if (!mime) {
826
+ return false;
827
+ }
828
+ return _BaseAPI.jsonRegex.test(mime);
829
+ }
830
+ async request(context, initOverrides) {
831
+ const { url, init } = await this.createFetchParams(context, initOverrides);
832
+ const response = await this.fetchApi(url, init);
833
+ if (response && response.status >= 200 && response.status < 300) {
834
+ return response;
835
+ }
836
+ throw new ResponseError(response, "Response returned an error code");
837
+ }
838
+ async createFetchParams(context, initOverrides) {
839
+ let url = this.configuration.basePath + context.path;
840
+ if (context.query !== void 0 && Object.keys(context.query).length !== 0) {
841
+ url += "?" + this.configuration.queryParamsStringify(context.query);
842
+ }
843
+ const headers = Object.assign(
844
+ {},
845
+ this.configuration.headers,
846
+ context.headers
847
+ );
848
+ Object.keys(headers).forEach(
849
+ (key) => headers[key] === void 0 ? delete headers[key] : {}
850
+ );
851
+ const initOverrideFn = typeof initOverrides === "function" ? initOverrides : async () => initOverrides;
852
+ const initParams = {
853
+ method: context.method,
854
+ headers,
855
+ body: context.body,
856
+ credentials: this.configuration.credentials
857
+ };
858
+ const overriddenInit = {
859
+ ...initParams,
860
+ ...await initOverrideFn({
861
+ init: initParams,
862
+ context
863
+ })
864
+ };
865
+ let body;
866
+ if (isFormData(overriddenInit.body) || overriddenInit.body instanceof URLSearchParams || isBlob(overriddenInit.body)) {
867
+ body = overriddenInit.body;
868
+ } else if (this.isJsonMime(headers["Content-Type"])) {
869
+ body = JSON.stringify(overriddenInit.body);
870
+ } else {
871
+ body = overriddenInit.body;
872
+ }
873
+ const init = {
874
+ ...overriddenInit,
875
+ body
876
+ };
877
+ return { url, init };
878
+ }
879
+ fetchApi = async (url, init) => {
880
+ let fetchParams = { url, init };
881
+ for (const middleware of this.middleware) {
882
+ if (middleware.pre) {
883
+ fetchParams = await middleware.pre({
884
+ fetch: this.fetchApi,
885
+ ...fetchParams
886
+ }) || fetchParams;
887
+ }
888
+ }
889
+ let response = void 0;
890
+ try {
891
+ response = await (this.configuration.fetchApi || fetch)(
892
+ fetchParams.url,
893
+ fetchParams.init
894
+ );
895
+ } catch (e) {
896
+ for (const middleware of this.middleware) {
897
+ if (middleware.onError) {
898
+ response = await middleware.onError({
899
+ fetch: this.fetchApi,
900
+ url: fetchParams.url,
901
+ init: fetchParams.init,
902
+ error: e,
903
+ response: response ? response.clone() : void 0
904
+ }) || response;
905
+ }
906
+ }
907
+ if (response === void 0) {
908
+ if (e instanceof Error) {
909
+ throw new FetchError(
910
+ e,
911
+ "The request failed and the interceptors did not return an alternative response"
912
+ );
913
+ } else {
914
+ throw e;
915
+ }
916
+ }
917
+ }
918
+ for (const middleware of this.middleware) {
919
+ if (middleware.post) {
920
+ response = await middleware.post({
921
+ fetch: this.fetchApi,
922
+ url: fetchParams.url,
923
+ init: fetchParams.init,
924
+ response: response.clone()
925
+ }) || response;
926
+ }
927
+ }
928
+ return response;
929
+ };
930
+ /**
931
+ * Create a shallow clone of `this` by constructing a new instance
932
+ * and then shallow cloning data members.
933
+ */
934
+ clone() {
935
+ const constructor = this.constructor;
936
+ const next = new constructor(this.configuration);
937
+ next.middleware = this.middleware.slice();
938
+ return next;
939
+ }
940
+ };
941
+ function isBlob(value) {
942
+ return typeof Blob !== "undefined" && value instanceof Blob;
943
+ }
944
+ function isFormData(value) {
945
+ return typeof FormData !== "undefined" && value instanceof FormData;
946
+ }
947
+ var ResponseError = class extends Error {
948
+ constructor(response, msg) {
949
+ super(msg);
950
+ this.response = response;
951
+ }
952
+ response;
953
+ name = "ResponseError";
954
+ };
955
+ var FetchError = class extends Error {
956
+ constructor(cause, msg) {
957
+ super(msg);
958
+ this.cause = cause;
959
+ }
960
+ cause;
961
+ name = "FetchError";
962
+ };
963
+ function querystring(params, prefix = "") {
964
+ return Object.keys(params).map((key) => querystringSingleKey(key, params[key], prefix)).filter((part) => part.length > 0).join("&");
965
+ }
966
+ function querystringSingleKey(key, value, keyPrefix = "") {
967
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
968
+ if (value instanceof Array) {
969
+ const multiValue = value.map((singleValue) => encodeURIComponent(String(singleValue))).join(`&${encodeURIComponent(fullKey)}=`);
970
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
971
+ }
972
+ if (value instanceof Set) {
973
+ const valueAsArray = Array.from(value);
974
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
975
+ }
976
+ if (value instanceof Date) {
977
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
978
+ }
979
+ if (value instanceof Object) {
980
+ return querystring(value, fullKey);
981
+ }
982
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
983
+ }
984
+ function mapValues(data, fn) {
985
+ return Object.keys(data).reduce(
986
+ (acc, key) => ({ ...acc, [key]: fn(data[key]) }),
987
+ {}
988
+ );
989
+ }
990
+ function CompanyCreditBalanceFromJSON(json) {
991
+ return CompanyCreditBalanceFromJSONTyped(json, false);
992
+ }
993
+ function CompanyCreditBalanceFromJSONTyped(json, ignoreDiscriminator) {
994
+ if (json == null) {
995
+ return json;
996
+ }
997
+ return {
998
+ remaining: json["remaining"],
999
+ reserved: json["reserved"],
1000
+ settled: json["settled"]
1001
+ };
1002
+ }
723
1003
  var TrialStatus = {
724
1004
  Active: "active",
725
1005
  Converted: "converted",
@@ -753,6 +1033,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
753
1033
  return json;
754
1034
  }
755
1035
  return {
1036
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
756
1037
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
757
1038
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
758
1039
  };
@@ -835,7 +1116,10 @@ var CheckFlagReturnFromJSON = (json) => {
835
1116
  return {
836
1117
  featureUsageExceeded,
837
1118
  companyId: companyId == null ? void 0 : companyId,
1119
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
838
1120
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1121
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1122
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
839
1123
  error: error == null ? void 0 : error,
840
1124
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
841
1125
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -861,6 +1145,22 @@ var CheckPlanReturnFromJSON = (json) => {
861
1145
  trialStatus: trialStatus == null ? void 0 : trialStatus
862
1146
  };
863
1147
  };
1148
+ var CreditBalancesFromJSON = (json) => {
1149
+ if (json == null || typeof json !== "object") {
1150
+ return {};
1151
+ }
1152
+ const balances = {};
1153
+ for (const [creditId, raw] of Object.entries(json)) {
1154
+ if (raw == null || typeof raw !== "object") continue;
1155
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1156
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1157
+ continue;
1158
+ }
1159
+ balances[creditId] = { remaining, reserved, settled };
1160
+ }
1161
+ return balances;
1162
+ };
1163
+ var cacheVersion = "bb56e75d";
864
1164
  function contextString(context) {
865
1165
  const sortedContext = Object.keys(context).reduce((acc, key) => {
866
1166
  const sortedKeys = Object.keys(
@@ -875,8 +1175,12 @@ function contextString(context) {
875
1175
  }, {});
876
1176
  return JSON.stringify(sortedContext);
877
1177
  }
878
- var version = "1.4.0";
1178
+ var version = "1.5.0";
879
1179
  var anonymousIdKey = "schematicId";
1180
+ var flagStateCachePrefix = "schematicCache";
1181
+ var flagStateCacheMaxContexts = 10;
1182
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1183
+ var emptyCreditBalances = Object.freeze({});
880
1184
  var Schematic = class {
881
1185
  additionalHeaders = {};
882
1186
  apiKey;
@@ -893,11 +1197,17 @@ var Schematic = class {
893
1197
  isPending = true;
894
1198
  isPendingListeners = /* @__PURE__ */ new Set();
895
1199
  planListeners = /* @__PURE__ */ new Set();
1200
+ creditBalanceListeners = /* @__PURE__ */ new Set();
896
1201
  storage;
1202
+ persistFlagState = true;
1203
+ flagStateCacheKey;
1204
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1205
+ cachedFlagState = null;
897
1206
  useWebSocket = false;
898
1207
  checks = {};
899
1208
  featureUsageEventMap = {};
900
1209
  planChecks = {};
1210
+ creditBalances = {};
901
1211
  webSocketUrl = "wss://api.schematichq.com";
902
1212
  webSocketConnectionTimeout = 1e4;
903
1213
  webSocketReconnect = true;
@@ -926,11 +1236,16 @@ var Schematic = class {
926
1236
  fallbackCheckCache = {};
927
1237
  constructor(apiKey, options) {
928
1238
  this.apiKey = apiKey;
1239
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
929
1240
  this.eventQueue = [];
930
1241
  this.contextDependentEventQueue = [];
931
1242
  this.useWebSocket = options?.useWebSocket ?? false;
932
1243
  this.debugEnabled = options?.debug ?? false;
933
1244
  this.offlineEnabled = options?.offline ?? false;
1245
+ this.persistFlagState = options?.persistFlagState ?? true;
1246
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1247
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1248
+ }
934
1249
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
935
1250
  const params = new URLSearchParams(window.location.search);
936
1251
  const debugParam = params.get("schematic_debug");
@@ -963,6 +1278,7 @@ var Schematic = class {
963
1278
  } catch {
964
1279
  }
965
1280
  }
1281
+ this.hydrateFlagStateFromCache();
966
1282
  if (options?.apiUrl !== void 0) {
967
1283
  this.apiUrl = options.apiUrl;
968
1284
  }
@@ -1256,6 +1572,29 @@ var Schematic = class {
1256
1572
  });
1257
1573
  this.notifyPlanListeners(plan);
1258
1574
  }
1575
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1576
+ const contextStr = contextString(context);
1577
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1578
+ const previous = this.creditBalances[contextStr] ?? {};
1579
+ let changed = false;
1580
+ const merged = { ...previous };
1581
+ for (const [creditId, next] of Object.entries(updates)) {
1582
+ const current = previous[creditId];
1583
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1584
+ merged[creditId] = next;
1585
+ changed = true;
1586
+ }
1587
+ }
1588
+ if (changed) {
1589
+ this.creditBalances[contextStr] = merged;
1590
+ this.debug(
1591
+ `WebSocket credit balance update received. Notifying listeners`,
1592
+ { creditBalances: merged }
1593
+ );
1594
+ this.notifyCreditBalanceListeners(merged);
1595
+ }
1596
+ }
1597
+ this.persistContextToCache(contextString(context));
1259
1598
  this.flushContextDependentEventQueue();
1260
1599
  this.setIsPending(false);
1261
1600
  };
@@ -1363,7 +1702,32 @@ var Schematic = class {
1363
1702
  return;
1364
1703
  }
1365
1704
  try {
1366
- this.setIsPending(true);
1705
+ const newContextStr = contextString(context);
1706
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1707
+ if (cacheHit) {
1708
+ this.debug(
1709
+ `setContext: cache hit for context, pre-resolving from cached values`
1710
+ );
1711
+ this.context = context;
1712
+ this.flushContextDependentEventQueue();
1713
+ this.setIsPending(false);
1714
+ const cachedChecks = this.checks[newContextStr] ?? {};
1715
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1716
+ if (check === void 0) continue;
1717
+ this.notifyFlagCheckListeners(flagKey, check);
1718
+ this.notifyFlagValueListeners(flagKey, check.value);
1719
+ }
1720
+ const cachedPlan = this.planChecks[newContextStr];
1721
+ if (cachedPlan !== void 0) {
1722
+ this.notifyPlanListeners(cachedPlan);
1723
+ }
1724
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1725
+ if (cachedCreditBalances !== void 0) {
1726
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1727
+ }
1728
+ } else {
1729
+ this.setIsPending(true);
1730
+ }
1367
1731
  if (!this.conn) {
1368
1732
  if (this.isConnecting) {
1369
1733
  this.debug(
@@ -1374,7 +1738,7 @@ var Schematic = class {
1374
1738
  }
1375
1739
  if (this.conn !== null) {
1376
1740
  const socket2 = await this.conn;
1377
- await this.wsSendMessage(socket2, context);
1741
+ await this.wsSendMessage(socket2, context, cacheHit);
1378
1742
  return;
1379
1743
  }
1380
1744
  }
@@ -1390,7 +1754,7 @@ var Schematic = class {
1390
1754
  this.conn = this.wsConnect();
1391
1755
  const socket2 = await this.conn;
1392
1756
  this.isConnecting = false;
1393
- await this.wsSendMessage(socket2, context);
1757
+ await this.wsSendMessage(socket2, context, cacheHit);
1394
1758
  return;
1395
1759
  } catch (error) {
1396
1760
  this.isConnecting = false;
@@ -1398,7 +1762,7 @@ var Schematic = class {
1398
1762
  }
1399
1763
  }
1400
1764
  const socket = await this.conn;
1401
- await this.wsSendMessage(socket, context);
1765
+ await this.wsSendMessage(socket, context, cacheHit);
1402
1766
  } catch (error) {
1403
1767
  console.warn("Failed to establish WebSocket connection:", error);
1404
1768
  this.context = context;
@@ -1458,10 +1822,12 @@ var Schematic = class {
1458
1822
  `Optimistically updating feature usage for event: ${eventName}`,
1459
1823
  { quantity }
1460
1824
  );
1825
+ let dirty = false;
1461
1826
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1462
1827
  if (check === void 0) return;
1463
1828
  const updatedCheck = { ...check };
1464
1829
  if (typeof updatedCheck.featureUsage === "number") {
1830
+ dirty = true;
1465
1831
  updatedCheck.featureUsage += quantity;
1466
1832
  if (typeof updatedCheck.featureAllocation === "number") {
1467
1833
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1491,6 +1857,9 @@ var Schematic = class {
1491
1857
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1492
1858
  }
1493
1859
  });
1860
+ if (dirty) {
1861
+ this.persistContextToCache(contextString(this.context));
1862
+ }
1494
1863
  };
1495
1864
  /**
1496
1865
  * Event processing
@@ -1593,6 +1962,206 @@ var Schematic = class {
1593
1962
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1594
1963
  return generatedAnonymousId;
1595
1964
  };
1965
+ hasCachedValuesForContext = (contextStr) => {
1966
+ const cachedChecks = this.checks[contextStr];
1967
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1968
+ return true;
1969
+ }
1970
+ if (this.planChecks[contextStr] !== void 0) {
1971
+ return true;
1972
+ }
1973
+ const cachedCreditBalances = this.creditBalances[contextStr];
1974
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1975
+ };
1976
+ readFlagStateCache = () => {
1977
+ if (!this.persistFlagState || this.storage === void 0) {
1978
+ return null;
1979
+ }
1980
+ let raw;
1981
+ try {
1982
+ raw = this.storage.getItem(this.flagStateCacheKey);
1983
+ } catch (error) {
1984
+ this.debug("Failed to read flag state cache:", error);
1985
+ return null;
1986
+ }
1987
+ if (raw === null || raw === void 0) {
1988
+ return null;
1989
+ }
1990
+ let parsed;
1991
+ try {
1992
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1993
+ } catch (error) {
1994
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
1995
+ return null;
1996
+ }
1997
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
1998
+ this.debug(
1999
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
2000
+ {
2001
+ parsedType: typeof parsed,
2002
+ version: parsed?.version
2003
+ }
2004
+ );
2005
+ return null;
2006
+ }
2007
+ return parsed;
2008
+ };
2009
+ reviveCachedCheck = (raw) => {
2010
+ if (raw === null || typeof raw !== "object") return null;
2011
+ const obj = raw;
2012
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
2013
+ return null;
2014
+ }
2015
+ const revived = { ...obj };
2016
+ if (typeof revived.featureUsageResetAt === "string") {
2017
+ const d = new Date(revived.featureUsageResetAt);
2018
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
2019
+ }
2020
+ return revived;
2021
+ };
2022
+ reviveCachedPlan = (raw) => {
2023
+ if (raw === null || typeof raw !== "object") return null;
2024
+ const obj = raw;
2025
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
2026
+ return null;
2027
+ }
2028
+ const revived = { ...obj };
2029
+ if (typeof revived.trialEndDate === "string") {
2030
+ const d = new Date(revived.trialEndDate);
2031
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
2032
+ }
2033
+ return revived;
2034
+ };
2035
+ reviveCachedCreditBalances = (raw) => {
2036
+ if (raw === null || typeof raw !== "object") return null;
2037
+ const revived = {};
2038
+ for (const [creditId, value] of Object.entries(
2039
+ raw
2040
+ )) {
2041
+ if (value === null || typeof value !== "object") continue;
2042
+ const obj = value;
2043
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
2044
+ continue;
2045
+ }
2046
+ revived[creditId] = {
2047
+ remaining: obj.remaining,
2048
+ reserved: obj.reserved,
2049
+ settled: obj.settled
2050
+ };
2051
+ }
2052
+ return Object.keys(revived).length > 0 ? revived : null;
2053
+ };
2054
+ hydrateFlagStateFromCache = () => {
2055
+ const parsed = this.readFlagStateCache();
2056
+ if (parsed === null) return;
2057
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2058
+ const fresh = {};
2059
+ let contextsLoaded = 0;
2060
+ let contextsExpired = 0;
2061
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2062
+ if (entry === null || typeof entry !== "object") continue;
2063
+ if (typeof entry.updatedAt !== "number") continue;
2064
+ if (entry.updatedAt < cutoff) {
2065
+ contextsExpired += 1;
2066
+ continue;
2067
+ }
2068
+ const revivedChecks = {};
2069
+ if (entry.checks !== null && typeof entry.checks === "object") {
2070
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2071
+ const revived = this.reviveCachedCheck(rawCheck);
2072
+ if (revived === null || revived.flag !== flagKey) continue;
2073
+ revivedChecks[flagKey] = revived;
2074
+ if (typeof revived.featureUsageEvent === "string") {
2075
+ this.updateFeatureUsageEventMap(revived);
2076
+ }
2077
+ }
2078
+ if (Object.keys(revivedChecks).length > 0) {
2079
+ this.checks[contextStr] = revivedChecks;
2080
+ }
2081
+ }
2082
+ let revivedPlan;
2083
+ if (entry.plan !== void 0 && entry.plan !== null) {
2084
+ const p = this.reviveCachedPlan(entry.plan);
2085
+ if (p !== null) {
2086
+ revivedPlan = p;
2087
+ this.planChecks[contextStr] = p;
2088
+ }
2089
+ }
2090
+ let revivedCreditBalances;
2091
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2092
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2093
+ if (balances !== null) {
2094
+ revivedCreditBalances = balances;
2095
+ this.creditBalances[contextStr] = balances;
2096
+ }
2097
+ }
2098
+ fresh[contextStr] = {
2099
+ checks: revivedChecks,
2100
+ plan: revivedPlan,
2101
+ creditBalances: revivedCreditBalances,
2102
+ updatedAt: entry.updatedAt
2103
+ };
2104
+ contextsLoaded += 1;
2105
+ }
2106
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2107
+ this.debug(
2108
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2109
+ );
2110
+ };
2111
+ persistContextToCache = (contextStr) => {
2112
+ const checksForContext = this.checks[contextStr];
2113
+ const planForContext = this.planChecks[contextStr];
2114
+ const creditBalancesForContext = this.creditBalances[contextStr];
2115
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2116
+ return;
2117
+ }
2118
+ if (this.cachedFlagState === null) {
2119
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2120
+ version: cacheVersion,
2121
+ contexts: {}
2122
+ };
2123
+ }
2124
+ const cleanChecks = {};
2125
+ if (checksForContext !== void 0) {
2126
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2127
+ if (check !== void 0) cleanChecks[flagKey] = check;
2128
+ }
2129
+ }
2130
+ this.cachedFlagState.contexts[contextStr] = {
2131
+ checks: cleanChecks,
2132
+ plan: planForContext,
2133
+ creditBalances: creditBalancesForContext,
2134
+ updatedAt: Date.now()
2135
+ };
2136
+ const entries = Object.entries(this.cachedFlagState.contexts);
2137
+ if (entries.length > flagStateCacheMaxContexts) {
2138
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2139
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2140
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2141
+ for (const key of Object.keys(this.checks)) {
2142
+ if (!survivorKeys.has(key)) delete this.checks[key];
2143
+ }
2144
+ for (const key of Object.keys(this.planChecks)) {
2145
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2146
+ }
2147
+ for (const key of Object.keys(this.creditBalances)) {
2148
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2149
+ }
2150
+ this.cachedFlagState = {
2151
+ version: cacheVersion,
2152
+ contexts: Object.fromEntries(survivors)
2153
+ };
2154
+ }
2155
+ if (!this.persistFlagState || this.storage === void 0) return;
2156
+ try {
2157
+ this.storage.setItem(
2158
+ this.flagStateCacheKey,
2159
+ JSON.stringify(this.cachedFlagState)
2160
+ );
2161
+ } catch (error) {
2162
+ this.debug("Failed to write flag state cache:", error);
2163
+ }
2164
+ };
1596
2165
  handleEvent = (eventType, eventBody) => {
1597
2166
  const event = {
1598
2167
  api_key: this.apiKey,
@@ -1853,6 +2422,9 @@ var Schematic = class {
1853
2422
  if (this.conn !== null) {
1854
2423
  try {
1855
2424
  const socket = await this.conn;
2425
+ if (this.currentWebSocket === socket) {
2426
+ this.currentWebSocket = null;
2427
+ }
1856
2428
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1857
2429
  socket.close();
1858
2430
  }
@@ -2018,6 +2590,7 @@ var Schematic = class {
2018
2590
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2019
2591
  let timeoutId = null;
2020
2592
  let isResolved = false;
2593
+ let didOpen = false;
2021
2594
  timeoutId = setTimeout(() => {
2022
2595
  if (!isResolved) {
2023
2596
  isResolved = true;
@@ -2031,6 +2604,7 @@ var Schematic = class {
2031
2604
  webSocket.onopen = () => {
2032
2605
  if (isResolved) return;
2033
2606
  isResolved = true;
2607
+ didOpen = true;
2034
2608
  if (timeoutId !== null) {
2035
2609
  clearTimeout(timeoutId);
2036
2610
  }
@@ -2056,11 +2630,12 @@ var Schematic = class {
2056
2630
  }
2057
2631
  this.debug(`WebSocket connection ${connectionId} closed`);
2058
2632
  this.conn = null;
2633
+ const wasCurrent = this.currentWebSocket === webSocket;
2059
2634
  if (this.currentWebSocket === webSocket) {
2060
2635
  this.currentWebSocket = null;
2061
2636
  this.isConnecting = false;
2062
2637
  }
2063
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2638
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2064
2639
  this.attemptReconnect();
2065
2640
  }
2066
2641
  };
@@ -2156,6 +2731,16 @@ var Schematic = class {
2156
2731
  const plan = this.planChecks[contextStr];
2157
2732
  return plan;
2158
2733
  };
2734
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2735
+ getCreditBalances = () => {
2736
+ const contextStr = contextString(this.context);
2737
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2738
+ };
2739
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2740
+ getCreditBalance = (creditId) => {
2741
+ const contextStr = contextString(this.context);
2742
+ return this.creditBalances[contextStr]?.[creditId];
2743
+ };
2159
2744
  // flag checks state
2160
2745
  getFlagCheck = (flagKey) => {
2161
2746
  const contextStr = contextString(this.context);
@@ -2215,6 +2800,13 @@ var Schematic = class {
2215
2800
  this.planListeners.delete(listener);
2216
2801
  };
2217
2802
  };
2803
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2804
+ addCreditBalanceListener = (listener) => {
2805
+ this.creditBalanceListeners.add(listener);
2806
+ return () => {
2807
+ this.creditBalanceListeners.delete(listener);
2808
+ };
2809
+ };
2218
2810
  notifyFlagCheckListeners = (flagKey, check) => {
2219
2811
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2220
2812
  if (listeners.size > 0) {
@@ -2278,6 +2870,17 @@ var Schematic = class {
2278
2870
  });
2279
2871
  });
2280
2872
  };
2873
+ notifyCreditBalanceListeners = (value) => {
2874
+ const listeners = this.creditBalanceListeners ?? [];
2875
+ if (listeners.size > 0) {
2876
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2877
+ value
2878
+ });
2879
+ }
2880
+ listeners.forEach(
2881
+ (listener) => notifyCreditBalanceListener(listener, value)
2882
+ );
2883
+ };
2281
2884
  };
2282
2885
  var notifyPendingListener = (listener, value) => {
2283
2886
  if (listener.length > 0) {
@@ -2307,12 +2910,19 @@ var notifyPlanListener = (listener, value) => {
2307
2910
  listener();
2308
2911
  }
2309
2912
  };
2913
+ var notifyCreditBalanceListener = (listener, value) => {
2914
+ if (listener.length > 0) {
2915
+ listener(value);
2916
+ } else {
2917
+ listener();
2918
+ }
2919
+ };
2310
2920
 
2311
2921
  // src/context/schematic.tsx
2312
2922
  var import_react = __toESM(require("react"));
2313
2923
 
2314
2924
  // src/version.ts
2315
- var version2 = "1.4.1";
2925
+ var version2 = "1.5.0";
2316
2926
 
2317
2927
  // src/context/schematic.tsx
2318
2928
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -2455,6 +3065,34 @@ var useSchematicPlan = (opts) => {
2455
3065
  }, [client, fallbackPlan]);
2456
3066
  return (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, () => fallbackPlan);
2457
3067
  };
3068
+ var useSchematicCreditBalance = (creditId, opts) => {
3069
+ const client = useSchematicClient(opts);
3070
+ const subscribe = (0, import_react2.useCallback)(
3071
+ (callback) => client.addCreditBalanceListener(callback),
3072
+ [client]
3073
+ );
3074
+ const getSnapshot = (0, import_react2.useCallback)(
3075
+ () => client.getCreditBalance(creditId),
3076
+ [client, creditId]
3077
+ );
3078
+ const balance = (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, () => void 0);
3079
+ const isPendingSubscribe = (0, import_react2.useCallback)(
3080
+ (callback) => client.addIsPendingListener(callback),
3081
+ [client]
3082
+ );
3083
+ const isPending = (0, import_react2.useSyncExternalStore)(
3084
+ isPendingSubscribe,
3085
+ () => client.getIsPending(),
3086
+ () => true
3087
+ );
3088
+ return (0, import_react2.useMemo)(
3089
+ () => ({
3090
+ balance: balance?.settled ?? 0,
3091
+ isLoading: balance === void 0 && isPending
3092
+ }),
3093
+ [balance, isPending]
3094
+ );
3095
+ };
2458
3096
  var useSchematicIsPending = (opts) => {
2459
3097
  const client = useSchematicClient(opts);
2460
3098
  const subscribe = (0, import_react2.useCallback)(