@schematichq/schematic-react 1.4.0 → 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.
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
@@ -565,19 +569,16 @@ for (let i = 0; i < 256; ++i) {
565
569
  function unsafeStringify(arr, offset = 0) {
566
570
  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();
567
571
  }
568
- var getRandomValues;
569
572
  var rnds8 = new Uint8Array(16);
570
573
  function rng() {
571
- if (!getRandomValues) {
572
- if (typeof crypto === "undefined" || !crypto.getRandomValues) {
573
- throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
574
- }
575
- getRandomValues = crypto.getRandomValues.bind(crypto);
574
+ return crypto.getRandomValues(rnds8);
575
+ }
576
+ function v4(options, buf, offset) {
577
+ if (!buf && !options && crypto.randomUUID) {
578
+ return crypto.randomUUID();
576
579
  }
577
- return getRandomValues(rnds8);
580
+ return _v4(options, buf, offset);
578
581
  }
579
- var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
580
- var native_default = { randomUUID };
581
582
  function _v4(options, buf, offset) {
582
583
  options = options || {};
583
584
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -598,12 +599,6 @@ function _v4(options, buf, offset) {
598
599
  }
599
600
  return unsafeStringify(rnds);
600
601
  }
601
- function v4(options, buf, offset) {
602
- if (native_default.randomUUID && !buf && !options) {
603
- return native_default.randomUUID();
604
- }
605
- return _v4(options, buf, offset);
606
- }
607
602
  var v4_default = v4;
608
603
  var import_polyfill = __toESM(require_browser_polyfill());
609
604
  function EntitlementValueTypeFromJSON(json) {
@@ -612,6 +607,18 @@ function EntitlementValueTypeFromJSON(json) {
612
607
  function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
613
608
  return json;
614
609
  }
610
+ function MetricPeriodMonthResetFromJSON(json) {
611
+ return MetricPeriodMonthResetFromJSONTyped(json, false);
612
+ }
613
+ function MetricPeriodMonthResetFromJSONTyped(json, ignoreDiscriminator) {
614
+ return json;
615
+ }
616
+ function MetricPeriodFromJSON(json) {
617
+ return MetricPeriodFromJSONTyped(json, false);
618
+ }
619
+ function MetricPeriodFromJSONTyped(json, ignoreDiscriminator) {
620
+ return json;
621
+ }
615
622
  function FeatureEntitlementFromJSON(json) {
616
623
  return FeatureEntitlementFromJSONTyped(json, false);
617
624
  }
@@ -621,21 +628,31 @@ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
621
628
  }
622
629
  return {
623
630
  allocation: json["allocation"] == null ? void 0 : json["allocation"],
631
+ consumptionRate: json["consumption_rate"] == null ? void 0 : json["consumption_rate"],
624
632
  creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
625
633
  creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
634
+ creditReserved: json["credit_reserved"] == null ? void 0 : json["credit_reserved"],
635
+ creditSettled: json["credit_settled"] == null ? void 0 : json["credit_settled"],
626
636
  creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
627
637
  creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
628
638
  eventName: json["event_name"] == null ? void 0 : json["event_name"],
639
+ eventSubtype: json["event_subtype"] == null ? void 0 : json["event_subtype"],
629
640
  featureId: json["feature_id"],
630
641
  featureKey: json["feature_key"],
631
- metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
642
+ metricPeriod: json["metric_period"] == null ? void 0 : MetricPeriodFromJSON(json["metric_period"]),
632
643
  metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
633
- monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
644
+ monthReset: json["month_reset"] == null ? void 0 : MetricPeriodMonthResetFromJSON(json["month_reset"]),
634
645
  softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
635
646
  usage: json["usage"] == null ? void 0 : json["usage"],
636
647
  valueType: EntitlementValueTypeFromJSON(json["value_type"])
637
648
  };
638
649
  }
650
+ function RuleTypeFromJSON(json) {
651
+ return RuleTypeFromJSONTyped(json, false);
652
+ }
653
+ function RuleTypeFromJSONTyped(json, ignoreDiscriminator) {
654
+ return json;
655
+ }
639
656
  function CheckFlagResponseDataFromJSON(json) {
640
657
  return CheckFlagResponseDataFromJSONTyped(json, false);
641
658
  }
@@ -650,13 +667,13 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
650
667
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
651
668
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
652
669
  featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
653
- featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
670
+ featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : MetricPeriodFromJSON(json["feature_usage_period"]),
654
671
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
655
672
  flag: json["flag"],
656
673
  flagId: json["flag_id"] == null ? void 0 : json["flag_id"],
657
674
  reason: json["reason"],
658
675
  ruleId: json["rule_id"] == null ? void 0 : json["rule_id"],
659
- ruleType: json["rule_type"] == null ? void 0 : json["rule_type"],
676
+ ruleType: json["rule_type"] == null ? void 0 : RuleTypeFromJSON(json["rule_type"]),
660
677
  userId: json["user_id"] == null ? void 0 : json["user_id"],
661
678
  value: json["value"]
662
679
  };
@@ -673,6 +690,268 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
673
690
  params: json["params"]
674
691
  };
675
692
  }
693
+ var BASE_PATH = "https://api.schematichq.com".replace(/\/+$/, "");
694
+ var Configuration = class {
695
+ constructor(configuration = {}) {
696
+ this.configuration = configuration;
697
+ }
698
+ configuration;
699
+ set config(configuration) {
700
+ this.configuration = configuration;
701
+ }
702
+ get basePath() {
703
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
704
+ }
705
+ get fetchApi() {
706
+ return this.configuration.fetchApi;
707
+ }
708
+ get middleware() {
709
+ return this.configuration.middleware || [];
710
+ }
711
+ get queryParamsStringify() {
712
+ return this.configuration.queryParamsStringify || querystring;
713
+ }
714
+ get username() {
715
+ return this.configuration.username;
716
+ }
717
+ get password() {
718
+ return this.configuration.password;
719
+ }
720
+ get apiKey() {
721
+ const apiKey = this.configuration.apiKey;
722
+ if (apiKey) {
723
+ return typeof apiKey === "function" ? apiKey : () => apiKey;
724
+ }
725
+ return void 0;
726
+ }
727
+ get accessToken() {
728
+ const accessToken = this.configuration.accessToken;
729
+ if (accessToken) {
730
+ return typeof accessToken === "function" ? accessToken : async () => accessToken;
731
+ }
732
+ return void 0;
733
+ }
734
+ get headers() {
735
+ return this.configuration.headers;
736
+ }
737
+ get credentials() {
738
+ return this.configuration.credentials;
739
+ }
740
+ };
741
+ var DefaultConfig = new Configuration();
742
+ var BaseAPI = class _BaseAPI {
743
+ constructor(configuration = DefaultConfig) {
744
+ this.configuration = configuration;
745
+ this.middleware = configuration.middleware;
746
+ }
747
+ configuration;
748
+ static jsonRegex = new RegExp(
749
+ "^(:?application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(:?;.*)?$",
750
+ "i"
751
+ );
752
+ middleware;
753
+ withMiddleware(...middlewares) {
754
+ const next = this.clone();
755
+ next.middleware = next.middleware.concat(...middlewares);
756
+ return next;
757
+ }
758
+ withPreMiddleware(...preMiddlewares) {
759
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
760
+ return this.withMiddleware(...middlewares);
761
+ }
762
+ withPostMiddleware(...postMiddlewares) {
763
+ const middlewares = postMiddlewares.map((post) => ({ post }));
764
+ return this.withMiddleware(...middlewares);
765
+ }
766
+ /**
767
+ * Check if the given MIME is a JSON MIME.
768
+ * JSON MIME examples:
769
+ * application/json
770
+ * application/json; charset=UTF8
771
+ * APPLICATION/JSON
772
+ * application/vnd.company+json
773
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
774
+ * @return True if the given MIME is JSON, false otherwise.
775
+ */
776
+ isJsonMime(mime) {
777
+ if (!mime) {
778
+ return false;
779
+ }
780
+ return _BaseAPI.jsonRegex.test(mime);
781
+ }
782
+ async request(context, initOverrides) {
783
+ const { url, init } = await this.createFetchParams(context, initOverrides);
784
+ const response = await this.fetchApi(url, init);
785
+ if (response && response.status >= 200 && response.status < 300) {
786
+ return response;
787
+ }
788
+ throw new ResponseError(response, "Response returned an error code");
789
+ }
790
+ async createFetchParams(context, initOverrides) {
791
+ let url = this.configuration.basePath + context.path;
792
+ if (context.query !== void 0 && Object.keys(context.query).length !== 0) {
793
+ url += "?" + this.configuration.queryParamsStringify(context.query);
794
+ }
795
+ const headers = Object.assign(
796
+ {},
797
+ this.configuration.headers,
798
+ context.headers
799
+ );
800
+ Object.keys(headers).forEach(
801
+ (key) => headers[key] === void 0 ? delete headers[key] : {}
802
+ );
803
+ const initOverrideFn = typeof initOverrides === "function" ? initOverrides : async () => initOverrides;
804
+ const initParams = {
805
+ method: context.method,
806
+ headers,
807
+ body: context.body,
808
+ credentials: this.configuration.credentials
809
+ };
810
+ const overriddenInit = {
811
+ ...initParams,
812
+ ...await initOverrideFn({
813
+ init: initParams,
814
+ context
815
+ })
816
+ };
817
+ let body;
818
+ if (isFormData(overriddenInit.body) || overriddenInit.body instanceof URLSearchParams || isBlob(overriddenInit.body)) {
819
+ body = overriddenInit.body;
820
+ } else if (this.isJsonMime(headers["Content-Type"])) {
821
+ body = JSON.stringify(overriddenInit.body);
822
+ } else {
823
+ body = overriddenInit.body;
824
+ }
825
+ const init = {
826
+ ...overriddenInit,
827
+ body
828
+ };
829
+ return { url, init };
830
+ }
831
+ fetchApi = async (url, init) => {
832
+ let fetchParams = { url, init };
833
+ for (const middleware of this.middleware) {
834
+ if (middleware.pre) {
835
+ fetchParams = await middleware.pre({
836
+ fetch: this.fetchApi,
837
+ ...fetchParams
838
+ }) || fetchParams;
839
+ }
840
+ }
841
+ let response = void 0;
842
+ try {
843
+ response = await (this.configuration.fetchApi || fetch)(
844
+ fetchParams.url,
845
+ fetchParams.init
846
+ );
847
+ } catch (e) {
848
+ for (const middleware of this.middleware) {
849
+ if (middleware.onError) {
850
+ response = await middleware.onError({
851
+ fetch: this.fetchApi,
852
+ url: fetchParams.url,
853
+ init: fetchParams.init,
854
+ error: e,
855
+ response: response ? response.clone() : void 0
856
+ }) || response;
857
+ }
858
+ }
859
+ if (response === void 0) {
860
+ if (e instanceof Error) {
861
+ throw new FetchError(
862
+ e,
863
+ "The request failed and the interceptors did not return an alternative response"
864
+ );
865
+ } else {
866
+ throw e;
867
+ }
868
+ }
869
+ }
870
+ for (const middleware of this.middleware) {
871
+ if (middleware.post) {
872
+ response = await middleware.post({
873
+ fetch: this.fetchApi,
874
+ url: fetchParams.url,
875
+ init: fetchParams.init,
876
+ response: response.clone()
877
+ }) || response;
878
+ }
879
+ }
880
+ return response;
881
+ };
882
+ /**
883
+ * Create a shallow clone of `this` by constructing a new instance
884
+ * and then shallow cloning data members.
885
+ */
886
+ clone() {
887
+ const constructor = this.constructor;
888
+ const next = new constructor(this.configuration);
889
+ next.middleware = this.middleware.slice();
890
+ return next;
891
+ }
892
+ };
893
+ function isBlob(value) {
894
+ return typeof Blob !== "undefined" && value instanceof Blob;
895
+ }
896
+ function isFormData(value) {
897
+ return typeof FormData !== "undefined" && value instanceof FormData;
898
+ }
899
+ var ResponseError = class extends Error {
900
+ constructor(response, msg) {
901
+ super(msg);
902
+ this.response = response;
903
+ }
904
+ response;
905
+ name = "ResponseError";
906
+ };
907
+ var FetchError = class extends Error {
908
+ constructor(cause, msg) {
909
+ super(msg);
910
+ this.cause = cause;
911
+ }
912
+ cause;
913
+ name = "FetchError";
914
+ };
915
+ function querystring(params, prefix = "") {
916
+ return Object.keys(params).map((key) => querystringSingleKey(key, params[key], prefix)).filter((part) => part.length > 0).join("&");
917
+ }
918
+ function querystringSingleKey(key, value, keyPrefix = "") {
919
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
920
+ if (value instanceof Array) {
921
+ const multiValue = value.map((singleValue) => encodeURIComponent(String(singleValue))).join(`&${encodeURIComponent(fullKey)}=`);
922
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
923
+ }
924
+ if (value instanceof Set) {
925
+ const valueAsArray = Array.from(value);
926
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
927
+ }
928
+ if (value instanceof Date) {
929
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
930
+ }
931
+ if (value instanceof Object) {
932
+ return querystring(value, fullKey);
933
+ }
934
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
935
+ }
936
+ function mapValues(data, fn) {
937
+ return Object.keys(data).reduce(
938
+ (acc, key) => ({ ...acc, [key]: fn(data[key]) }),
939
+ {}
940
+ );
941
+ }
942
+ function CompanyCreditBalanceFromJSON(json) {
943
+ return CompanyCreditBalanceFromJSONTyped(json, false);
944
+ }
945
+ function CompanyCreditBalanceFromJSONTyped(json, ignoreDiscriminator) {
946
+ if (json == null) {
947
+ return json;
948
+ }
949
+ return {
950
+ remaining: json["remaining"],
951
+ reserved: json["reserved"],
952
+ settled: json["settled"]
953
+ };
954
+ }
676
955
  var TrialStatus = {
677
956
  Active: "active",
678
957
  Converted: "converted",
@@ -706,6 +985,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
706
985
  return json;
707
986
  }
708
987
  return {
988
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
709
989
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
710
990
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
711
991
  };
@@ -788,7 +1068,10 @@ var CheckFlagReturnFromJSON = (json) => {
788
1068
  return {
789
1069
  featureUsageExceeded,
790
1070
  companyId: companyId == null ? void 0 : companyId,
1071
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
791
1072
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1073
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1074
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
792
1075
  error: error == null ? void 0 : error,
793
1076
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
794
1077
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -814,6 +1097,22 @@ var CheckPlanReturnFromJSON = (json) => {
814
1097
  trialStatus: trialStatus == null ? void 0 : trialStatus
815
1098
  };
816
1099
  };
1100
+ var CreditBalancesFromJSON = (json) => {
1101
+ if (json == null || typeof json !== "object") {
1102
+ return {};
1103
+ }
1104
+ const balances = {};
1105
+ for (const [creditId, raw] of Object.entries(json)) {
1106
+ if (raw == null || typeof raw !== "object") continue;
1107
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1108
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1109
+ continue;
1110
+ }
1111
+ balances[creditId] = { remaining, reserved, settled };
1112
+ }
1113
+ return balances;
1114
+ };
1115
+ var cacheVersion = "bb56e75d";
817
1116
  function contextString(context) {
818
1117
  const sortedContext = Object.keys(context).reduce((acc, key) => {
819
1118
  const sortedKeys = Object.keys(
@@ -828,8 +1127,12 @@ function contextString(context) {
828
1127
  }, {});
829
1128
  return JSON.stringify(sortedContext);
830
1129
  }
831
- var version = "1.4.0";
1130
+ var version = "1.5.0";
832
1131
  var anonymousIdKey = "schematicId";
1132
+ var flagStateCachePrefix = "schematicCache";
1133
+ var flagStateCacheMaxContexts = 10;
1134
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1135
+ var emptyCreditBalances = Object.freeze({});
833
1136
  var Schematic = class {
834
1137
  additionalHeaders = {};
835
1138
  apiKey;
@@ -846,11 +1149,17 @@ var Schematic = class {
846
1149
  isPending = true;
847
1150
  isPendingListeners = /* @__PURE__ */ new Set();
848
1151
  planListeners = /* @__PURE__ */ new Set();
1152
+ creditBalanceListeners = /* @__PURE__ */ new Set();
849
1153
  storage;
1154
+ persistFlagState = true;
1155
+ flagStateCacheKey;
1156
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1157
+ cachedFlagState = null;
850
1158
  useWebSocket = false;
851
1159
  checks = {};
852
1160
  featureUsageEventMap = {};
853
1161
  planChecks = {};
1162
+ creditBalances = {};
854
1163
  webSocketUrl = "wss://api.schematichq.com";
855
1164
  webSocketConnectionTimeout = 1e4;
856
1165
  webSocketReconnect = true;
@@ -879,11 +1188,16 @@ var Schematic = class {
879
1188
  fallbackCheckCache = {};
880
1189
  constructor(apiKey, options) {
881
1190
  this.apiKey = apiKey;
1191
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
882
1192
  this.eventQueue = [];
883
1193
  this.contextDependentEventQueue = [];
884
1194
  this.useWebSocket = options?.useWebSocket ?? false;
885
1195
  this.debugEnabled = options?.debug ?? false;
886
1196
  this.offlineEnabled = options?.offline ?? false;
1197
+ this.persistFlagState = options?.persistFlagState ?? true;
1198
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1199
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1200
+ }
887
1201
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
888
1202
  const params = new URLSearchParams(window.location.search);
889
1203
  const debugParam = params.get("schematic_debug");
@@ -916,6 +1230,7 @@ var Schematic = class {
916
1230
  } catch {
917
1231
  }
918
1232
  }
1233
+ this.hydrateFlagStateFromCache();
919
1234
  if (options?.apiUrl !== void 0) {
920
1235
  this.apiUrl = options.apiUrl;
921
1236
  }
@@ -1209,6 +1524,29 @@ var Schematic = class {
1209
1524
  });
1210
1525
  this.notifyPlanListeners(plan);
1211
1526
  }
1527
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1528
+ const contextStr = contextString(context);
1529
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1530
+ const previous = this.creditBalances[contextStr] ?? {};
1531
+ let changed = false;
1532
+ const merged = { ...previous };
1533
+ for (const [creditId, next] of Object.entries(updates)) {
1534
+ const current = previous[creditId];
1535
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1536
+ merged[creditId] = next;
1537
+ changed = true;
1538
+ }
1539
+ }
1540
+ if (changed) {
1541
+ this.creditBalances[contextStr] = merged;
1542
+ this.debug(
1543
+ `WebSocket credit balance update received. Notifying listeners`,
1544
+ { creditBalances: merged }
1545
+ );
1546
+ this.notifyCreditBalanceListeners(merged);
1547
+ }
1548
+ }
1549
+ this.persistContextToCache(contextString(context));
1212
1550
  this.flushContextDependentEventQueue();
1213
1551
  this.setIsPending(false);
1214
1552
  };
@@ -1316,7 +1654,32 @@ var Schematic = class {
1316
1654
  return;
1317
1655
  }
1318
1656
  try {
1319
- this.setIsPending(true);
1657
+ const newContextStr = contextString(context);
1658
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1659
+ if (cacheHit) {
1660
+ this.debug(
1661
+ `setContext: cache hit for context, pre-resolving from cached values`
1662
+ );
1663
+ this.context = context;
1664
+ this.flushContextDependentEventQueue();
1665
+ this.setIsPending(false);
1666
+ const cachedChecks = this.checks[newContextStr] ?? {};
1667
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1668
+ if (check === void 0) continue;
1669
+ this.notifyFlagCheckListeners(flagKey, check);
1670
+ this.notifyFlagValueListeners(flagKey, check.value);
1671
+ }
1672
+ const cachedPlan = this.planChecks[newContextStr];
1673
+ if (cachedPlan !== void 0) {
1674
+ this.notifyPlanListeners(cachedPlan);
1675
+ }
1676
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1677
+ if (cachedCreditBalances !== void 0) {
1678
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1679
+ }
1680
+ } else {
1681
+ this.setIsPending(true);
1682
+ }
1320
1683
  if (!this.conn) {
1321
1684
  if (this.isConnecting) {
1322
1685
  this.debug(
@@ -1327,7 +1690,7 @@ var Schematic = class {
1327
1690
  }
1328
1691
  if (this.conn !== null) {
1329
1692
  const socket2 = await this.conn;
1330
- await this.wsSendMessage(socket2, context);
1693
+ await this.wsSendMessage(socket2, context, cacheHit);
1331
1694
  return;
1332
1695
  }
1333
1696
  }
@@ -1343,7 +1706,7 @@ var Schematic = class {
1343
1706
  this.conn = this.wsConnect();
1344
1707
  const socket2 = await this.conn;
1345
1708
  this.isConnecting = false;
1346
- await this.wsSendMessage(socket2, context);
1709
+ await this.wsSendMessage(socket2, context, cacheHit);
1347
1710
  return;
1348
1711
  } catch (error) {
1349
1712
  this.isConnecting = false;
@@ -1351,7 +1714,7 @@ var Schematic = class {
1351
1714
  }
1352
1715
  }
1353
1716
  const socket = await this.conn;
1354
- await this.wsSendMessage(socket, context);
1717
+ await this.wsSendMessage(socket, context, cacheHit);
1355
1718
  } catch (error) {
1356
1719
  console.warn("Failed to establish WebSocket connection:", error);
1357
1720
  this.context = context;
@@ -1411,10 +1774,12 @@ var Schematic = class {
1411
1774
  `Optimistically updating feature usage for event: ${eventName}`,
1412
1775
  { quantity }
1413
1776
  );
1777
+ let dirty = false;
1414
1778
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1415
1779
  if (check === void 0) return;
1416
1780
  const updatedCheck = { ...check };
1417
1781
  if (typeof updatedCheck.featureUsage === "number") {
1782
+ dirty = true;
1418
1783
  updatedCheck.featureUsage += quantity;
1419
1784
  if (typeof updatedCheck.featureAllocation === "number") {
1420
1785
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1444,6 +1809,9 @@ var Schematic = class {
1444
1809
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1445
1810
  }
1446
1811
  });
1812
+ if (dirty) {
1813
+ this.persistContextToCache(contextString(this.context));
1814
+ }
1447
1815
  };
1448
1816
  /**
1449
1817
  * Event processing
@@ -1546,6 +1914,206 @@ var Schematic = class {
1546
1914
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1547
1915
  return generatedAnonymousId;
1548
1916
  };
1917
+ hasCachedValuesForContext = (contextStr) => {
1918
+ const cachedChecks = this.checks[contextStr];
1919
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1920
+ return true;
1921
+ }
1922
+ if (this.planChecks[contextStr] !== void 0) {
1923
+ return true;
1924
+ }
1925
+ const cachedCreditBalances = this.creditBalances[contextStr];
1926
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1927
+ };
1928
+ readFlagStateCache = () => {
1929
+ if (!this.persistFlagState || this.storage === void 0) {
1930
+ return null;
1931
+ }
1932
+ let raw;
1933
+ try {
1934
+ raw = this.storage.getItem(this.flagStateCacheKey);
1935
+ } catch (error) {
1936
+ this.debug("Failed to read flag state cache:", error);
1937
+ return null;
1938
+ }
1939
+ if (raw === null || raw === void 0) {
1940
+ return null;
1941
+ }
1942
+ let parsed;
1943
+ try {
1944
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1945
+ } catch (error) {
1946
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
1947
+ return null;
1948
+ }
1949
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
1950
+ this.debug(
1951
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
1952
+ {
1953
+ parsedType: typeof parsed,
1954
+ version: parsed?.version
1955
+ }
1956
+ );
1957
+ return null;
1958
+ }
1959
+ return parsed;
1960
+ };
1961
+ reviveCachedCheck = (raw) => {
1962
+ if (raw === null || typeof raw !== "object") return null;
1963
+ const obj = raw;
1964
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
1965
+ return null;
1966
+ }
1967
+ const revived = { ...obj };
1968
+ if (typeof revived.featureUsageResetAt === "string") {
1969
+ const d = new Date(revived.featureUsageResetAt);
1970
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
1971
+ }
1972
+ return revived;
1973
+ };
1974
+ reviveCachedPlan = (raw) => {
1975
+ if (raw === null || typeof raw !== "object") return null;
1976
+ const obj = raw;
1977
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
1978
+ return null;
1979
+ }
1980
+ const revived = { ...obj };
1981
+ if (typeof revived.trialEndDate === "string") {
1982
+ const d = new Date(revived.trialEndDate);
1983
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
1984
+ }
1985
+ return revived;
1986
+ };
1987
+ reviveCachedCreditBalances = (raw) => {
1988
+ if (raw === null || typeof raw !== "object") return null;
1989
+ const revived = {};
1990
+ for (const [creditId, value] of Object.entries(
1991
+ raw
1992
+ )) {
1993
+ if (value === null || typeof value !== "object") continue;
1994
+ const obj = value;
1995
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
1996
+ continue;
1997
+ }
1998
+ revived[creditId] = {
1999
+ remaining: obj.remaining,
2000
+ reserved: obj.reserved,
2001
+ settled: obj.settled
2002
+ };
2003
+ }
2004
+ return Object.keys(revived).length > 0 ? revived : null;
2005
+ };
2006
+ hydrateFlagStateFromCache = () => {
2007
+ const parsed = this.readFlagStateCache();
2008
+ if (parsed === null) return;
2009
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2010
+ const fresh = {};
2011
+ let contextsLoaded = 0;
2012
+ let contextsExpired = 0;
2013
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2014
+ if (entry === null || typeof entry !== "object") continue;
2015
+ if (typeof entry.updatedAt !== "number") continue;
2016
+ if (entry.updatedAt < cutoff) {
2017
+ contextsExpired += 1;
2018
+ continue;
2019
+ }
2020
+ const revivedChecks = {};
2021
+ if (entry.checks !== null && typeof entry.checks === "object") {
2022
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2023
+ const revived = this.reviveCachedCheck(rawCheck);
2024
+ if (revived === null || revived.flag !== flagKey) continue;
2025
+ revivedChecks[flagKey] = revived;
2026
+ if (typeof revived.featureUsageEvent === "string") {
2027
+ this.updateFeatureUsageEventMap(revived);
2028
+ }
2029
+ }
2030
+ if (Object.keys(revivedChecks).length > 0) {
2031
+ this.checks[contextStr] = revivedChecks;
2032
+ }
2033
+ }
2034
+ let revivedPlan;
2035
+ if (entry.plan !== void 0 && entry.plan !== null) {
2036
+ const p = this.reviveCachedPlan(entry.plan);
2037
+ if (p !== null) {
2038
+ revivedPlan = p;
2039
+ this.planChecks[contextStr] = p;
2040
+ }
2041
+ }
2042
+ let revivedCreditBalances;
2043
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2044
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2045
+ if (balances !== null) {
2046
+ revivedCreditBalances = balances;
2047
+ this.creditBalances[contextStr] = balances;
2048
+ }
2049
+ }
2050
+ fresh[contextStr] = {
2051
+ checks: revivedChecks,
2052
+ plan: revivedPlan,
2053
+ creditBalances: revivedCreditBalances,
2054
+ updatedAt: entry.updatedAt
2055
+ };
2056
+ contextsLoaded += 1;
2057
+ }
2058
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2059
+ this.debug(
2060
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2061
+ );
2062
+ };
2063
+ persistContextToCache = (contextStr) => {
2064
+ const checksForContext = this.checks[contextStr];
2065
+ const planForContext = this.planChecks[contextStr];
2066
+ const creditBalancesForContext = this.creditBalances[contextStr];
2067
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2068
+ return;
2069
+ }
2070
+ if (this.cachedFlagState === null) {
2071
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2072
+ version: cacheVersion,
2073
+ contexts: {}
2074
+ };
2075
+ }
2076
+ const cleanChecks = {};
2077
+ if (checksForContext !== void 0) {
2078
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2079
+ if (check !== void 0) cleanChecks[flagKey] = check;
2080
+ }
2081
+ }
2082
+ this.cachedFlagState.contexts[contextStr] = {
2083
+ checks: cleanChecks,
2084
+ plan: planForContext,
2085
+ creditBalances: creditBalancesForContext,
2086
+ updatedAt: Date.now()
2087
+ };
2088
+ const entries = Object.entries(this.cachedFlagState.contexts);
2089
+ if (entries.length > flagStateCacheMaxContexts) {
2090
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2091
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2092
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2093
+ for (const key of Object.keys(this.checks)) {
2094
+ if (!survivorKeys.has(key)) delete this.checks[key];
2095
+ }
2096
+ for (const key of Object.keys(this.planChecks)) {
2097
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2098
+ }
2099
+ for (const key of Object.keys(this.creditBalances)) {
2100
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2101
+ }
2102
+ this.cachedFlagState = {
2103
+ version: cacheVersion,
2104
+ contexts: Object.fromEntries(survivors)
2105
+ };
2106
+ }
2107
+ if (!this.persistFlagState || this.storage === void 0) return;
2108
+ try {
2109
+ this.storage.setItem(
2110
+ this.flagStateCacheKey,
2111
+ JSON.stringify(this.cachedFlagState)
2112
+ );
2113
+ } catch (error) {
2114
+ this.debug("Failed to write flag state cache:", error);
2115
+ }
2116
+ };
1549
2117
  handleEvent = (eventType, eventBody) => {
1550
2118
  const event = {
1551
2119
  api_key: this.apiKey,
@@ -1806,6 +2374,9 @@ var Schematic = class {
1806
2374
  if (this.conn !== null) {
1807
2375
  try {
1808
2376
  const socket = await this.conn;
2377
+ if (this.currentWebSocket === socket) {
2378
+ this.currentWebSocket = null;
2379
+ }
1809
2380
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1810
2381
  socket.close();
1811
2382
  }
@@ -1971,6 +2542,7 @@ var Schematic = class {
1971
2542
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1972
2543
  let timeoutId = null;
1973
2544
  let isResolved = false;
2545
+ let didOpen = false;
1974
2546
  timeoutId = setTimeout(() => {
1975
2547
  if (!isResolved) {
1976
2548
  isResolved = true;
@@ -1984,6 +2556,7 @@ var Schematic = class {
1984
2556
  webSocket.onopen = () => {
1985
2557
  if (isResolved) return;
1986
2558
  isResolved = true;
2559
+ didOpen = true;
1987
2560
  if (timeoutId !== null) {
1988
2561
  clearTimeout(timeoutId);
1989
2562
  }
@@ -2009,11 +2582,12 @@ var Schematic = class {
2009
2582
  }
2010
2583
  this.debug(`WebSocket connection ${connectionId} closed`);
2011
2584
  this.conn = null;
2585
+ const wasCurrent = this.currentWebSocket === webSocket;
2012
2586
  if (this.currentWebSocket === webSocket) {
2013
2587
  this.currentWebSocket = null;
2014
2588
  this.isConnecting = false;
2015
2589
  }
2016
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2590
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2017
2591
  this.attemptReconnect();
2018
2592
  }
2019
2593
  };
@@ -2109,6 +2683,16 @@ var Schematic = class {
2109
2683
  const plan = this.planChecks[contextStr];
2110
2684
  return plan;
2111
2685
  };
2686
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2687
+ getCreditBalances = () => {
2688
+ const contextStr = contextString(this.context);
2689
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2690
+ };
2691
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2692
+ getCreditBalance = (creditId) => {
2693
+ const contextStr = contextString(this.context);
2694
+ return this.creditBalances[contextStr]?.[creditId];
2695
+ };
2112
2696
  // flag checks state
2113
2697
  getFlagCheck = (flagKey) => {
2114
2698
  const contextStr = contextString(this.context);
@@ -2168,6 +2752,13 @@ var Schematic = class {
2168
2752
  this.planListeners.delete(listener);
2169
2753
  };
2170
2754
  };
2755
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2756
+ addCreditBalanceListener = (listener) => {
2757
+ this.creditBalanceListeners.add(listener);
2758
+ return () => {
2759
+ this.creditBalanceListeners.delete(listener);
2760
+ };
2761
+ };
2171
2762
  notifyFlagCheckListeners = (flagKey, check) => {
2172
2763
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2173
2764
  if (listeners.size > 0) {
@@ -2231,6 +2822,17 @@ var Schematic = class {
2231
2822
  });
2232
2823
  });
2233
2824
  };
2825
+ notifyCreditBalanceListeners = (value) => {
2826
+ const listeners = this.creditBalanceListeners ?? [];
2827
+ if (listeners.size > 0) {
2828
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2829
+ value
2830
+ });
2831
+ }
2832
+ listeners.forEach(
2833
+ (listener) => notifyCreditBalanceListener(listener, value)
2834
+ );
2835
+ };
2234
2836
  };
2235
2837
  var notifyPendingListener = (listener, value) => {
2236
2838
  if (listener.length > 0) {
@@ -2260,12 +2862,19 @@ var notifyPlanListener = (listener, value) => {
2260
2862
  listener();
2261
2863
  }
2262
2864
  };
2865
+ var notifyCreditBalanceListener = (listener, value) => {
2866
+ if (listener.length > 0) {
2867
+ listener(value);
2868
+ } else {
2869
+ listener();
2870
+ }
2871
+ };
2263
2872
 
2264
2873
  // src/context/schematic.tsx
2265
2874
  import React, { createContext, useEffect, useMemo, useRef } from "react";
2266
2875
 
2267
2876
  // src/version.ts
2268
- var version2 = "1.4.0";
2877
+ var version2 = "1.5.0";
2269
2878
 
2270
2879
  // src/context/schematic.tsx
2271
2880
  import { jsx } from "react/jsx-runtime";
@@ -2408,6 +3017,34 @@ var useSchematicPlan = (opts) => {
2408
3017
  }, [client, fallbackPlan]);
2409
3018
  return useSyncExternalStore(subscribe, getSnapshot, () => fallbackPlan);
2410
3019
  };
3020
+ var useSchematicCreditBalance = (creditId, opts) => {
3021
+ const client = useSchematicClient(opts);
3022
+ const subscribe = useCallback(
3023
+ (callback) => client.addCreditBalanceListener(callback),
3024
+ [client]
3025
+ );
3026
+ const getSnapshot = useCallback(
3027
+ () => client.getCreditBalance(creditId),
3028
+ [client, creditId]
3029
+ );
3030
+ const balance = useSyncExternalStore(subscribe, getSnapshot, () => void 0);
3031
+ const isPendingSubscribe = useCallback(
3032
+ (callback) => client.addIsPendingListener(callback),
3033
+ [client]
3034
+ );
3035
+ const isPending = useSyncExternalStore(
3036
+ isPendingSubscribe,
3037
+ () => client.getIsPending(),
3038
+ () => true
3039
+ );
3040
+ return useMemo2(
3041
+ () => ({
3042
+ balance: balance?.settled ?? 0,
3043
+ isLoading: balance === void 0 && isPending
3044
+ }),
3045
+ [balance, isPending]
3046
+ );
3047
+ };
2411
3048
  var useSchematicIsPending = (opts) => {
2412
3049
  const client = useSchematicClient(opts);
2413
3050
  const subscribe = useCallback(
@@ -2425,6 +3062,7 @@ export {
2425
3062
  UsagePeriod,
2426
3063
  useSchematic,
2427
3064
  useSchematicContext,
3065
+ useSchematicCreditBalance,
2428
3066
  useSchematicEntitlement,
2429
3067
  useSchematicEvents,
2430
3068
  useSchematicFlag,