@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.
@@ -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
  function TrialStatusFromJSON(json) {
677
956
  return TrialStatusFromJSONTyped(json, false);
678
957
  }
@@ -701,6 +980,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
701
980
  return json;
702
981
  }
703
982
  return {
983
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
704
984
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
705
985
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
706
986
  };
@@ -783,7 +1063,10 @@ var CheckFlagReturnFromJSON = (json) => {
783
1063
  return {
784
1064
  featureUsageExceeded,
785
1065
  companyId: companyId == null ? void 0 : companyId,
1066
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
786
1067
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1068
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1069
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
787
1070
  error: error == null ? void 0 : error,
788
1071
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
789
1072
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -809,6 +1092,22 @@ var CheckPlanReturnFromJSON = (json) => {
809
1092
  trialStatus: trialStatus == null ? void 0 : trialStatus
810
1093
  };
811
1094
  };
1095
+ var CreditBalancesFromJSON = (json) => {
1096
+ if (json == null || typeof json !== "object") {
1097
+ return {};
1098
+ }
1099
+ const balances = {};
1100
+ for (const [creditId, raw] of Object.entries(json)) {
1101
+ if (raw == null || typeof raw !== "object") continue;
1102
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1103
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1104
+ continue;
1105
+ }
1106
+ balances[creditId] = { remaining, reserved, settled };
1107
+ }
1108
+ return balances;
1109
+ };
1110
+ var cacheVersion = "bb56e75d";
812
1111
  function contextString(context) {
813
1112
  const sortedContext = Object.keys(context).reduce((acc, key) => {
814
1113
  const sortedKeys = Object.keys(
@@ -823,8 +1122,12 @@ function contextString(context) {
823
1122
  }, {});
824
1123
  return JSON.stringify(sortedContext);
825
1124
  }
826
- var version = "1.4.0";
1125
+ var version = "1.5.0";
827
1126
  var anonymousIdKey = "schematicId";
1127
+ var flagStateCachePrefix = "schematicCache";
1128
+ var flagStateCacheMaxContexts = 10;
1129
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1130
+ var emptyCreditBalances = Object.freeze({});
828
1131
  var Schematic = class {
829
1132
  additionalHeaders = {};
830
1133
  apiKey;
@@ -841,11 +1144,17 @@ var Schematic = class {
841
1144
  isPending = true;
842
1145
  isPendingListeners = /* @__PURE__ */ new Set();
843
1146
  planListeners = /* @__PURE__ */ new Set();
1147
+ creditBalanceListeners = /* @__PURE__ */ new Set();
844
1148
  storage;
1149
+ persistFlagState = true;
1150
+ flagStateCacheKey;
1151
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1152
+ cachedFlagState = null;
845
1153
  useWebSocket = false;
846
1154
  checks = {};
847
1155
  featureUsageEventMap = {};
848
1156
  planChecks = {};
1157
+ creditBalances = {};
849
1158
  webSocketUrl = "wss://api.schematichq.com";
850
1159
  webSocketConnectionTimeout = 1e4;
851
1160
  webSocketReconnect = true;
@@ -874,11 +1183,16 @@ var Schematic = class {
874
1183
  fallbackCheckCache = {};
875
1184
  constructor(apiKey, options) {
876
1185
  this.apiKey = apiKey;
1186
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
877
1187
  this.eventQueue = [];
878
1188
  this.contextDependentEventQueue = [];
879
1189
  this.useWebSocket = options?.useWebSocket ?? false;
880
1190
  this.debugEnabled = options?.debug ?? false;
881
1191
  this.offlineEnabled = options?.offline ?? false;
1192
+ this.persistFlagState = options?.persistFlagState ?? true;
1193
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1194
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1195
+ }
882
1196
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
883
1197
  const params = new URLSearchParams(window.location.search);
884
1198
  const debugParam = params.get("schematic_debug");
@@ -911,6 +1225,7 @@ var Schematic = class {
911
1225
  } catch {
912
1226
  }
913
1227
  }
1228
+ this.hydrateFlagStateFromCache();
914
1229
  if (options?.apiUrl !== void 0) {
915
1230
  this.apiUrl = options.apiUrl;
916
1231
  }
@@ -1204,6 +1519,29 @@ var Schematic = class {
1204
1519
  });
1205
1520
  this.notifyPlanListeners(plan);
1206
1521
  }
1522
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1523
+ const contextStr = contextString(context);
1524
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1525
+ const previous = this.creditBalances[contextStr] ?? {};
1526
+ let changed = false;
1527
+ const merged = { ...previous };
1528
+ for (const [creditId, next] of Object.entries(updates)) {
1529
+ const current = previous[creditId];
1530
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1531
+ merged[creditId] = next;
1532
+ changed = true;
1533
+ }
1534
+ }
1535
+ if (changed) {
1536
+ this.creditBalances[contextStr] = merged;
1537
+ this.debug(
1538
+ `WebSocket credit balance update received. Notifying listeners`,
1539
+ { creditBalances: merged }
1540
+ );
1541
+ this.notifyCreditBalanceListeners(merged);
1542
+ }
1543
+ }
1544
+ this.persistContextToCache(contextString(context));
1207
1545
  this.flushContextDependentEventQueue();
1208
1546
  this.setIsPending(false);
1209
1547
  };
@@ -1311,7 +1649,32 @@ var Schematic = class {
1311
1649
  return;
1312
1650
  }
1313
1651
  try {
1314
- this.setIsPending(true);
1652
+ const newContextStr = contextString(context);
1653
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1654
+ if (cacheHit) {
1655
+ this.debug(
1656
+ `setContext: cache hit for context, pre-resolving from cached values`
1657
+ );
1658
+ this.context = context;
1659
+ this.flushContextDependentEventQueue();
1660
+ this.setIsPending(false);
1661
+ const cachedChecks = this.checks[newContextStr] ?? {};
1662
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1663
+ if (check === void 0) continue;
1664
+ this.notifyFlagCheckListeners(flagKey, check);
1665
+ this.notifyFlagValueListeners(flagKey, check.value);
1666
+ }
1667
+ const cachedPlan = this.planChecks[newContextStr];
1668
+ if (cachedPlan !== void 0) {
1669
+ this.notifyPlanListeners(cachedPlan);
1670
+ }
1671
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1672
+ if (cachedCreditBalances !== void 0) {
1673
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1674
+ }
1675
+ } else {
1676
+ this.setIsPending(true);
1677
+ }
1315
1678
  if (!this.conn) {
1316
1679
  if (this.isConnecting) {
1317
1680
  this.debug(
@@ -1322,7 +1685,7 @@ var Schematic = class {
1322
1685
  }
1323
1686
  if (this.conn !== null) {
1324
1687
  const socket2 = await this.conn;
1325
- await this.wsSendMessage(socket2, context);
1688
+ await this.wsSendMessage(socket2, context, cacheHit);
1326
1689
  return;
1327
1690
  }
1328
1691
  }
@@ -1338,7 +1701,7 @@ var Schematic = class {
1338
1701
  this.conn = this.wsConnect();
1339
1702
  const socket2 = await this.conn;
1340
1703
  this.isConnecting = false;
1341
- await this.wsSendMessage(socket2, context);
1704
+ await this.wsSendMessage(socket2, context, cacheHit);
1342
1705
  return;
1343
1706
  } catch (error) {
1344
1707
  this.isConnecting = false;
@@ -1346,7 +1709,7 @@ var Schematic = class {
1346
1709
  }
1347
1710
  }
1348
1711
  const socket = await this.conn;
1349
- await this.wsSendMessage(socket, context);
1712
+ await this.wsSendMessage(socket, context, cacheHit);
1350
1713
  } catch (error) {
1351
1714
  console.warn("Failed to establish WebSocket connection:", error);
1352
1715
  this.context = context;
@@ -1406,10 +1769,12 @@ var Schematic = class {
1406
1769
  `Optimistically updating feature usage for event: ${eventName}`,
1407
1770
  { quantity }
1408
1771
  );
1772
+ let dirty = false;
1409
1773
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1410
1774
  if (check === void 0) return;
1411
1775
  const updatedCheck = { ...check };
1412
1776
  if (typeof updatedCheck.featureUsage === "number") {
1777
+ dirty = true;
1413
1778
  updatedCheck.featureUsage += quantity;
1414
1779
  if (typeof updatedCheck.featureAllocation === "number") {
1415
1780
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1439,6 +1804,9 @@ var Schematic = class {
1439
1804
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1440
1805
  }
1441
1806
  });
1807
+ if (dirty) {
1808
+ this.persistContextToCache(contextString(this.context));
1809
+ }
1442
1810
  };
1443
1811
  /**
1444
1812
  * Event processing
@@ -1541,6 +1909,206 @@ var Schematic = class {
1541
1909
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1542
1910
  return generatedAnonymousId;
1543
1911
  };
1912
+ hasCachedValuesForContext = (contextStr) => {
1913
+ const cachedChecks = this.checks[contextStr];
1914
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1915
+ return true;
1916
+ }
1917
+ if (this.planChecks[contextStr] !== void 0) {
1918
+ return true;
1919
+ }
1920
+ const cachedCreditBalances = this.creditBalances[contextStr];
1921
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1922
+ };
1923
+ readFlagStateCache = () => {
1924
+ if (!this.persistFlagState || this.storage === void 0) {
1925
+ return null;
1926
+ }
1927
+ let raw;
1928
+ try {
1929
+ raw = this.storage.getItem(this.flagStateCacheKey);
1930
+ } catch (error) {
1931
+ this.debug("Failed to read flag state cache:", error);
1932
+ return null;
1933
+ }
1934
+ if (raw === null || raw === void 0) {
1935
+ return null;
1936
+ }
1937
+ let parsed;
1938
+ try {
1939
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1940
+ } catch (error) {
1941
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
1942
+ return null;
1943
+ }
1944
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
1945
+ this.debug(
1946
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
1947
+ {
1948
+ parsedType: typeof parsed,
1949
+ version: parsed?.version
1950
+ }
1951
+ );
1952
+ return null;
1953
+ }
1954
+ return parsed;
1955
+ };
1956
+ reviveCachedCheck = (raw) => {
1957
+ if (raw === null || typeof raw !== "object") return null;
1958
+ const obj = raw;
1959
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
1960
+ return null;
1961
+ }
1962
+ const revived = { ...obj };
1963
+ if (typeof revived.featureUsageResetAt === "string") {
1964
+ const d = new Date(revived.featureUsageResetAt);
1965
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
1966
+ }
1967
+ return revived;
1968
+ };
1969
+ reviveCachedPlan = (raw) => {
1970
+ if (raw === null || typeof raw !== "object") return null;
1971
+ const obj = raw;
1972
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
1973
+ return null;
1974
+ }
1975
+ const revived = { ...obj };
1976
+ if (typeof revived.trialEndDate === "string") {
1977
+ const d = new Date(revived.trialEndDate);
1978
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
1979
+ }
1980
+ return revived;
1981
+ };
1982
+ reviveCachedCreditBalances = (raw) => {
1983
+ if (raw === null || typeof raw !== "object") return null;
1984
+ const revived = {};
1985
+ for (const [creditId, value] of Object.entries(
1986
+ raw
1987
+ )) {
1988
+ if (value === null || typeof value !== "object") continue;
1989
+ const obj = value;
1990
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
1991
+ continue;
1992
+ }
1993
+ revived[creditId] = {
1994
+ remaining: obj.remaining,
1995
+ reserved: obj.reserved,
1996
+ settled: obj.settled
1997
+ };
1998
+ }
1999
+ return Object.keys(revived).length > 0 ? revived : null;
2000
+ };
2001
+ hydrateFlagStateFromCache = () => {
2002
+ const parsed = this.readFlagStateCache();
2003
+ if (parsed === null) return;
2004
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2005
+ const fresh = {};
2006
+ let contextsLoaded = 0;
2007
+ let contextsExpired = 0;
2008
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2009
+ if (entry === null || typeof entry !== "object") continue;
2010
+ if (typeof entry.updatedAt !== "number") continue;
2011
+ if (entry.updatedAt < cutoff) {
2012
+ contextsExpired += 1;
2013
+ continue;
2014
+ }
2015
+ const revivedChecks = {};
2016
+ if (entry.checks !== null && typeof entry.checks === "object") {
2017
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2018
+ const revived = this.reviveCachedCheck(rawCheck);
2019
+ if (revived === null || revived.flag !== flagKey) continue;
2020
+ revivedChecks[flagKey] = revived;
2021
+ if (typeof revived.featureUsageEvent === "string") {
2022
+ this.updateFeatureUsageEventMap(revived);
2023
+ }
2024
+ }
2025
+ if (Object.keys(revivedChecks).length > 0) {
2026
+ this.checks[contextStr] = revivedChecks;
2027
+ }
2028
+ }
2029
+ let revivedPlan;
2030
+ if (entry.plan !== void 0 && entry.plan !== null) {
2031
+ const p = this.reviveCachedPlan(entry.plan);
2032
+ if (p !== null) {
2033
+ revivedPlan = p;
2034
+ this.planChecks[contextStr] = p;
2035
+ }
2036
+ }
2037
+ let revivedCreditBalances;
2038
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2039
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2040
+ if (balances !== null) {
2041
+ revivedCreditBalances = balances;
2042
+ this.creditBalances[contextStr] = balances;
2043
+ }
2044
+ }
2045
+ fresh[contextStr] = {
2046
+ checks: revivedChecks,
2047
+ plan: revivedPlan,
2048
+ creditBalances: revivedCreditBalances,
2049
+ updatedAt: entry.updatedAt
2050
+ };
2051
+ contextsLoaded += 1;
2052
+ }
2053
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2054
+ this.debug(
2055
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2056
+ );
2057
+ };
2058
+ persistContextToCache = (contextStr) => {
2059
+ const checksForContext = this.checks[contextStr];
2060
+ const planForContext = this.planChecks[contextStr];
2061
+ const creditBalancesForContext = this.creditBalances[contextStr];
2062
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2063
+ return;
2064
+ }
2065
+ if (this.cachedFlagState === null) {
2066
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2067
+ version: cacheVersion,
2068
+ contexts: {}
2069
+ };
2070
+ }
2071
+ const cleanChecks = {};
2072
+ if (checksForContext !== void 0) {
2073
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2074
+ if (check !== void 0) cleanChecks[flagKey] = check;
2075
+ }
2076
+ }
2077
+ this.cachedFlagState.contexts[contextStr] = {
2078
+ checks: cleanChecks,
2079
+ plan: planForContext,
2080
+ creditBalances: creditBalancesForContext,
2081
+ updatedAt: Date.now()
2082
+ };
2083
+ const entries = Object.entries(this.cachedFlagState.contexts);
2084
+ if (entries.length > flagStateCacheMaxContexts) {
2085
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2086
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2087
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2088
+ for (const key of Object.keys(this.checks)) {
2089
+ if (!survivorKeys.has(key)) delete this.checks[key];
2090
+ }
2091
+ for (const key of Object.keys(this.planChecks)) {
2092
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2093
+ }
2094
+ for (const key of Object.keys(this.creditBalances)) {
2095
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2096
+ }
2097
+ this.cachedFlagState = {
2098
+ version: cacheVersion,
2099
+ contexts: Object.fromEntries(survivors)
2100
+ };
2101
+ }
2102
+ if (!this.persistFlagState || this.storage === void 0) return;
2103
+ try {
2104
+ this.storage.setItem(
2105
+ this.flagStateCacheKey,
2106
+ JSON.stringify(this.cachedFlagState)
2107
+ );
2108
+ } catch (error) {
2109
+ this.debug("Failed to write flag state cache:", error);
2110
+ }
2111
+ };
1544
2112
  handleEvent = (eventType, eventBody) => {
1545
2113
  const event = {
1546
2114
  api_key: this.apiKey,
@@ -1801,6 +2369,9 @@ var Schematic = class {
1801
2369
  if (this.conn !== null) {
1802
2370
  try {
1803
2371
  const socket = await this.conn;
2372
+ if (this.currentWebSocket === socket) {
2373
+ this.currentWebSocket = null;
2374
+ }
1804
2375
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1805
2376
  socket.close();
1806
2377
  }
@@ -1966,6 +2537,7 @@ var Schematic = class {
1966
2537
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1967
2538
  let timeoutId = null;
1968
2539
  let isResolved = false;
2540
+ let didOpen = false;
1969
2541
  timeoutId = setTimeout(() => {
1970
2542
  if (!isResolved) {
1971
2543
  isResolved = true;
@@ -1979,6 +2551,7 @@ var Schematic = class {
1979
2551
  webSocket.onopen = () => {
1980
2552
  if (isResolved) return;
1981
2553
  isResolved = true;
2554
+ didOpen = true;
1982
2555
  if (timeoutId !== null) {
1983
2556
  clearTimeout(timeoutId);
1984
2557
  }
@@ -2004,11 +2577,12 @@ var Schematic = class {
2004
2577
  }
2005
2578
  this.debug(`WebSocket connection ${connectionId} closed`);
2006
2579
  this.conn = null;
2580
+ const wasCurrent = this.currentWebSocket === webSocket;
2007
2581
  if (this.currentWebSocket === webSocket) {
2008
2582
  this.currentWebSocket = null;
2009
2583
  this.isConnecting = false;
2010
2584
  }
2011
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2585
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2012
2586
  this.attemptReconnect();
2013
2587
  }
2014
2588
  };
@@ -2104,6 +2678,16 @@ var Schematic = class {
2104
2678
  const plan = this.planChecks[contextStr];
2105
2679
  return plan;
2106
2680
  };
2681
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2682
+ getCreditBalances = () => {
2683
+ const contextStr = contextString(this.context);
2684
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2685
+ };
2686
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2687
+ getCreditBalance = (creditId) => {
2688
+ const contextStr = contextString(this.context);
2689
+ return this.creditBalances[contextStr]?.[creditId];
2690
+ };
2107
2691
  // flag checks state
2108
2692
  getFlagCheck = (flagKey) => {
2109
2693
  const contextStr = contextString(this.context);
@@ -2163,6 +2747,13 @@ var Schematic = class {
2163
2747
  this.planListeners.delete(listener);
2164
2748
  };
2165
2749
  };
2750
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2751
+ addCreditBalanceListener = (listener) => {
2752
+ this.creditBalanceListeners.add(listener);
2753
+ return () => {
2754
+ this.creditBalanceListeners.delete(listener);
2755
+ };
2756
+ };
2166
2757
  notifyFlagCheckListeners = (flagKey, check) => {
2167
2758
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2168
2759
  if (listeners.size > 0) {
@@ -2226,6 +2817,17 @@ var Schematic = class {
2226
2817
  });
2227
2818
  });
2228
2819
  };
2820
+ notifyCreditBalanceListeners = (value) => {
2821
+ const listeners = this.creditBalanceListeners ?? [];
2822
+ if (listeners.size > 0) {
2823
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2824
+ value
2825
+ });
2826
+ }
2827
+ listeners.forEach(
2828
+ (listener) => notifyCreditBalanceListener(listener, value)
2829
+ );
2830
+ };
2229
2831
  };
2230
2832
  var notifyPendingListener = (listener, value) => {
2231
2833
  if (listener.length > 0) {
@@ -2255,12 +2857,19 @@ var notifyPlanListener = (listener, value) => {
2255
2857
  listener();
2256
2858
  }
2257
2859
  };
2860
+ var notifyCreditBalanceListener = (listener, value) => {
2861
+ if (listener.length > 0) {
2862
+ listener(value);
2863
+ } else {
2864
+ listener();
2865
+ }
2866
+ };
2258
2867
 
2259
2868
  // src/context/schematic.ts
2260
2869
  import { inject, onScopeDispose } from "vue";
2261
2870
 
2262
2871
  // src/version.ts
2263
- var version2 = "1.4.0";
2872
+ var version2 = "1.5.0";
2264
2873
 
2265
2874
  // src/context/schematic.ts
2266
2875
  var SchematicSymbol = Symbol("schematic");
@@ -2393,6 +3002,35 @@ var useSchematicPlan = (opts) => {
2393
3002
  });
2394
3003
  return plan;
2395
3004
  };
3005
+ var useSchematicCreditBalance = (creditId, opts) => {
3006
+ const client = useSchematicClient(opts);
3007
+ const creditBalance = ref(
3008
+ client.getCreditBalance(creditId)
3009
+ );
3010
+ const isPending = ref(client.getIsPending());
3011
+ let unsubscribeBalance = null;
3012
+ let unsubscribePending = null;
3013
+ onMounted(() => {
3014
+ unsubscribeBalance = client.addCreditBalanceListener(() => {
3015
+ creditBalance.value = client.getCreditBalance(creditId);
3016
+ });
3017
+ unsubscribePending = client.addIsPendingListener(() => {
3018
+ isPending.value = client.getIsPending();
3019
+ });
3020
+ });
3021
+ onScopeDispose2(() => {
3022
+ unsubscribeBalance?.();
3023
+ unsubscribePending?.();
3024
+ });
3025
+ return {
3026
+ /** The spendable balance; 0 while loading or when the company holds no balance in this credit */
3027
+ balance: computed(() => creditBalance.value?.settled ?? 0),
3028
+ /** True while the balance is still loading and no value has arrived yet */
3029
+ isLoading: computed(
3030
+ () => creditBalance.value === void 0 && isPending.value
3031
+ )
3032
+ };
3033
+ };
2396
3034
  var useSchematicIsPending = (opts) => {
2397
3035
  const client = useSchematicClient(opts);
2398
3036
  const isPending = ref(client.getIsPending());
@@ -2415,6 +3053,7 @@ export {
2415
3053
  useSchematic,
2416
3054
  useSchematicClient,
2417
3055
  useSchematicContext,
3056
+ useSchematicCreditBalance,
2418
3057
  useSchematicEntitlement,
2419
3058
  useSchematicEvents,
2420
3059
  useSchematicFlag,