@schematichq/schematic-js 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.
@@ -5,7 +5,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getProtoOf = Object.getPrototypeOf;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
7
  var __commonJS = (cb, mod) => function __require() {
8
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
8
+ try {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ } catch (e) {
11
+ throw mod = 0, e;
12
+ }
9
13
  };
10
14
  var __copyProps = (to, from, except, desc) => {
11
15
  if (from && typeof from === "object" || typeof from === "function") {
@@ -570,23 +574,18 @@ function unsafeStringify(arr, offset = 0) {
570
574
  }
571
575
 
572
576
  // node_modules/uuid/dist/rng.js
573
- var getRandomValues;
574
577
  var rnds8 = new Uint8Array(16);
575
578
  function rng() {
576
- if (!getRandomValues) {
577
- if (typeof crypto === "undefined" || !crypto.getRandomValues) {
578
- throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
579
- }
580
- getRandomValues = crypto.getRandomValues.bind(crypto);
581
- }
582
- return getRandomValues(rnds8);
579
+ return crypto.getRandomValues(rnds8);
583
580
  }
584
581
 
585
- // node_modules/uuid/dist/native.js
586
- var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
587
- var native_default = { randomUUID };
588
-
589
582
  // node_modules/uuid/dist/v4.js
583
+ function v4(options, buf, offset) {
584
+ if (!buf && !options && crypto.randomUUID) {
585
+ return crypto.randomUUID();
586
+ }
587
+ return _v4(options, buf, offset);
588
+ }
590
589
  function _v4(options, buf, offset) {
591
590
  options = options || {};
592
591
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -607,12 +606,6 @@ function _v4(options, buf, offset) {
607
606
  }
608
607
  return unsafeStringify(rnds);
609
608
  }
610
- function v4(options, buf, offset) {
611
- if (native_default.randomUUID && !buf && !options) {
612
- return native_default.randomUUID();
613
- }
614
- return _v4(options, buf, offset);
615
- }
616
609
  var v4_default = v4;
617
610
 
618
611
  // src/index.ts
@@ -626,6 +619,22 @@ function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
626
619
  return json;
627
620
  }
628
621
 
622
+ // src/types/api/models/MetricPeriodMonthReset.ts
623
+ function MetricPeriodMonthResetFromJSON(json) {
624
+ return MetricPeriodMonthResetFromJSONTyped(json, false);
625
+ }
626
+ function MetricPeriodMonthResetFromJSONTyped(json, ignoreDiscriminator) {
627
+ return json;
628
+ }
629
+
630
+ // src/types/api/models/MetricPeriod.ts
631
+ function MetricPeriodFromJSON(json) {
632
+ return MetricPeriodFromJSONTyped(json, false);
633
+ }
634
+ function MetricPeriodFromJSONTyped(json, ignoreDiscriminator) {
635
+ return json;
636
+ }
637
+
629
638
  // src/types/api/models/FeatureEntitlement.ts
630
639
  function FeatureEntitlementFromJSON(json) {
631
640
  return FeatureEntitlementFromJSONTyped(json, false);
@@ -636,22 +645,34 @@ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
636
645
  }
637
646
  return {
638
647
  allocation: json["allocation"] == null ? void 0 : json["allocation"],
648
+ consumptionRate: json["consumption_rate"] == null ? void 0 : json["consumption_rate"],
639
649
  creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
640
650
  creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
651
+ creditReserved: json["credit_reserved"] == null ? void 0 : json["credit_reserved"],
652
+ creditSettled: json["credit_settled"] == null ? void 0 : json["credit_settled"],
641
653
  creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
642
654
  creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
643
655
  eventName: json["event_name"] == null ? void 0 : json["event_name"],
656
+ eventSubtype: json["event_subtype"] == null ? void 0 : json["event_subtype"],
644
657
  featureId: json["feature_id"],
645
658
  featureKey: json["feature_key"],
646
- metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
659
+ metricPeriod: json["metric_period"] == null ? void 0 : MetricPeriodFromJSON(json["metric_period"]),
647
660
  metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
648
- monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
661
+ monthReset: json["month_reset"] == null ? void 0 : MetricPeriodMonthResetFromJSON(json["month_reset"]),
649
662
  softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
650
663
  usage: json["usage"] == null ? void 0 : json["usage"],
651
664
  valueType: EntitlementValueTypeFromJSON(json["value_type"])
652
665
  };
653
666
  }
654
667
 
668
+ // src/types/api/models/RuleType.ts
669
+ function RuleTypeFromJSON(json) {
670
+ return RuleTypeFromJSONTyped(json, false);
671
+ }
672
+ function RuleTypeFromJSONTyped(json, ignoreDiscriminator) {
673
+ return json;
674
+ }
675
+
655
676
  // src/types/api/models/CheckFlagResponseData.ts
656
677
  function CheckFlagResponseDataFromJSON(json) {
657
678
  return CheckFlagResponseDataFromJSONTyped(json, false);
@@ -667,13 +688,13 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
667
688
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
668
689
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
669
690
  featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
670
- featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
691
+ featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : MetricPeriodFromJSON(json["feature_usage_period"]),
671
692
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
672
693
  flag: json["flag"],
673
694
  flagId: json["flag_id"] == null ? void 0 : json["flag_id"],
674
695
  reason: json["reason"],
675
696
  ruleId: json["rule_id"] == null ? void 0 : json["rule_id"],
676
- ruleType: json["rule_type"] == null ? void 0 : json["rule_type"],
697
+ ruleType: json["rule_type"] == null ? void 0 : RuleTypeFromJSON(json["rule_type"]),
677
698
  userId: json["user_id"] == null ? void 0 : json["user_id"],
678
699
  value: json["value"]
679
700
  };
@@ -693,6 +714,272 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
693
714
  };
694
715
  }
695
716
 
717
+ // src/types/api/runtime.ts
718
+ var BASE_PATH = "https://api.schematichq.com".replace(/\/+$/, "");
719
+ var Configuration = class {
720
+ constructor(configuration = {}) {
721
+ this.configuration = configuration;
722
+ }
723
+ configuration;
724
+ set config(configuration) {
725
+ this.configuration = configuration;
726
+ }
727
+ get basePath() {
728
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
729
+ }
730
+ get fetchApi() {
731
+ return this.configuration.fetchApi;
732
+ }
733
+ get middleware() {
734
+ return this.configuration.middleware || [];
735
+ }
736
+ get queryParamsStringify() {
737
+ return this.configuration.queryParamsStringify || querystring;
738
+ }
739
+ get username() {
740
+ return this.configuration.username;
741
+ }
742
+ get password() {
743
+ return this.configuration.password;
744
+ }
745
+ get apiKey() {
746
+ const apiKey = this.configuration.apiKey;
747
+ if (apiKey) {
748
+ return typeof apiKey === "function" ? apiKey : () => apiKey;
749
+ }
750
+ return void 0;
751
+ }
752
+ get accessToken() {
753
+ const accessToken = this.configuration.accessToken;
754
+ if (accessToken) {
755
+ return typeof accessToken === "function" ? accessToken : async () => accessToken;
756
+ }
757
+ return void 0;
758
+ }
759
+ get headers() {
760
+ return this.configuration.headers;
761
+ }
762
+ get credentials() {
763
+ return this.configuration.credentials;
764
+ }
765
+ };
766
+ var DefaultConfig = new Configuration();
767
+ var BaseAPI = class _BaseAPI {
768
+ constructor(configuration = DefaultConfig) {
769
+ this.configuration = configuration;
770
+ this.middleware = configuration.middleware;
771
+ }
772
+ configuration;
773
+ static jsonRegex = new RegExp(
774
+ "^(:?application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(:?;.*)?$",
775
+ "i"
776
+ );
777
+ middleware;
778
+ withMiddleware(...middlewares) {
779
+ const next = this.clone();
780
+ next.middleware = next.middleware.concat(...middlewares);
781
+ return next;
782
+ }
783
+ withPreMiddleware(...preMiddlewares) {
784
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
785
+ return this.withMiddleware(...middlewares);
786
+ }
787
+ withPostMiddleware(...postMiddlewares) {
788
+ const middlewares = postMiddlewares.map((post) => ({ post }));
789
+ return this.withMiddleware(...middlewares);
790
+ }
791
+ /**
792
+ * Check if the given MIME is a JSON MIME.
793
+ * JSON MIME examples:
794
+ * application/json
795
+ * application/json; charset=UTF8
796
+ * APPLICATION/JSON
797
+ * application/vnd.company+json
798
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
799
+ * @return True if the given MIME is JSON, false otherwise.
800
+ */
801
+ isJsonMime(mime) {
802
+ if (!mime) {
803
+ return false;
804
+ }
805
+ return _BaseAPI.jsonRegex.test(mime);
806
+ }
807
+ async request(context, initOverrides) {
808
+ const { url, init } = await this.createFetchParams(context, initOverrides);
809
+ const response = await this.fetchApi(url, init);
810
+ if (response && response.status >= 200 && response.status < 300) {
811
+ return response;
812
+ }
813
+ throw new ResponseError(response, "Response returned an error code");
814
+ }
815
+ async createFetchParams(context, initOverrides) {
816
+ let url = this.configuration.basePath + context.path;
817
+ if (context.query !== void 0 && Object.keys(context.query).length !== 0) {
818
+ url += "?" + this.configuration.queryParamsStringify(context.query);
819
+ }
820
+ const headers = Object.assign(
821
+ {},
822
+ this.configuration.headers,
823
+ context.headers
824
+ );
825
+ Object.keys(headers).forEach(
826
+ (key) => headers[key] === void 0 ? delete headers[key] : {}
827
+ );
828
+ const initOverrideFn = typeof initOverrides === "function" ? initOverrides : async () => initOverrides;
829
+ const initParams = {
830
+ method: context.method,
831
+ headers,
832
+ body: context.body,
833
+ credentials: this.configuration.credentials
834
+ };
835
+ const overriddenInit = {
836
+ ...initParams,
837
+ ...await initOverrideFn({
838
+ init: initParams,
839
+ context
840
+ })
841
+ };
842
+ let body;
843
+ if (isFormData(overriddenInit.body) || overriddenInit.body instanceof URLSearchParams || isBlob(overriddenInit.body)) {
844
+ body = overriddenInit.body;
845
+ } else if (this.isJsonMime(headers["Content-Type"])) {
846
+ body = JSON.stringify(overriddenInit.body);
847
+ } else {
848
+ body = overriddenInit.body;
849
+ }
850
+ const init = {
851
+ ...overriddenInit,
852
+ body
853
+ };
854
+ return { url, init };
855
+ }
856
+ fetchApi = async (url, init) => {
857
+ let fetchParams = { url, init };
858
+ for (const middleware of this.middleware) {
859
+ if (middleware.pre) {
860
+ fetchParams = await middleware.pre({
861
+ fetch: this.fetchApi,
862
+ ...fetchParams
863
+ }) || fetchParams;
864
+ }
865
+ }
866
+ let response = void 0;
867
+ try {
868
+ response = await (this.configuration.fetchApi || fetch)(
869
+ fetchParams.url,
870
+ fetchParams.init
871
+ );
872
+ } catch (e) {
873
+ for (const middleware of this.middleware) {
874
+ if (middleware.onError) {
875
+ response = await middleware.onError({
876
+ fetch: this.fetchApi,
877
+ url: fetchParams.url,
878
+ init: fetchParams.init,
879
+ error: e,
880
+ response: response ? response.clone() : void 0
881
+ }) || response;
882
+ }
883
+ }
884
+ if (response === void 0) {
885
+ if (e instanceof Error) {
886
+ throw new FetchError(
887
+ e,
888
+ "The request failed and the interceptors did not return an alternative response"
889
+ );
890
+ } else {
891
+ throw e;
892
+ }
893
+ }
894
+ }
895
+ for (const middleware of this.middleware) {
896
+ if (middleware.post) {
897
+ response = await middleware.post({
898
+ fetch: this.fetchApi,
899
+ url: fetchParams.url,
900
+ init: fetchParams.init,
901
+ response: response.clone()
902
+ }) || response;
903
+ }
904
+ }
905
+ return response;
906
+ };
907
+ /**
908
+ * Create a shallow clone of `this` by constructing a new instance
909
+ * and then shallow cloning data members.
910
+ */
911
+ clone() {
912
+ const constructor = this.constructor;
913
+ const next = new constructor(this.configuration);
914
+ next.middleware = this.middleware.slice();
915
+ return next;
916
+ }
917
+ };
918
+ function isBlob(value) {
919
+ return typeof Blob !== "undefined" && value instanceof Blob;
920
+ }
921
+ function isFormData(value) {
922
+ return typeof FormData !== "undefined" && value instanceof FormData;
923
+ }
924
+ var ResponseError = class extends Error {
925
+ constructor(response, msg) {
926
+ super(msg);
927
+ this.response = response;
928
+ }
929
+ response;
930
+ name = "ResponseError";
931
+ };
932
+ var FetchError = class extends Error {
933
+ constructor(cause, msg) {
934
+ super(msg);
935
+ this.cause = cause;
936
+ }
937
+ cause;
938
+ name = "FetchError";
939
+ };
940
+ function querystring(params, prefix = "") {
941
+ return Object.keys(params).map((key) => querystringSingleKey(key, params[key], prefix)).filter((part) => part.length > 0).join("&");
942
+ }
943
+ function querystringSingleKey(key, value, keyPrefix = "") {
944
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
945
+ if (value instanceof Array) {
946
+ const multiValue = value.map((singleValue) => encodeURIComponent(String(singleValue))).join(`&${encodeURIComponent(fullKey)}=`);
947
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
948
+ }
949
+ if (value instanceof Set) {
950
+ const valueAsArray = Array.from(value);
951
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
952
+ }
953
+ if (value instanceof Date) {
954
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
955
+ }
956
+ if (value instanceof Object) {
957
+ return querystring(value, fullKey);
958
+ }
959
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
960
+ }
961
+ function mapValues(data, fn) {
962
+ return Object.keys(data).reduce(
963
+ (acc, key) => ({ ...acc, [key]: fn(data[key]) }),
964
+ {}
965
+ );
966
+ }
967
+
968
+ // src/types/api/models/CompanyCreditBalance.ts
969
+ function CompanyCreditBalanceFromJSON(json) {
970
+ return CompanyCreditBalanceFromJSONTyped(json, false);
971
+ }
972
+ function CompanyCreditBalanceFromJSONTyped(json, ignoreDiscriminator) {
973
+ if (json == null) {
974
+ return json;
975
+ }
976
+ return {
977
+ remaining: json["remaining"],
978
+ reserved: json["reserved"],
979
+ settled: json["settled"]
980
+ };
981
+ }
982
+
696
983
  // src/types/api/models/TrialStatus.ts
697
984
  var TrialStatus = {
698
985
  Active: "active",
@@ -731,6 +1018,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
731
1018
  return json;
732
1019
  }
733
1020
  return {
1021
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
734
1022
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
735
1023
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
736
1024
  };
@@ -819,7 +1107,10 @@ var CheckFlagReturnFromJSON = (json) => {
819
1107
  return {
820
1108
  featureUsageExceeded,
821
1109
  companyId: companyId == null ? void 0 : companyId,
1110
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
822
1111
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1112
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1113
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
823
1114
  error: error == null ? void 0 : error,
824
1115
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
825
1116
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -845,6 +1136,24 @@ var CheckPlanReturnFromJSON = (json) => {
845
1136
  trialStatus: trialStatus == null ? void 0 : trialStatus
846
1137
  };
847
1138
  };
1139
+ var CreditBalancesFromJSON = (json) => {
1140
+ if (json == null || typeof json !== "object") {
1141
+ return {};
1142
+ }
1143
+ const balances = {};
1144
+ for (const [creditId, raw] of Object.entries(json)) {
1145
+ if (raw == null || typeof raw !== "object") continue;
1146
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1147
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1148
+ continue;
1149
+ }
1150
+ balances[creditId] = { remaining, reserved, settled };
1151
+ }
1152
+ return balances;
1153
+ };
1154
+
1155
+ // src/cacheVersion.ts
1156
+ var cacheVersion = "bb56e75d";
848
1157
 
849
1158
  // src/utils.ts
850
1159
  function contextString(context) {
@@ -863,10 +1172,14 @@ function contextString(context) {
863
1172
  }
864
1173
 
865
1174
  // src/version.ts
866
- var version = "1.4.0";
1175
+ var version = "1.5.0";
867
1176
 
868
1177
  // src/index.ts
869
1178
  var anonymousIdKey = "schematicId";
1179
+ var flagStateCachePrefix = "schematicCache";
1180
+ var flagStateCacheMaxContexts = 10;
1181
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1182
+ var emptyCreditBalances = Object.freeze({});
870
1183
  var Schematic = class {
871
1184
  additionalHeaders = {};
872
1185
  apiKey;
@@ -883,11 +1196,17 @@ var Schematic = class {
883
1196
  isPending = true;
884
1197
  isPendingListeners = /* @__PURE__ */ new Set();
885
1198
  planListeners = /* @__PURE__ */ new Set();
1199
+ creditBalanceListeners = /* @__PURE__ */ new Set();
886
1200
  storage;
1201
+ persistFlagState = true;
1202
+ flagStateCacheKey;
1203
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1204
+ cachedFlagState = null;
887
1205
  useWebSocket = false;
888
1206
  checks = {};
889
1207
  featureUsageEventMap = {};
890
1208
  planChecks = {};
1209
+ creditBalances = {};
891
1210
  webSocketUrl = "wss://api.schematichq.com";
892
1211
  webSocketConnectionTimeout = 1e4;
893
1212
  webSocketReconnect = true;
@@ -916,11 +1235,16 @@ var Schematic = class {
916
1235
  fallbackCheckCache = {};
917
1236
  constructor(apiKey, options) {
918
1237
  this.apiKey = apiKey;
1238
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
919
1239
  this.eventQueue = [];
920
1240
  this.contextDependentEventQueue = [];
921
1241
  this.useWebSocket = options?.useWebSocket ?? false;
922
1242
  this.debugEnabled = options?.debug ?? false;
923
1243
  this.offlineEnabled = options?.offline ?? false;
1244
+ this.persistFlagState = options?.persistFlagState ?? true;
1245
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1246
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1247
+ }
924
1248
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
925
1249
  const params = new URLSearchParams(window.location.search);
926
1250
  const debugParam = params.get("schematic_debug");
@@ -953,6 +1277,7 @@ var Schematic = class {
953
1277
  } catch {
954
1278
  }
955
1279
  }
1280
+ this.hydrateFlagStateFromCache();
956
1281
  if (options?.apiUrl !== void 0) {
957
1282
  this.apiUrl = options.apiUrl;
958
1283
  }
@@ -1246,6 +1571,29 @@ var Schematic = class {
1246
1571
  });
1247
1572
  this.notifyPlanListeners(plan);
1248
1573
  }
1574
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1575
+ const contextStr = contextString(context);
1576
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1577
+ const previous = this.creditBalances[contextStr] ?? {};
1578
+ let changed = false;
1579
+ const merged = { ...previous };
1580
+ for (const [creditId, next] of Object.entries(updates)) {
1581
+ const current = previous[creditId];
1582
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1583
+ merged[creditId] = next;
1584
+ changed = true;
1585
+ }
1586
+ }
1587
+ if (changed) {
1588
+ this.creditBalances[contextStr] = merged;
1589
+ this.debug(
1590
+ `WebSocket credit balance update received. Notifying listeners`,
1591
+ { creditBalances: merged }
1592
+ );
1593
+ this.notifyCreditBalanceListeners(merged);
1594
+ }
1595
+ }
1596
+ this.persistContextToCache(contextString(context));
1249
1597
  this.flushContextDependentEventQueue();
1250
1598
  this.setIsPending(false);
1251
1599
  };
@@ -1353,7 +1701,32 @@ var Schematic = class {
1353
1701
  return;
1354
1702
  }
1355
1703
  try {
1356
- this.setIsPending(true);
1704
+ const newContextStr = contextString(context);
1705
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1706
+ if (cacheHit) {
1707
+ this.debug(
1708
+ `setContext: cache hit for context, pre-resolving from cached values`
1709
+ );
1710
+ this.context = context;
1711
+ this.flushContextDependentEventQueue();
1712
+ this.setIsPending(false);
1713
+ const cachedChecks = this.checks[newContextStr] ?? {};
1714
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1715
+ if (check === void 0) continue;
1716
+ this.notifyFlagCheckListeners(flagKey, check);
1717
+ this.notifyFlagValueListeners(flagKey, check.value);
1718
+ }
1719
+ const cachedPlan = this.planChecks[newContextStr];
1720
+ if (cachedPlan !== void 0) {
1721
+ this.notifyPlanListeners(cachedPlan);
1722
+ }
1723
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1724
+ if (cachedCreditBalances !== void 0) {
1725
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1726
+ }
1727
+ } else {
1728
+ this.setIsPending(true);
1729
+ }
1357
1730
  if (!this.conn) {
1358
1731
  if (this.isConnecting) {
1359
1732
  this.debug(
@@ -1364,7 +1737,7 @@ var Schematic = class {
1364
1737
  }
1365
1738
  if (this.conn !== null) {
1366
1739
  const socket2 = await this.conn;
1367
- await this.wsSendMessage(socket2, context);
1740
+ await this.wsSendMessage(socket2, context, cacheHit);
1368
1741
  return;
1369
1742
  }
1370
1743
  }
@@ -1380,7 +1753,7 @@ var Schematic = class {
1380
1753
  this.conn = this.wsConnect();
1381
1754
  const socket2 = await this.conn;
1382
1755
  this.isConnecting = false;
1383
- await this.wsSendMessage(socket2, context);
1756
+ await this.wsSendMessage(socket2, context, cacheHit);
1384
1757
  return;
1385
1758
  } catch (error) {
1386
1759
  this.isConnecting = false;
@@ -1388,7 +1761,7 @@ var Schematic = class {
1388
1761
  }
1389
1762
  }
1390
1763
  const socket = await this.conn;
1391
- await this.wsSendMessage(socket, context);
1764
+ await this.wsSendMessage(socket, context, cacheHit);
1392
1765
  } catch (error) {
1393
1766
  console.warn("Failed to establish WebSocket connection:", error);
1394
1767
  this.context = context;
@@ -1448,10 +1821,12 @@ var Schematic = class {
1448
1821
  `Optimistically updating feature usage for event: ${eventName}`,
1449
1822
  { quantity }
1450
1823
  );
1824
+ let dirty = false;
1451
1825
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1452
1826
  if (check === void 0) return;
1453
1827
  const updatedCheck = { ...check };
1454
1828
  if (typeof updatedCheck.featureUsage === "number") {
1829
+ dirty = true;
1455
1830
  updatedCheck.featureUsage += quantity;
1456
1831
  if (typeof updatedCheck.featureAllocation === "number") {
1457
1832
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1481,6 +1856,9 @@ var Schematic = class {
1481
1856
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1482
1857
  }
1483
1858
  });
1859
+ if (dirty) {
1860
+ this.persistContextToCache(contextString(this.context));
1861
+ }
1484
1862
  };
1485
1863
  /**
1486
1864
  * Event processing
@@ -1583,6 +1961,206 @@ var Schematic = class {
1583
1961
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1584
1962
  return generatedAnonymousId;
1585
1963
  };
1964
+ hasCachedValuesForContext = (contextStr) => {
1965
+ const cachedChecks = this.checks[contextStr];
1966
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1967
+ return true;
1968
+ }
1969
+ if (this.planChecks[contextStr] !== void 0) {
1970
+ return true;
1971
+ }
1972
+ const cachedCreditBalances = this.creditBalances[contextStr];
1973
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1974
+ };
1975
+ readFlagStateCache = () => {
1976
+ if (!this.persistFlagState || this.storage === void 0) {
1977
+ return null;
1978
+ }
1979
+ let raw;
1980
+ try {
1981
+ raw = this.storage.getItem(this.flagStateCacheKey);
1982
+ } catch (error) {
1983
+ this.debug("Failed to read flag state cache:", error);
1984
+ return null;
1985
+ }
1986
+ if (raw === null || raw === void 0) {
1987
+ return null;
1988
+ }
1989
+ let parsed;
1990
+ try {
1991
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1992
+ } catch (error) {
1993
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
1994
+ return null;
1995
+ }
1996
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
1997
+ this.debug(
1998
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
1999
+ {
2000
+ parsedType: typeof parsed,
2001
+ version: parsed?.version
2002
+ }
2003
+ );
2004
+ return null;
2005
+ }
2006
+ return parsed;
2007
+ };
2008
+ reviveCachedCheck = (raw) => {
2009
+ if (raw === null || typeof raw !== "object") return null;
2010
+ const obj = raw;
2011
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
2012
+ return null;
2013
+ }
2014
+ const revived = { ...obj };
2015
+ if (typeof revived.featureUsageResetAt === "string") {
2016
+ const d = new Date(revived.featureUsageResetAt);
2017
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
2018
+ }
2019
+ return revived;
2020
+ };
2021
+ reviveCachedPlan = (raw) => {
2022
+ if (raw === null || typeof raw !== "object") return null;
2023
+ const obj = raw;
2024
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
2025
+ return null;
2026
+ }
2027
+ const revived = { ...obj };
2028
+ if (typeof revived.trialEndDate === "string") {
2029
+ const d = new Date(revived.trialEndDate);
2030
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
2031
+ }
2032
+ return revived;
2033
+ };
2034
+ reviveCachedCreditBalances = (raw) => {
2035
+ if (raw === null || typeof raw !== "object") return null;
2036
+ const revived = {};
2037
+ for (const [creditId, value] of Object.entries(
2038
+ raw
2039
+ )) {
2040
+ if (value === null || typeof value !== "object") continue;
2041
+ const obj = value;
2042
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
2043
+ continue;
2044
+ }
2045
+ revived[creditId] = {
2046
+ remaining: obj.remaining,
2047
+ reserved: obj.reserved,
2048
+ settled: obj.settled
2049
+ };
2050
+ }
2051
+ return Object.keys(revived).length > 0 ? revived : null;
2052
+ };
2053
+ hydrateFlagStateFromCache = () => {
2054
+ const parsed = this.readFlagStateCache();
2055
+ if (parsed === null) return;
2056
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2057
+ const fresh = {};
2058
+ let contextsLoaded = 0;
2059
+ let contextsExpired = 0;
2060
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2061
+ if (entry === null || typeof entry !== "object") continue;
2062
+ if (typeof entry.updatedAt !== "number") continue;
2063
+ if (entry.updatedAt < cutoff) {
2064
+ contextsExpired += 1;
2065
+ continue;
2066
+ }
2067
+ const revivedChecks = {};
2068
+ if (entry.checks !== null && typeof entry.checks === "object") {
2069
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2070
+ const revived = this.reviveCachedCheck(rawCheck);
2071
+ if (revived === null || revived.flag !== flagKey) continue;
2072
+ revivedChecks[flagKey] = revived;
2073
+ if (typeof revived.featureUsageEvent === "string") {
2074
+ this.updateFeatureUsageEventMap(revived);
2075
+ }
2076
+ }
2077
+ if (Object.keys(revivedChecks).length > 0) {
2078
+ this.checks[contextStr] = revivedChecks;
2079
+ }
2080
+ }
2081
+ let revivedPlan;
2082
+ if (entry.plan !== void 0 && entry.plan !== null) {
2083
+ const p = this.reviveCachedPlan(entry.plan);
2084
+ if (p !== null) {
2085
+ revivedPlan = p;
2086
+ this.planChecks[contextStr] = p;
2087
+ }
2088
+ }
2089
+ let revivedCreditBalances;
2090
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2091
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2092
+ if (balances !== null) {
2093
+ revivedCreditBalances = balances;
2094
+ this.creditBalances[contextStr] = balances;
2095
+ }
2096
+ }
2097
+ fresh[contextStr] = {
2098
+ checks: revivedChecks,
2099
+ plan: revivedPlan,
2100
+ creditBalances: revivedCreditBalances,
2101
+ updatedAt: entry.updatedAt
2102
+ };
2103
+ contextsLoaded += 1;
2104
+ }
2105
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2106
+ this.debug(
2107
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2108
+ );
2109
+ };
2110
+ persistContextToCache = (contextStr) => {
2111
+ const checksForContext = this.checks[contextStr];
2112
+ const planForContext = this.planChecks[contextStr];
2113
+ const creditBalancesForContext = this.creditBalances[contextStr];
2114
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2115
+ return;
2116
+ }
2117
+ if (this.cachedFlagState === null) {
2118
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2119
+ version: cacheVersion,
2120
+ contexts: {}
2121
+ };
2122
+ }
2123
+ const cleanChecks = {};
2124
+ if (checksForContext !== void 0) {
2125
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2126
+ if (check !== void 0) cleanChecks[flagKey] = check;
2127
+ }
2128
+ }
2129
+ this.cachedFlagState.contexts[contextStr] = {
2130
+ checks: cleanChecks,
2131
+ plan: planForContext,
2132
+ creditBalances: creditBalancesForContext,
2133
+ updatedAt: Date.now()
2134
+ };
2135
+ const entries = Object.entries(this.cachedFlagState.contexts);
2136
+ if (entries.length > flagStateCacheMaxContexts) {
2137
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2138
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2139
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2140
+ for (const key of Object.keys(this.checks)) {
2141
+ if (!survivorKeys.has(key)) delete this.checks[key];
2142
+ }
2143
+ for (const key of Object.keys(this.planChecks)) {
2144
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2145
+ }
2146
+ for (const key of Object.keys(this.creditBalances)) {
2147
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2148
+ }
2149
+ this.cachedFlagState = {
2150
+ version: cacheVersion,
2151
+ contexts: Object.fromEntries(survivors)
2152
+ };
2153
+ }
2154
+ if (!this.persistFlagState || this.storage === void 0) return;
2155
+ try {
2156
+ this.storage.setItem(
2157
+ this.flagStateCacheKey,
2158
+ JSON.stringify(this.cachedFlagState)
2159
+ );
2160
+ } catch (error) {
2161
+ this.debug("Failed to write flag state cache:", error);
2162
+ }
2163
+ };
1586
2164
  handleEvent = (eventType, eventBody) => {
1587
2165
  const event = {
1588
2166
  api_key: this.apiKey,
@@ -1843,6 +2421,9 @@ var Schematic = class {
1843
2421
  if (this.conn !== null) {
1844
2422
  try {
1845
2423
  const socket = await this.conn;
2424
+ if (this.currentWebSocket === socket) {
2425
+ this.currentWebSocket = null;
2426
+ }
1846
2427
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1847
2428
  socket.close();
1848
2429
  }
@@ -2008,6 +2589,7 @@ var Schematic = class {
2008
2589
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2009
2590
  let timeoutId = null;
2010
2591
  let isResolved = false;
2592
+ let didOpen = false;
2011
2593
  timeoutId = setTimeout(() => {
2012
2594
  if (!isResolved) {
2013
2595
  isResolved = true;
@@ -2021,6 +2603,7 @@ var Schematic = class {
2021
2603
  webSocket.onopen = () => {
2022
2604
  if (isResolved) return;
2023
2605
  isResolved = true;
2606
+ didOpen = true;
2024
2607
  if (timeoutId !== null) {
2025
2608
  clearTimeout(timeoutId);
2026
2609
  }
@@ -2046,11 +2629,12 @@ var Schematic = class {
2046
2629
  }
2047
2630
  this.debug(`WebSocket connection ${connectionId} closed`);
2048
2631
  this.conn = null;
2632
+ const wasCurrent = this.currentWebSocket === webSocket;
2049
2633
  if (this.currentWebSocket === webSocket) {
2050
2634
  this.currentWebSocket = null;
2051
2635
  this.isConnecting = false;
2052
2636
  }
2053
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2637
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2054
2638
  this.attemptReconnect();
2055
2639
  }
2056
2640
  };
@@ -2146,6 +2730,16 @@ var Schematic = class {
2146
2730
  const plan = this.planChecks[contextStr];
2147
2731
  return plan;
2148
2732
  };
2733
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2734
+ getCreditBalances = () => {
2735
+ const contextStr = contextString(this.context);
2736
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2737
+ };
2738
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2739
+ getCreditBalance = (creditId) => {
2740
+ const contextStr = contextString(this.context);
2741
+ return this.creditBalances[contextStr]?.[creditId];
2742
+ };
2149
2743
  // flag checks state
2150
2744
  getFlagCheck = (flagKey) => {
2151
2745
  const contextStr = contextString(this.context);
@@ -2205,6 +2799,13 @@ var Schematic = class {
2205
2799
  this.planListeners.delete(listener);
2206
2800
  };
2207
2801
  };
2802
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2803
+ addCreditBalanceListener = (listener) => {
2804
+ this.creditBalanceListeners.add(listener);
2805
+ return () => {
2806
+ this.creditBalanceListeners.delete(listener);
2807
+ };
2808
+ };
2208
2809
  notifyFlagCheckListeners = (flagKey, check) => {
2209
2810
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2210
2811
  if (listeners.size > 0) {
@@ -2268,6 +2869,17 @@ var Schematic = class {
2268
2869
  });
2269
2870
  });
2270
2871
  };
2872
+ notifyCreditBalanceListeners = (value) => {
2873
+ const listeners = this.creditBalanceListeners ?? [];
2874
+ if (listeners.size > 0) {
2875
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2876
+ value
2877
+ });
2878
+ }
2879
+ listeners.forEach(
2880
+ (listener) => notifyCreditBalanceListener(listener, value)
2881
+ );
2882
+ };
2271
2883
  };
2272
2884
  var notifyPendingListener = (listener, value) => {
2273
2885
  if (listener.length > 0) {
@@ -2297,11 +2909,19 @@ var notifyPlanListener = (listener, value) => {
2297
2909
  listener();
2298
2910
  }
2299
2911
  };
2912
+ var notifyCreditBalanceListener = (listener, value) => {
2913
+ if (listener.length > 0) {
2914
+ listener(value);
2915
+ } else {
2916
+ listener();
2917
+ }
2918
+ };
2300
2919
  export {
2301
2920
  CheckFlagResponseFromJSON,
2302
2921
  CheckFlagReturnFromJSON,
2303
2922
  CheckFlagsResponseFromJSON,
2304
2923
  CheckPlanReturnFromJSON,
2924
+ CreditBalancesFromJSON,
2305
2925
  DatastreamCompanyPlanFromJSON,
2306
2926
  EventBodyFlagCheckToJSON,
2307
2927
  RuleType,