@schematichq/schematic-vue 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.
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  useSchematic: () => useSchematic,
28
28
  useSchematicClient: () => useSchematicClient,
29
29
  useSchematicContext: () => useSchematicContext,
30
+ useSchematicCreditBalance: () => useSchematicCreditBalance,
30
31
  useSchematicEntitlement: () => useSchematicEntitlement,
31
32
  useSchematicEvents: () => useSchematicEvents,
32
33
  useSchematicFlag: () => useSchematicFlag,
@@ -43,7 +44,11 @@ var __getOwnPropNames2 = Object.getOwnPropertyNames;
43
44
  var __getProtoOf = Object.getPrototypeOf;
44
45
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
45
46
  var __commonJS = (cb, mod) => function __require() {
46
- return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
47
+ try {
48
+ return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
49
+ } catch (e) {
50
+ throw mod = 0, e;
51
+ }
47
52
  };
48
53
  var __copyProps2 = (to, from, except, desc) => {
49
54
  if (from && typeof from === "object" || typeof from === "function") {
@@ -602,19 +607,16 @@ for (let i = 0; i < 256; ++i) {
602
607
  function unsafeStringify(arr, offset = 0) {
603
608
  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();
604
609
  }
605
- var getRandomValues;
606
610
  var rnds8 = new Uint8Array(16);
607
611
  function rng() {
608
- if (!getRandomValues) {
609
- if (typeof crypto === "undefined" || !crypto.getRandomValues) {
610
- throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
611
- }
612
- getRandomValues = crypto.getRandomValues.bind(crypto);
612
+ return crypto.getRandomValues(rnds8);
613
+ }
614
+ function v4(options, buf, offset) {
615
+ if (!buf && !options && crypto.randomUUID) {
616
+ return crypto.randomUUID();
613
617
  }
614
- return getRandomValues(rnds8);
618
+ return _v4(options, buf, offset);
615
619
  }
616
- var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
617
- var native_default = { randomUUID };
618
620
  function _v4(options, buf, offset) {
619
621
  options = options || {};
620
622
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -635,12 +637,6 @@ function _v4(options, buf, offset) {
635
637
  }
636
638
  return unsafeStringify(rnds);
637
639
  }
638
- function v4(options, buf, offset) {
639
- if (native_default.randomUUID && !buf && !options) {
640
- return native_default.randomUUID();
641
- }
642
- return _v4(options, buf, offset);
643
- }
644
640
  var v4_default = v4;
645
641
  var import_polyfill = __toESM(require_browser_polyfill());
646
642
  function EntitlementValueTypeFromJSON(json) {
@@ -649,6 +645,18 @@ function EntitlementValueTypeFromJSON(json) {
649
645
  function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
650
646
  return json;
651
647
  }
648
+ function MetricPeriodMonthResetFromJSON(json) {
649
+ return MetricPeriodMonthResetFromJSONTyped(json, false);
650
+ }
651
+ function MetricPeriodMonthResetFromJSONTyped(json, ignoreDiscriminator) {
652
+ return json;
653
+ }
654
+ function MetricPeriodFromJSON(json) {
655
+ return MetricPeriodFromJSONTyped(json, false);
656
+ }
657
+ function MetricPeriodFromJSONTyped(json, ignoreDiscriminator) {
658
+ return json;
659
+ }
652
660
  function FeatureEntitlementFromJSON(json) {
653
661
  return FeatureEntitlementFromJSONTyped(json, false);
654
662
  }
@@ -658,21 +666,31 @@ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
658
666
  }
659
667
  return {
660
668
  allocation: json["allocation"] == null ? void 0 : json["allocation"],
669
+ consumptionRate: json["consumption_rate"] == null ? void 0 : json["consumption_rate"],
661
670
  creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
662
671
  creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
672
+ creditReserved: json["credit_reserved"] == null ? void 0 : json["credit_reserved"],
673
+ creditSettled: json["credit_settled"] == null ? void 0 : json["credit_settled"],
663
674
  creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
664
675
  creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
665
676
  eventName: json["event_name"] == null ? void 0 : json["event_name"],
677
+ eventSubtype: json["event_subtype"] == null ? void 0 : json["event_subtype"],
666
678
  featureId: json["feature_id"],
667
679
  featureKey: json["feature_key"],
668
- metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
680
+ metricPeriod: json["metric_period"] == null ? void 0 : MetricPeriodFromJSON(json["metric_period"]),
669
681
  metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
670
- monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
682
+ monthReset: json["month_reset"] == null ? void 0 : MetricPeriodMonthResetFromJSON(json["month_reset"]),
671
683
  softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
672
684
  usage: json["usage"] == null ? void 0 : json["usage"],
673
685
  valueType: EntitlementValueTypeFromJSON(json["value_type"])
674
686
  };
675
687
  }
688
+ function RuleTypeFromJSON(json) {
689
+ return RuleTypeFromJSONTyped(json, false);
690
+ }
691
+ function RuleTypeFromJSONTyped(json, ignoreDiscriminator) {
692
+ return json;
693
+ }
676
694
  function CheckFlagResponseDataFromJSON(json) {
677
695
  return CheckFlagResponseDataFromJSONTyped(json, false);
678
696
  }
@@ -687,13 +705,13 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
687
705
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
688
706
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
689
707
  featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
690
- featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
708
+ featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : MetricPeriodFromJSON(json["feature_usage_period"]),
691
709
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
692
710
  flag: json["flag"],
693
711
  flagId: json["flag_id"] == null ? void 0 : json["flag_id"],
694
712
  reason: json["reason"],
695
713
  ruleId: json["rule_id"] == null ? void 0 : json["rule_id"],
696
- ruleType: json["rule_type"] == null ? void 0 : json["rule_type"],
714
+ ruleType: json["rule_type"] == null ? void 0 : RuleTypeFromJSON(json["rule_type"]),
697
715
  userId: json["user_id"] == null ? void 0 : json["user_id"],
698
716
  value: json["value"]
699
717
  };
@@ -710,6 +728,268 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
710
728
  params: json["params"]
711
729
  };
712
730
  }
731
+ var BASE_PATH = "https://api.schematichq.com".replace(/\/+$/, "");
732
+ var Configuration = class {
733
+ constructor(configuration = {}) {
734
+ this.configuration = configuration;
735
+ }
736
+ configuration;
737
+ set config(configuration) {
738
+ this.configuration = configuration;
739
+ }
740
+ get basePath() {
741
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
742
+ }
743
+ get fetchApi() {
744
+ return this.configuration.fetchApi;
745
+ }
746
+ get middleware() {
747
+ return this.configuration.middleware || [];
748
+ }
749
+ get queryParamsStringify() {
750
+ return this.configuration.queryParamsStringify || querystring;
751
+ }
752
+ get username() {
753
+ return this.configuration.username;
754
+ }
755
+ get password() {
756
+ return this.configuration.password;
757
+ }
758
+ get apiKey() {
759
+ const apiKey = this.configuration.apiKey;
760
+ if (apiKey) {
761
+ return typeof apiKey === "function" ? apiKey : () => apiKey;
762
+ }
763
+ return void 0;
764
+ }
765
+ get accessToken() {
766
+ const accessToken = this.configuration.accessToken;
767
+ if (accessToken) {
768
+ return typeof accessToken === "function" ? accessToken : async () => accessToken;
769
+ }
770
+ return void 0;
771
+ }
772
+ get headers() {
773
+ return this.configuration.headers;
774
+ }
775
+ get credentials() {
776
+ return this.configuration.credentials;
777
+ }
778
+ };
779
+ var DefaultConfig = new Configuration();
780
+ var BaseAPI = class _BaseAPI {
781
+ constructor(configuration = DefaultConfig) {
782
+ this.configuration = configuration;
783
+ this.middleware = configuration.middleware;
784
+ }
785
+ configuration;
786
+ static jsonRegex = new RegExp(
787
+ "^(:?application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(:?;.*)?$",
788
+ "i"
789
+ );
790
+ middleware;
791
+ withMiddleware(...middlewares) {
792
+ const next = this.clone();
793
+ next.middleware = next.middleware.concat(...middlewares);
794
+ return next;
795
+ }
796
+ withPreMiddleware(...preMiddlewares) {
797
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
798
+ return this.withMiddleware(...middlewares);
799
+ }
800
+ withPostMiddleware(...postMiddlewares) {
801
+ const middlewares = postMiddlewares.map((post) => ({ post }));
802
+ return this.withMiddleware(...middlewares);
803
+ }
804
+ /**
805
+ * Check if the given MIME is a JSON MIME.
806
+ * JSON MIME examples:
807
+ * application/json
808
+ * application/json; charset=UTF8
809
+ * APPLICATION/JSON
810
+ * application/vnd.company+json
811
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
812
+ * @return True if the given MIME is JSON, false otherwise.
813
+ */
814
+ isJsonMime(mime) {
815
+ if (!mime) {
816
+ return false;
817
+ }
818
+ return _BaseAPI.jsonRegex.test(mime);
819
+ }
820
+ async request(context, initOverrides) {
821
+ const { url, init } = await this.createFetchParams(context, initOverrides);
822
+ const response = await this.fetchApi(url, init);
823
+ if (response && response.status >= 200 && response.status < 300) {
824
+ return response;
825
+ }
826
+ throw new ResponseError(response, "Response returned an error code");
827
+ }
828
+ async createFetchParams(context, initOverrides) {
829
+ let url = this.configuration.basePath + context.path;
830
+ if (context.query !== void 0 && Object.keys(context.query).length !== 0) {
831
+ url += "?" + this.configuration.queryParamsStringify(context.query);
832
+ }
833
+ const headers = Object.assign(
834
+ {},
835
+ this.configuration.headers,
836
+ context.headers
837
+ );
838
+ Object.keys(headers).forEach(
839
+ (key) => headers[key] === void 0 ? delete headers[key] : {}
840
+ );
841
+ const initOverrideFn = typeof initOverrides === "function" ? initOverrides : async () => initOverrides;
842
+ const initParams = {
843
+ method: context.method,
844
+ headers,
845
+ body: context.body,
846
+ credentials: this.configuration.credentials
847
+ };
848
+ const overriddenInit = {
849
+ ...initParams,
850
+ ...await initOverrideFn({
851
+ init: initParams,
852
+ context
853
+ })
854
+ };
855
+ let body;
856
+ if (isFormData(overriddenInit.body) || overriddenInit.body instanceof URLSearchParams || isBlob(overriddenInit.body)) {
857
+ body = overriddenInit.body;
858
+ } else if (this.isJsonMime(headers["Content-Type"])) {
859
+ body = JSON.stringify(overriddenInit.body);
860
+ } else {
861
+ body = overriddenInit.body;
862
+ }
863
+ const init = {
864
+ ...overriddenInit,
865
+ body
866
+ };
867
+ return { url, init };
868
+ }
869
+ fetchApi = async (url, init) => {
870
+ let fetchParams = { url, init };
871
+ for (const middleware of this.middleware) {
872
+ if (middleware.pre) {
873
+ fetchParams = await middleware.pre({
874
+ fetch: this.fetchApi,
875
+ ...fetchParams
876
+ }) || fetchParams;
877
+ }
878
+ }
879
+ let response = void 0;
880
+ try {
881
+ response = await (this.configuration.fetchApi || fetch)(
882
+ fetchParams.url,
883
+ fetchParams.init
884
+ );
885
+ } catch (e) {
886
+ for (const middleware of this.middleware) {
887
+ if (middleware.onError) {
888
+ response = await middleware.onError({
889
+ fetch: this.fetchApi,
890
+ url: fetchParams.url,
891
+ init: fetchParams.init,
892
+ error: e,
893
+ response: response ? response.clone() : void 0
894
+ }) || response;
895
+ }
896
+ }
897
+ if (response === void 0) {
898
+ if (e instanceof Error) {
899
+ throw new FetchError(
900
+ e,
901
+ "The request failed and the interceptors did not return an alternative response"
902
+ );
903
+ } else {
904
+ throw e;
905
+ }
906
+ }
907
+ }
908
+ for (const middleware of this.middleware) {
909
+ if (middleware.post) {
910
+ response = await middleware.post({
911
+ fetch: this.fetchApi,
912
+ url: fetchParams.url,
913
+ init: fetchParams.init,
914
+ response: response.clone()
915
+ }) || response;
916
+ }
917
+ }
918
+ return response;
919
+ };
920
+ /**
921
+ * Create a shallow clone of `this` by constructing a new instance
922
+ * and then shallow cloning data members.
923
+ */
924
+ clone() {
925
+ const constructor = this.constructor;
926
+ const next = new constructor(this.configuration);
927
+ next.middleware = this.middleware.slice();
928
+ return next;
929
+ }
930
+ };
931
+ function isBlob(value) {
932
+ return typeof Blob !== "undefined" && value instanceof Blob;
933
+ }
934
+ function isFormData(value) {
935
+ return typeof FormData !== "undefined" && value instanceof FormData;
936
+ }
937
+ var ResponseError = class extends Error {
938
+ constructor(response, msg) {
939
+ super(msg);
940
+ this.response = response;
941
+ }
942
+ response;
943
+ name = "ResponseError";
944
+ };
945
+ var FetchError = class extends Error {
946
+ constructor(cause, msg) {
947
+ super(msg);
948
+ this.cause = cause;
949
+ }
950
+ cause;
951
+ name = "FetchError";
952
+ };
953
+ function querystring(params, prefix = "") {
954
+ return Object.keys(params).map((key) => querystringSingleKey(key, params[key], prefix)).filter((part) => part.length > 0).join("&");
955
+ }
956
+ function querystringSingleKey(key, value, keyPrefix = "") {
957
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
958
+ if (value instanceof Array) {
959
+ const multiValue = value.map((singleValue) => encodeURIComponent(String(singleValue))).join(`&${encodeURIComponent(fullKey)}=`);
960
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
961
+ }
962
+ if (value instanceof Set) {
963
+ const valueAsArray = Array.from(value);
964
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
965
+ }
966
+ if (value instanceof Date) {
967
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
968
+ }
969
+ if (value instanceof Object) {
970
+ return querystring(value, fullKey);
971
+ }
972
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
973
+ }
974
+ function mapValues(data, fn) {
975
+ return Object.keys(data).reduce(
976
+ (acc, key) => ({ ...acc, [key]: fn(data[key]) }),
977
+ {}
978
+ );
979
+ }
980
+ function CompanyCreditBalanceFromJSON(json) {
981
+ return CompanyCreditBalanceFromJSONTyped(json, false);
982
+ }
983
+ function CompanyCreditBalanceFromJSONTyped(json, ignoreDiscriminator) {
984
+ if (json == null) {
985
+ return json;
986
+ }
987
+ return {
988
+ remaining: json["remaining"],
989
+ reserved: json["reserved"],
990
+ settled: json["settled"]
991
+ };
992
+ }
713
993
  function TrialStatusFromJSON(json) {
714
994
  return TrialStatusFromJSONTyped(json, false);
715
995
  }
@@ -738,6 +1018,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
738
1018
  return json;
739
1019
  }
740
1020
  return {
1021
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
741
1022
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
742
1023
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
743
1024
  };
@@ -820,7 +1101,10 @@ var CheckFlagReturnFromJSON = (json) => {
820
1101
  return {
821
1102
  featureUsageExceeded,
822
1103
  companyId: companyId == null ? void 0 : companyId,
1104
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
823
1105
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1106
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1107
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
824
1108
  error: error == null ? void 0 : error,
825
1109
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
826
1110
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -846,6 +1130,22 @@ var CheckPlanReturnFromJSON = (json) => {
846
1130
  trialStatus: trialStatus == null ? void 0 : trialStatus
847
1131
  };
848
1132
  };
1133
+ var CreditBalancesFromJSON = (json) => {
1134
+ if (json == null || typeof json !== "object") {
1135
+ return {};
1136
+ }
1137
+ const balances = {};
1138
+ for (const [creditId, raw] of Object.entries(json)) {
1139
+ if (raw == null || typeof raw !== "object") continue;
1140
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1141
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1142
+ continue;
1143
+ }
1144
+ balances[creditId] = { remaining, reserved, settled };
1145
+ }
1146
+ return balances;
1147
+ };
1148
+ var cacheVersion = "bb56e75d";
849
1149
  function contextString(context) {
850
1150
  const sortedContext = Object.keys(context).reduce((acc, key) => {
851
1151
  const sortedKeys = Object.keys(
@@ -860,8 +1160,12 @@ function contextString(context) {
860
1160
  }, {});
861
1161
  return JSON.stringify(sortedContext);
862
1162
  }
863
- var version = "1.4.0";
1163
+ var version = "1.5.0";
864
1164
  var anonymousIdKey = "schematicId";
1165
+ var flagStateCachePrefix = "schematicCache";
1166
+ var flagStateCacheMaxContexts = 10;
1167
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1168
+ var emptyCreditBalances = Object.freeze({});
865
1169
  var Schematic = class {
866
1170
  additionalHeaders = {};
867
1171
  apiKey;
@@ -878,11 +1182,17 @@ var Schematic = class {
878
1182
  isPending = true;
879
1183
  isPendingListeners = /* @__PURE__ */ new Set();
880
1184
  planListeners = /* @__PURE__ */ new Set();
1185
+ creditBalanceListeners = /* @__PURE__ */ new Set();
881
1186
  storage;
1187
+ persistFlagState = true;
1188
+ flagStateCacheKey;
1189
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1190
+ cachedFlagState = null;
882
1191
  useWebSocket = false;
883
1192
  checks = {};
884
1193
  featureUsageEventMap = {};
885
1194
  planChecks = {};
1195
+ creditBalances = {};
886
1196
  webSocketUrl = "wss://api.schematichq.com";
887
1197
  webSocketConnectionTimeout = 1e4;
888
1198
  webSocketReconnect = true;
@@ -911,11 +1221,16 @@ var Schematic = class {
911
1221
  fallbackCheckCache = {};
912
1222
  constructor(apiKey, options) {
913
1223
  this.apiKey = apiKey;
1224
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
914
1225
  this.eventQueue = [];
915
1226
  this.contextDependentEventQueue = [];
916
1227
  this.useWebSocket = options?.useWebSocket ?? false;
917
1228
  this.debugEnabled = options?.debug ?? false;
918
1229
  this.offlineEnabled = options?.offline ?? false;
1230
+ this.persistFlagState = options?.persistFlagState ?? true;
1231
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1232
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1233
+ }
919
1234
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
920
1235
  const params = new URLSearchParams(window.location.search);
921
1236
  const debugParam = params.get("schematic_debug");
@@ -948,6 +1263,7 @@ var Schematic = class {
948
1263
  } catch {
949
1264
  }
950
1265
  }
1266
+ this.hydrateFlagStateFromCache();
951
1267
  if (options?.apiUrl !== void 0) {
952
1268
  this.apiUrl = options.apiUrl;
953
1269
  }
@@ -1241,6 +1557,29 @@ var Schematic = class {
1241
1557
  });
1242
1558
  this.notifyPlanListeners(plan);
1243
1559
  }
1560
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1561
+ const contextStr = contextString(context);
1562
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1563
+ const previous = this.creditBalances[contextStr] ?? {};
1564
+ let changed = false;
1565
+ const merged = { ...previous };
1566
+ for (const [creditId, next] of Object.entries(updates)) {
1567
+ const current = previous[creditId];
1568
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1569
+ merged[creditId] = next;
1570
+ changed = true;
1571
+ }
1572
+ }
1573
+ if (changed) {
1574
+ this.creditBalances[contextStr] = merged;
1575
+ this.debug(
1576
+ `WebSocket credit balance update received. Notifying listeners`,
1577
+ { creditBalances: merged }
1578
+ );
1579
+ this.notifyCreditBalanceListeners(merged);
1580
+ }
1581
+ }
1582
+ this.persistContextToCache(contextString(context));
1244
1583
  this.flushContextDependentEventQueue();
1245
1584
  this.setIsPending(false);
1246
1585
  };
@@ -1348,7 +1687,32 @@ var Schematic = class {
1348
1687
  return;
1349
1688
  }
1350
1689
  try {
1351
- this.setIsPending(true);
1690
+ const newContextStr = contextString(context);
1691
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1692
+ if (cacheHit) {
1693
+ this.debug(
1694
+ `setContext: cache hit for context, pre-resolving from cached values`
1695
+ );
1696
+ this.context = context;
1697
+ this.flushContextDependentEventQueue();
1698
+ this.setIsPending(false);
1699
+ const cachedChecks = this.checks[newContextStr] ?? {};
1700
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1701
+ if (check === void 0) continue;
1702
+ this.notifyFlagCheckListeners(flagKey, check);
1703
+ this.notifyFlagValueListeners(flagKey, check.value);
1704
+ }
1705
+ const cachedPlan = this.planChecks[newContextStr];
1706
+ if (cachedPlan !== void 0) {
1707
+ this.notifyPlanListeners(cachedPlan);
1708
+ }
1709
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1710
+ if (cachedCreditBalances !== void 0) {
1711
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1712
+ }
1713
+ } else {
1714
+ this.setIsPending(true);
1715
+ }
1352
1716
  if (!this.conn) {
1353
1717
  if (this.isConnecting) {
1354
1718
  this.debug(
@@ -1359,7 +1723,7 @@ var Schematic = class {
1359
1723
  }
1360
1724
  if (this.conn !== null) {
1361
1725
  const socket2 = await this.conn;
1362
- await this.wsSendMessage(socket2, context);
1726
+ await this.wsSendMessage(socket2, context, cacheHit);
1363
1727
  return;
1364
1728
  }
1365
1729
  }
@@ -1375,7 +1739,7 @@ var Schematic = class {
1375
1739
  this.conn = this.wsConnect();
1376
1740
  const socket2 = await this.conn;
1377
1741
  this.isConnecting = false;
1378
- await this.wsSendMessage(socket2, context);
1742
+ await this.wsSendMessage(socket2, context, cacheHit);
1379
1743
  return;
1380
1744
  } catch (error) {
1381
1745
  this.isConnecting = false;
@@ -1383,7 +1747,7 @@ var Schematic = class {
1383
1747
  }
1384
1748
  }
1385
1749
  const socket = await this.conn;
1386
- await this.wsSendMessage(socket, context);
1750
+ await this.wsSendMessage(socket, context, cacheHit);
1387
1751
  } catch (error) {
1388
1752
  console.warn("Failed to establish WebSocket connection:", error);
1389
1753
  this.context = context;
@@ -1443,10 +1807,12 @@ var Schematic = class {
1443
1807
  `Optimistically updating feature usage for event: ${eventName}`,
1444
1808
  { quantity }
1445
1809
  );
1810
+ let dirty = false;
1446
1811
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1447
1812
  if (check === void 0) return;
1448
1813
  const updatedCheck = { ...check };
1449
1814
  if (typeof updatedCheck.featureUsage === "number") {
1815
+ dirty = true;
1450
1816
  updatedCheck.featureUsage += quantity;
1451
1817
  if (typeof updatedCheck.featureAllocation === "number") {
1452
1818
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1476,6 +1842,9 @@ var Schematic = class {
1476
1842
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1477
1843
  }
1478
1844
  });
1845
+ if (dirty) {
1846
+ this.persistContextToCache(contextString(this.context));
1847
+ }
1479
1848
  };
1480
1849
  /**
1481
1850
  * Event processing
@@ -1578,6 +1947,206 @@ var Schematic = class {
1578
1947
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1579
1948
  return generatedAnonymousId;
1580
1949
  };
1950
+ hasCachedValuesForContext = (contextStr) => {
1951
+ const cachedChecks = this.checks[contextStr];
1952
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1953
+ return true;
1954
+ }
1955
+ if (this.planChecks[contextStr] !== void 0) {
1956
+ return true;
1957
+ }
1958
+ const cachedCreditBalances = this.creditBalances[contextStr];
1959
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1960
+ };
1961
+ readFlagStateCache = () => {
1962
+ if (!this.persistFlagState || this.storage === void 0) {
1963
+ return null;
1964
+ }
1965
+ let raw;
1966
+ try {
1967
+ raw = this.storage.getItem(this.flagStateCacheKey);
1968
+ } catch (error) {
1969
+ this.debug("Failed to read flag state cache:", error);
1970
+ return null;
1971
+ }
1972
+ if (raw === null || raw === void 0) {
1973
+ return null;
1974
+ }
1975
+ let parsed;
1976
+ try {
1977
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1978
+ } catch (error) {
1979
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
1980
+ return null;
1981
+ }
1982
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
1983
+ this.debug(
1984
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
1985
+ {
1986
+ parsedType: typeof parsed,
1987
+ version: parsed?.version
1988
+ }
1989
+ );
1990
+ return null;
1991
+ }
1992
+ return parsed;
1993
+ };
1994
+ reviveCachedCheck = (raw) => {
1995
+ if (raw === null || typeof raw !== "object") return null;
1996
+ const obj = raw;
1997
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
1998
+ return null;
1999
+ }
2000
+ const revived = { ...obj };
2001
+ if (typeof revived.featureUsageResetAt === "string") {
2002
+ const d = new Date(revived.featureUsageResetAt);
2003
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
2004
+ }
2005
+ return revived;
2006
+ };
2007
+ reviveCachedPlan = (raw) => {
2008
+ if (raw === null || typeof raw !== "object") return null;
2009
+ const obj = raw;
2010
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
2011
+ return null;
2012
+ }
2013
+ const revived = { ...obj };
2014
+ if (typeof revived.trialEndDate === "string") {
2015
+ const d = new Date(revived.trialEndDate);
2016
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
2017
+ }
2018
+ return revived;
2019
+ };
2020
+ reviveCachedCreditBalances = (raw) => {
2021
+ if (raw === null || typeof raw !== "object") return null;
2022
+ const revived = {};
2023
+ for (const [creditId, value] of Object.entries(
2024
+ raw
2025
+ )) {
2026
+ if (value === null || typeof value !== "object") continue;
2027
+ const obj = value;
2028
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
2029
+ continue;
2030
+ }
2031
+ revived[creditId] = {
2032
+ remaining: obj.remaining,
2033
+ reserved: obj.reserved,
2034
+ settled: obj.settled
2035
+ };
2036
+ }
2037
+ return Object.keys(revived).length > 0 ? revived : null;
2038
+ };
2039
+ hydrateFlagStateFromCache = () => {
2040
+ const parsed = this.readFlagStateCache();
2041
+ if (parsed === null) return;
2042
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2043
+ const fresh = {};
2044
+ let contextsLoaded = 0;
2045
+ let contextsExpired = 0;
2046
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2047
+ if (entry === null || typeof entry !== "object") continue;
2048
+ if (typeof entry.updatedAt !== "number") continue;
2049
+ if (entry.updatedAt < cutoff) {
2050
+ contextsExpired += 1;
2051
+ continue;
2052
+ }
2053
+ const revivedChecks = {};
2054
+ if (entry.checks !== null && typeof entry.checks === "object") {
2055
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2056
+ const revived = this.reviveCachedCheck(rawCheck);
2057
+ if (revived === null || revived.flag !== flagKey) continue;
2058
+ revivedChecks[flagKey] = revived;
2059
+ if (typeof revived.featureUsageEvent === "string") {
2060
+ this.updateFeatureUsageEventMap(revived);
2061
+ }
2062
+ }
2063
+ if (Object.keys(revivedChecks).length > 0) {
2064
+ this.checks[contextStr] = revivedChecks;
2065
+ }
2066
+ }
2067
+ let revivedPlan;
2068
+ if (entry.plan !== void 0 && entry.plan !== null) {
2069
+ const p = this.reviveCachedPlan(entry.plan);
2070
+ if (p !== null) {
2071
+ revivedPlan = p;
2072
+ this.planChecks[contextStr] = p;
2073
+ }
2074
+ }
2075
+ let revivedCreditBalances;
2076
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2077
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2078
+ if (balances !== null) {
2079
+ revivedCreditBalances = balances;
2080
+ this.creditBalances[contextStr] = balances;
2081
+ }
2082
+ }
2083
+ fresh[contextStr] = {
2084
+ checks: revivedChecks,
2085
+ plan: revivedPlan,
2086
+ creditBalances: revivedCreditBalances,
2087
+ updatedAt: entry.updatedAt
2088
+ };
2089
+ contextsLoaded += 1;
2090
+ }
2091
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2092
+ this.debug(
2093
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2094
+ );
2095
+ };
2096
+ persistContextToCache = (contextStr) => {
2097
+ const checksForContext = this.checks[contextStr];
2098
+ const planForContext = this.planChecks[contextStr];
2099
+ const creditBalancesForContext = this.creditBalances[contextStr];
2100
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2101
+ return;
2102
+ }
2103
+ if (this.cachedFlagState === null) {
2104
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2105
+ version: cacheVersion,
2106
+ contexts: {}
2107
+ };
2108
+ }
2109
+ const cleanChecks = {};
2110
+ if (checksForContext !== void 0) {
2111
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2112
+ if (check !== void 0) cleanChecks[flagKey] = check;
2113
+ }
2114
+ }
2115
+ this.cachedFlagState.contexts[contextStr] = {
2116
+ checks: cleanChecks,
2117
+ plan: planForContext,
2118
+ creditBalances: creditBalancesForContext,
2119
+ updatedAt: Date.now()
2120
+ };
2121
+ const entries = Object.entries(this.cachedFlagState.contexts);
2122
+ if (entries.length > flagStateCacheMaxContexts) {
2123
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2124
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2125
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2126
+ for (const key of Object.keys(this.checks)) {
2127
+ if (!survivorKeys.has(key)) delete this.checks[key];
2128
+ }
2129
+ for (const key of Object.keys(this.planChecks)) {
2130
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2131
+ }
2132
+ for (const key of Object.keys(this.creditBalances)) {
2133
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2134
+ }
2135
+ this.cachedFlagState = {
2136
+ version: cacheVersion,
2137
+ contexts: Object.fromEntries(survivors)
2138
+ };
2139
+ }
2140
+ if (!this.persistFlagState || this.storage === void 0) return;
2141
+ try {
2142
+ this.storage.setItem(
2143
+ this.flagStateCacheKey,
2144
+ JSON.stringify(this.cachedFlagState)
2145
+ );
2146
+ } catch (error) {
2147
+ this.debug("Failed to write flag state cache:", error);
2148
+ }
2149
+ };
1581
2150
  handleEvent = (eventType, eventBody) => {
1582
2151
  const event = {
1583
2152
  api_key: this.apiKey,
@@ -1838,6 +2407,9 @@ var Schematic = class {
1838
2407
  if (this.conn !== null) {
1839
2408
  try {
1840
2409
  const socket = await this.conn;
2410
+ if (this.currentWebSocket === socket) {
2411
+ this.currentWebSocket = null;
2412
+ }
1841
2413
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1842
2414
  socket.close();
1843
2415
  }
@@ -2003,6 +2575,7 @@ var Schematic = class {
2003
2575
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2004
2576
  let timeoutId = null;
2005
2577
  let isResolved = false;
2578
+ let didOpen = false;
2006
2579
  timeoutId = setTimeout(() => {
2007
2580
  if (!isResolved) {
2008
2581
  isResolved = true;
@@ -2016,6 +2589,7 @@ var Schematic = class {
2016
2589
  webSocket.onopen = () => {
2017
2590
  if (isResolved) return;
2018
2591
  isResolved = true;
2592
+ didOpen = true;
2019
2593
  if (timeoutId !== null) {
2020
2594
  clearTimeout(timeoutId);
2021
2595
  }
@@ -2041,11 +2615,12 @@ var Schematic = class {
2041
2615
  }
2042
2616
  this.debug(`WebSocket connection ${connectionId} closed`);
2043
2617
  this.conn = null;
2618
+ const wasCurrent = this.currentWebSocket === webSocket;
2044
2619
  if (this.currentWebSocket === webSocket) {
2045
2620
  this.currentWebSocket = null;
2046
2621
  this.isConnecting = false;
2047
2622
  }
2048
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2623
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2049
2624
  this.attemptReconnect();
2050
2625
  }
2051
2626
  };
@@ -2141,6 +2716,16 @@ var Schematic = class {
2141
2716
  const plan = this.planChecks[contextStr];
2142
2717
  return plan;
2143
2718
  };
2719
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2720
+ getCreditBalances = () => {
2721
+ const contextStr = contextString(this.context);
2722
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2723
+ };
2724
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2725
+ getCreditBalance = (creditId) => {
2726
+ const contextStr = contextString(this.context);
2727
+ return this.creditBalances[contextStr]?.[creditId];
2728
+ };
2144
2729
  // flag checks state
2145
2730
  getFlagCheck = (flagKey) => {
2146
2731
  const contextStr = contextString(this.context);
@@ -2200,6 +2785,13 @@ var Schematic = class {
2200
2785
  this.planListeners.delete(listener);
2201
2786
  };
2202
2787
  };
2788
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2789
+ addCreditBalanceListener = (listener) => {
2790
+ this.creditBalanceListeners.add(listener);
2791
+ return () => {
2792
+ this.creditBalanceListeners.delete(listener);
2793
+ };
2794
+ };
2203
2795
  notifyFlagCheckListeners = (flagKey, check) => {
2204
2796
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2205
2797
  if (listeners.size > 0) {
@@ -2263,6 +2855,17 @@ var Schematic = class {
2263
2855
  });
2264
2856
  });
2265
2857
  };
2858
+ notifyCreditBalanceListeners = (value) => {
2859
+ const listeners = this.creditBalanceListeners ?? [];
2860
+ if (listeners.size > 0) {
2861
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2862
+ value
2863
+ });
2864
+ }
2865
+ listeners.forEach(
2866
+ (listener) => notifyCreditBalanceListener(listener, value)
2867
+ );
2868
+ };
2266
2869
  };
2267
2870
  var notifyPendingListener = (listener, value) => {
2268
2871
  if (listener.length > 0) {
@@ -2292,12 +2895,19 @@ var notifyPlanListener = (listener, value) => {
2292
2895
  listener();
2293
2896
  }
2294
2897
  };
2898
+ var notifyCreditBalanceListener = (listener, value) => {
2899
+ if (listener.length > 0) {
2900
+ listener(value);
2901
+ } else {
2902
+ listener();
2903
+ }
2904
+ };
2295
2905
 
2296
2906
  // src/context/schematic.ts
2297
2907
  var import_vue = require("vue");
2298
2908
 
2299
2909
  // src/version.ts
2300
- var version2 = "1.4.0";
2910
+ var version2 = "1.5.0";
2301
2911
 
2302
2912
  // src/context/schematic.ts
2303
2913
  var SchematicSymbol = Symbol("schematic");
@@ -2430,6 +3040,35 @@ var useSchematicPlan = (opts) => {
2430
3040
  });
2431
3041
  return plan;
2432
3042
  };
3043
+ var useSchematicCreditBalance = (creditId, opts) => {
3044
+ const client = useSchematicClient(opts);
3045
+ const creditBalance = (0, import_vue2.ref)(
3046
+ client.getCreditBalance(creditId)
3047
+ );
3048
+ const isPending = (0, import_vue2.ref)(client.getIsPending());
3049
+ let unsubscribeBalance = null;
3050
+ let unsubscribePending = null;
3051
+ (0, import_vue2.onMounted)(() => {
3052
+ unsubscribeBalance = client.addCreditBalanceListener(() => {
3053
+ creditBalance.value = client.getCreditBalance(creditId);
3054
+ });
3055
+ unsubscribePending = client.addIsPendingListener(() => {
3056
+ isPending.value = client.getIsPending();
3057
+ });
3058
+ });
3059
+ (0, import_vue2.onScopeDispose)(() => {
3060
+ unsubscribeBalance?.();
3061
+ unsubscribePending?.();
3062
+ });
3063
+ return {
3064
+ /** The spendable balance; 0 while loading or when the company holds no balance in this credit */
3065
+ balance: (0, import_vue2.computed)(() => creditBalance.value?.settled ?? 0),
3066
+ /** True while the balance is still loading and no value has arrived yet */
3067
+ isLoading: (0, import_vue2.computed)(
3068
+ () => creditBalance.value === void 0 && isPending.value
3069
+ )
3070
+ };
3071
+ };
2433
3072
  var useSchematicIsPending = (opts) => {
2434
3073
  const client = useSchematicClient(opts);
2435
3074
  const isPending = (0, import_vue2.ref)(client.getIsPending());