@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.
@@ -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 __export = (target, all) => {
12
16
  for (var name in all)
@@ -573,6 +577,7 @@ __export(index_exports, {
573
577
  CheckFlagReturnFromJSON: () => CheckFlagReturnFromJSON,
574
578
  CheckFlagsResponseFromJSON: () => CheckFlagsResponseFromJSON,
575
579
  CheckPlanReturnFromJSON: () => CheckPlanReturnFromJSON,
580
+ CreditBalancesFromJSON: () => CreditBalancesFromJSON,
576
581
  DatastreamCompanyPlanFromJSON: () => DatastreamCompanyPlanFromJSON,
577
582
  EventBodyFlagCheckToJSON: () => EventBodyFlagCheckToJSON,
578
583
  RuleType: () => RuleType,
@@ -592,23 +597,18 @@ function unsafeStringify(arr, offset = 0) {
592
597
  }
593
598
 
594
599
  // node_modules/uuid/dist/rng.js
595
- var getRandomValues;
596
600
  var rnds8 = new Uint8Array(16);
597
601
  function rng() {
598
- if (!getRandomValues) {
599
- if (typeof crypto === "undefined" || !crypto.getRandomValues) {
600
- throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
601
- }
602
- getRandomValues = crypto.getRandomValues.bind(crypto);
603
- }
604
- return getRandomValues(rnds8);
602
+ return crypto.getRandomValues(rnds8);
605
603
  }
606
604
 
607
- // node_modules/uuid/dist/native.js
608
- var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
609
- var native_default = { randomUUID };
610
-
611
605
  // node_modules/uuid/dist/v4.js
606
+ function v4(options, buf, offset) {
607
+ if (!buf && !options && crypto.randomUUID) {
608
+ return crypto.randomUUID();
609
+ }
610
+ return _v4(options, buf, offset);
611
+ }
612
612
  function _v4(options, buf, offset) {
613
613
  options = options || {};
614
614
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -629,12 +629,6 @@ function _v4(options, buf, offset) {
629
629
  }
630
630
  return unsafeStringify(rnds);
631
631
  }
632
- function v4(options, buf, offset) {
633
- if (native_default.randomUUID && !buf && !options) {
634
- return native_default.randomUUID();
635
- }
636
- return _v4(options, buf, offset);
637
- }
638
632
  var v4_default = v4;
639
633
 
640
634
  // src/index.ts
@@ -648,6 +642,22 @@ function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
648
642
  return json;
649
643
  }
650
644
 
645
+ // src/types/api/models/MetricPeriodMonthReset.ts
646
+ function MetricPeriodMonthResetFromJSON(json) {
647
+ return MetricPeriodMonthResetFromJSONTyped(json, false);
648
+ }
649
+ function MetricPeriodMonthResetFromJSONTyped(json, ignoreDiscriminator) {
650
+ return json;
651
+ }
652
+
653
+ // src/types/api/models/MetricPeriod.ts
654
+ function MetricPeriodFromJSON(json) {
655
+ return MetricPeriodFromJSONTyped(json, false);
656
+ }
657
+ function MetricPeriodFromJSONTyped(json, ignoreDiscriminator) {
658
+ return json;
659
+ }
660
+
651
661
  // src/types/api/models/FeatureEntitlement.ts
652
662
  function FeatureEntitlementFromJSON(json) {
653
663
  return FeatureEntitlementFromJSONTyped(json, false);
@@ -658,22 +668,34 @@ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
658
668
  }
659
669
  return {
660
670
  allocation: json["allocation"] == null ? void 0 : json["allocation"],
671
+ consumptionRate: json["consumption_rate"] == null ? void 0 : json["consumption_rate"],
661
672
  creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
662
673
  creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
674
+ creditReserved: json["credit_reserved"] == null ? void 0 : json["credit_reserved"],
675
+ creditSettled: json["credit_settled"] == null ? void 0 : json["credit_settled"],
663
676
  creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
664
677
  creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
665
678
  eventName: json["event_name"] == null ? void 0 : json["event_name"],
679
+ eventSubtype: json["event_subtype"] == null ? void 0 : json["event_subtype"],
666
680
  featureId: json["feature_id"],
667
681
  featureKey: json["feature_key"],
668
- metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
682
+ metricPeriod: json["metric_period"] == null ? void 0 : MetricPeriodFromJSON(json["metric_period"]),
669
683
  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"],
684
+ monthReset: json["month_reset"] == null ? void 0 : MetricPeriodMonthResetFromJSON(json["month_reset"]),
671
685
  softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
672
686
  usage: json["usage"] == null ? void 0 : json["usage"],
673
687
  valueType: EntitlementValueTypeFromJSON(json["value_type"])
674
688
  };
675
689
  }
676
690
 
691
+ // src/types/api/models/RuleType.ts
692
+ function RuleTypeFromJSON(json) {
693
+ return RuleTypeFromJSONTyped(json, false);
694
+ }
695
+ function RuleTypeFromJSONTyped(json, ignoreDiscriminator) {
696
+ return json;
697
+ }
698
+
677
699
  // src/types/api/models/CheckFlagResponseData.ts
678
700
  function CheckFlagResponseDataFromJSON(json) {
679
701
  return CheckFlagResponseDataFromJSONTyped(json, false);
@@ -689,13 +711,13 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
689
711
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
690
712
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
691
713
  featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
692
- featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
714
+ featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : MetricPeriodFromJSON(json["feature_usage_period"]),
693
715
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
694
716
  flag: json["flag"],
695
717
  flagId: json["flag_id"] == null ? void 0 : json["flag_id"],
696
718
  reason: json["reason"],
697
719
  ruleId: json["rule_id"] == null ? void 0 : json["rule_id"],
698
- ruleType: json["rule_type"] == null ? void 0 : json["rule_type"],
720
+ ruleType: json["rule_type"] == null ? void 0 : RuleTypeFromJSON(json["rule_type"]),
699
721
  userId: json["user_id"] == null ? void 0 : json["user_id"],
700
722
  value: json["value"]
701
723
  };
@@ -715,6 +737,272 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
715
737
  };
716
738
  }
717
739
 
740
+ // src/types/api/runtime.ts
741
+ var BASE_PATH = "https://api.schematichq.com".replace(/\/+$/, "");
742
+ var Configuration = class {
743
+ constructor(configuration = {}) {
744
+ this.configuration = configuration;
745
+ }
746
+ configuration;
747
+ set config(configuration) {
748
+ this.configuration = configuration;
749
+ }
750
+ get basePath() {
751
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
752
+ }
753
+ get fetchApi() {
754
+ return this.configuration.fetchApi;
755
+ }
756
+ get middleware() {
757
+ return this.configuration.middleware || [];
758
+ }
759
+ get queryParamsStringify() {
760
+ return this.configuration.queryParamsStringify || querystring;
761
+ }
762
+ get username() {
763
+ return this.configuration.username;
764
+ }
765
+ get password() {
766
+ return this.configuration.password;
767
+ }
768
+ get apiKey() {
769
+ const apiKey = this.configuration.apiKey;
770
+ if (apiKey) {
771
+ return typeof apiKey === "function" ? apiKey : () => apiKey;
772
+ }
773
+ return void 0;
774
+ }
775
+ get accessToken() {
776
+ const accessToken = this.configuration.accessToken;
777
+ if (accessToken) {
778
+ return typeof accessToken === "function" ? accessToken : async () => accessToken;
779
+ }
780
+ return void 0;
781
+ }
782
+ get headers() {
783
+ return this.configuration.headers;
784
+ }
785
+ get credentials() {
786
+ return this.configuration.credentials;
787
+ }
788
+ };
789
+ var DefaultConfig = new Configuration();
790
+ var BaseAPI = class _BaseAPI {
791
+ constructor(configuration = DefaultConfig) {
792
+ this.configuration = configuration;
793
+ this.middleware = configuration.middleware;
794
+ }
795
+ configuration;
796
+ static jsonRegex = new RegExp(
797
+ "^(:?application/json|[^;/ ]+/[^;/ ]+[+]json)[ ]*(:?;.*)?$",
798
+ "i"
799
+ );
800
+ middleware;
801
+ withMiddleware(...middlewares) {
802
+ const next = this.clone();
803
+ next.middleware = next.middleware.concat(...middlewares);
804
+ return next;
805
+ }
806
+ withPreMiddleware(...preMiddlewares) {
807
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
808
+ return this.withMiddleware(...middlewares);
809
+ }
810
+ withPostMiddleware(...postMiddlewares) {
811
+ const middlewares = postMiddlewares.map((post) => ({ post }));
812
+ return this.withMiddleware(...middlewares);
813
+ }
814
+ /**
815
+ * Check if the given MIME is a JSON MIME.
816
+ * JSON MIME examples:
817
+ * application/json
818
+ * application/json; charset=UTF8
819
+ * APPLICATION/JSON
820
+ * application/vnd.company+json
821
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
822
+ * @return True if the given MIME is JSON, false otherwise.
823
+ */
824
+ isJsonMime(mime) {
825
+ if (!mime) {
826
+ return false;
827
+ }
828
+ return _BaseAPI.jsonRegex.test(mime);
829
+ }
830
+ async request(context, initOverrides) {
831
+ const { url, init } = await this.createFetchParams(context, initOverrides);
832
+ const response = await this.fetchApi(url, init);
833
+ if (response && response.status >= 200 && response.status < 300) {
834
+ return response;
835
+ }
836
+ throw new ResponseError(response, "Response returned an error code");
837
+ }
838
+ async createFetchParams(context, initOverrides) {
839
+ let url = this.configuration.basePath + context.path;
840
+ if (context.query !== void 0 && Object.keys(context.query).length !== 0) {
841
+ url += "?" + this.configuration.queryParamsStringify(context.query);
842
+ }
843
+ const headers = Object.assign(
844
+ {},
845
+ this.configuration.headers,
846
+ context.headers
847
+ );
848
+ Object.keys(headers).forEach(
849
+ (key) => headers[key] === void 0 ? delete headers[key] : {}
850
+ );
851
+ const initOverrideFn = typeof initOverrides === "function" ? initOverrides : async () => initOverrides;
852
+ const initParams = {
853
+ method: context.method,
854
+ headers,
855
+ body: context.body,
856
+ credentials: this.configuration.credentials
857
+ };
858
+ const overriddenInit = {
859
+ ...initParams,
860
+ ...await initOverrideFn({
861
+ init: initParams,
862
+ context
863
+ })
864
+ };
865
+ let body;
866
+ if (isFormData(overriddenInit.body) || overriddenInit.body instanceof URLSearchParams || isBlob(overriddenInit.body)) {
867
+ body = overriddenInit.body;
868
+ } else if (this.isJsonMime(headers["Content-Type"])) {
869
+ body = JSON.stringify(overriddenInit.body);
870
+ } else {
871
+ body = overriddenInit.body;
872
+ }
873
+ const init = {
874
+ ...overriddenInit,
875
+ body
876
+ };
877
+ return { url, init };
878
+ }
879
+ fetchApi = async (url, init) => {
880
+ let fetchParams = { url, init };
881
+ for (const middleware of this.middleware) {
882
+ if (middleware.pre) {
883
+ fetchParams = await middleware.pre({
884
+ fetch: this.fetchApi,
885
+ ...fetchParams
886
+ }) || fetchParams;
887
+ }
888
+ }
889
+ let response = void 0;
890
+ try {
891
+ response = await (this.configuration.fetchApi || fetch)(
892
+ fetchParams.url,
893
+ fetchParams.init
894
+ );
895
+ } catch (e) {
896
+ for (const middleware of this.middleware) {
897
+ if (middleware.onError) {
898
+ response = await middleware.onError({
899
+ fetch: this.fetchApi,
900
+ url: fetchParams.url,
901
+ init: fetchParams.init,
902
+ error: e,
903
+ response: response ? response.clone() : void 0
904
+ }) || response;
905
+ }
906
+ }
907
+ if (response === void 0) {
908
+ if (e instanceof Error) {
909
+ throw new FetchError(
910
+ e,
911
+ "The request failed and the interceptors did not return an alternative response"
912
+ );
913
+ } else {
914
+ throw e;
915
+ }
916
+ }
917
+ }
918
+ for (const middleware of this.middleware) {
919
+ if (middleware.post) {
920
+ response = await middleware.post({
921
+ fetch: this.fetchApi,
922
+ url: fetchParams.url,
923
+ init: fetchParams.init,
924
+ response: response.clone()
925
+ }) || response;
926
+ }
927
+ }
928
+ return response;
929
+ };
930
+ /**
931
+ * Create a shallow clone of `this` by constructing a new instance
932
+ * and then shallow cloning data members.
933
+ */
934
+ clone() {
935
+ const constructor = this.constructor;
936
+ const next = new constructor(this.configuration);
937
+ next.middleware = this.middleware.slice();
938
+ return next;
939
+ }
940
+ };
941
+ function isBlob(value) {
942
+ return typeof Blob !== "undefined" && value instanceof Blob;
943
+ }
944
+ function isFormData(value) {
945
+ return typeof FormData !== "undefined" && value instanceof FormData;
946
+ }
947
+ var ResponseError = class extends Error {
948
+ constructor(response, msg) {
949
+ super(msg);
950
+ this.response = response;
951
+ }
952
+ response;
953
+ name = "ResponseError";
954
+ };
955
+ var FetchError = class extends Error {
956
+ constructor(cause, msg) {
957
+ super(msg);
958
+ this.cause = cause;
959
+ }
960
+ cause;
961
+ name = "FetchError";
962
+ };
963
+ function querystring(params, prefix = "") {
964
+ return Object.keys(params).map((key) => querystringSingleKey(key, params[key], prefix)).filter((part) => part.length > 0).join("&");
965
+ }
966
+ function querystringSingleKey(key, value, keyPrefix = "") {
967
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
968
+ if (value instanceof Array) {
969
+ const multiValue = value.map((singleValue) => encodeURIComponent(String(singleValue))).join(`&${encodeURIComponent(fullKey)}=`);
970
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
971
+ }
972
+ if (value instanceof Set) {
973
+ const valueAsArray = Array.from(value);
974
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
975
+ }
976
+ if (value instanceof Date) {
977
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
978
+ }
979
+ if (value instanceof Object) {
980
+ return querystring(value, fullKey);
981
+ }
982
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
983
+ }
984
+ function mapValues(data, fn) {
985
+ return Object.keys(data).reduce(
986
+ (acc, key) => ({ ...acc, [key]: fn(data[key]) }),
987
+ {}
988
+ );
989
+ }
990
+
991
+ // src/types/api/models/CompanyCreditBalance.ts
992
+ function CompanyCreditBalanceFromJSON(json) {
993
+ return CompanyCreditBalanceFromJSONTyped(json, false);
994
+ }
995
+ function CompanyCreditBalanceFromJSONTyped(json, ignoreDiscriminator) {
996
+ if (json == null) {
997
+ return json;
998
+ }
999
+ return {
1000
+ remaining: json["remaining"],
1001
+ reserved: json["reserved"],
1002
+ settled: json["settled"]
1003
+ };
1004
+ }
1005
+
718
1006
  // src/types/api/models/TrialStatus.ts
719
1007
  var TrialStatus = {
720
1008
  Active: "active",
@@ -753,6 +1041,7 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
753
1041
  return json;
754
1042
  }
755
1043
  return {
1044
+ creditBalances: json["credit_balances"] == null ? void 0 : mapValues(json["credit_balances"], CompanyCreditBalanceFromJSON),
756
1045
  flags: json["flags"].map(CheckFlagResponseDataFromJSON),
757
1046
  plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
758
1047
  };
@@ -841,7 +1130,10 @@ var CheckFlagReturnFromJSON = (json) => {
841
1130
  return {
842
1131
  featureUsageExceeded,
843
1132
  companyId: companyId == null ? void 0 : companyId,
1133
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
844
1134
  creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
1135
+ creditReserved: entitlement?.creditReserved == null ? void 0 : entitlement.creditReserved,
1136
+ creditSettled: entitlement?.creditSettled == null ? void 0 : entitlement.creditSettled,
845
1137
  error: error == null ? void 0 : error,
846
1138
  featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
847
1139
  featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
@@ -867,6 +1159,24 @@ var CheckPlanReturnFromJSON = (json) => {
867
1159
  trialStatus: trialStatus == null ? void 0 : trialStatus
868
1160
  };
869
1161
  };
1162
+ var CreditBalancesFromJSON = (json) => {
1163
+ if (json == null || typeof json !== "object") {
1164
+ return {};
1165
+ }
1166
+ const balances = {};
1167
+ for (const [creditId, raw] of Object.entries(json)) {
1168
+ if (raw == null || typeof raw !== "object") continue;
1169
+ const { remaining, reserved, settled } = CompanyCreditBalanceFromJSON(raw);
1170
+ if (typeof remaining !== "number" || typeof reserved !== "number" || typeof settled !== "number") {
1171
+ continue;
1172
+ }
1173
+ balances[creditId] = { remaining, reserved, settled };
1174
+ }
1175
+ return balances;
1176
+ };
1177
+
1178
+ // src/cacheVersion.ts
1179
+ var cacheVersion = "bb56e75d";
870
1180
 
871
1181
  // src/utils.ts
872
1182
  function contextString(context) {
@@ -885,10 +1195,14 @@ function contextString(context) {
885
1195
  }
886
1196
 
887
1197
  // src/version.ts
888
- var version = "1.4.0";
1198
+ var version = "1.5.0";
889
1199
 
890
1200
  // src/index.ts
891
1201
  var anonymousIdKey = "schematicId";
1202
+ var flagStateCachePrefix = "schematicCache";
1203
+ var flagStateCacheMaxContexts = 10;
1204
+ var flagStateCacheDefaultMaxAgeMs = 7 * 24 * 60 * 60 * 1e3;
1205
+ var emptyCreditBalances = Object.freeze({});
892
1206
  var Schematic = class {
893
1207
  additionalHeaders = {};
894
1208
  apiKey;
@@ -905,11 +1219,17 @@ var Schematic = class {
905
1219
  isPending = true;
906
1220
  isPendingListeners = /* @__PURE__ */ new Set();
907
1221
  planListeners = /* @__PURE__ */ new Set();
1222
+ creditBalanceListeners = /* @__PURE__ */ new Set();
908
1223
  storage;
1224
+ persistFlagState = true;
1225
+ flagStateCacheKey;
1226
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1227
+ cachedFlagState = null;
909
1228
  useWebSocket = false;
910
1229
  checks = {};
911
1230
  featureUsageEventMap = {};
912
1231
  planChecks = {};
1232
+ creditBalances = {};
913
1233
  webSocketUrl = "wss://api.schematichq.com";
914
1234
  webSocketConnectionTimeout = 1e4;
915
1235
  webSocketReconnect = true;
@@ -938,11 +1258,16 @@ var Schematic = class {
938
1258
  fallbackCheckCache = {};
939
1259
  constructor(apiKey, options) {
940
1260
  this.apiKey = apiKey;
1261
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
941
1262
  this.eventQueue = [];
942
1263
  this.contextDependentEventQueue = [];
943
1264
  this.useWebSocket = options?.useWebSocket ?? false;
944
1265
  this.debugEnabled = options?.debug ?? false;
945
1266
  this.offlineEnabled = options?.offline ?? false;
1267
+ this.persistFlagState = options?.persistFlagState ?? true;
1268
+ if (typeof options?.flagStateCacheMaxAgeMs === "number" && options.flagStateCacheMaxAgeMs > 0) {
1269
+ this.flagStateCacheMaxAgeMs = options.flagStateCacheMaxAgeMs;
1270
+ }
946
1271
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
947
1272
  const params = new URLSearchParams(window.location.search);
948
1273
  const debugParam = params.get("schematic_debug");
@@ -975,6 +1300,7 @@ var Schematic = class {
975
1300
  } catch {
976
1301
  }
977
1302
  }
1303
+ this.hydrateFlagStateFromCache();
978
1304
  if (options?.apiUrl !== void 0) {
979
1305
  this.apiUrl = options.apiUrl;
980
1306
  }
@@ -1268,6 +1594,29 @@ var Schematic = class {
1268
1594
  });
1269
1595
  this.notifyPlanListeners(plan);
1270
1596
  }
1597
+ if (message.credit_balances !== void 0 && message.credit_balances !== null) {
1598
+ const contextStr = contextString(context);
1599
+ const updates = CreditBalancesFromJSON(message.credit_balances);
1600
+ const previous = this.creditBalances[contextStr] ?? {};
1601
+ let changed = false;
1602
+ const merged = { ...previous };
1603
+ for (const [creditId, next] of Object.entries(updates)) {
1604
+ const current = previous[creditId];
1605
+ if (current === void 0 || current.remaining !== next.remaining || current.reserved !== next.reserved || current.settled !== next.settled) {
1606
+ merged[creditId] = next;
1607
+ changed = true;
1608
+ }
1609
+ }
1610
+ if (changed) {
1611
+ this.creditBalances[contextStr] = merged;
1612
+ this.debug(
1613
+ `WebSocket credit balance update received. Notifying listeners`,
1614
+ { creditBalances: merged }
1615
+ );
1616
+ this.notifyCreditBalanceListeners(merged);
1617
+ }
1618
+ }
1619
+ this.persistContextToCache(contextString(context));
1271
1620
  this.flushContextDependentEventQueue();
1272
1621
  this.setIsPending(false);
1273
1622
  };
@@ -1375,7 +1724,32 @@ var Schematic = class {
1375
1724
  return;
1376
1725
  }
1377
1726
  try {
1378
- this.setIsPending(true);
1727
+ const newContextStr = contextString(context);
1728
+ const cacheHit = this.hasCachedValuesForContext(newContextStr);
1729
+ if (cacheHit) {
1730
+ this.debug(
1731
+ `setContext: cache hit for context, pre-resolving from cached values`
1732
+ );
1733
+ this.context = context;
1734
+ this.flushContextDependentEventQueue();
1735
+ this.setIsPending(false);
1736
+ const cachedChecks = this.checks[newContextStr] ?? {};
1737
+ for (const [flagKey, check] of Object.entries(cachedChecks)) {
1738
+ if (check === void 0) continue;
1739
+ this.notifyFlagCheckListeners(flagKey, check);
1740
+ this.notifyFlagValueListeners(flagKey, check.value);
1741
+ }
1742
+ const cachedPlan = this.planChecks[newContextStr];
1743
+ if (cachedPlan !== void 0) {
1744
+ this.notifyPlanListeners(cachedPlan);
1745
+ }
1746
+ const cachedCreditBalances = this.creditBalances[newContextStr];
1747
+ if (cachedCreditBalances !== void 0) {
1748
+ this.notifyCreditBalanceListeners(cachedCreditBalances);
1749
+ }
1750
+ } else {
1751
+ this.setIsPending(true);
1752
+ }
1379
1753
  if (!this.conn) {
1380
1754
  if (this.isConnecting) {
1381
1755
  this.debug(
@@ -1386,7 +1760,7 @@ var Schematic = class {
1386
1760
  }
1387
1761
  if (this.conn !== null) {
1388
1762
  const socket2 = await this.conn;
1389
- await this.wsSendMessage(socket2, context);
1763
+ await this.wsSendMessage(socket2, context, cacheHit);
1390
1764
  return;
1391
1765
  }
1392
1766
  }
@@ -1402,7 +1776,7 @@ var Schematic = class {
1402
1776
  this.conn = this.wsConnect();
1403
1777
  const socket2 = await this.conn;
1404
1778
  this.isConnecting = false;
1405
- await this.wsSendMessage(socket2, context);
1779
+ await this.wsSendMessage(socket2, context, cacheHit);
1406
1780
  return;
1407
1781
  } catch (error) {
1408
1782
  this.isConnecting = false;
@@ -1410,7 +1784,7 @@ var Schematic = class {
1410
1784
  }
1411
1785
  }
1412
1786
  const socket = await this.conn;
1413
- await this.wsSendMessage(socket, context);
1787
+ await this.wsSendMessage(socket, context, cacheHit);
1414
1788
  } catch (error) {
1415
1789
  console.warn("Failed to establish WebSocket connection:", error);
1416
1790
  this.context = context;
@@ -1470,10 +1844,12 @@ var Schematic = class {
1470
1844
  `Optimistically updating feature usage for event: ${eventName}`,
1471
1845
  { quantity }
1472
1846
  );
1847
+ let dirty = false;
1473
1848
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1474
1849
  if (check === void 0) return;
1475
1850
  const updatedCheck = { ...check };
1476
1851
  if (typeof updatedCheck.featureUsage === "number") {
1852
+ dirty = true;
1477
1853
  updatedCheck.featureUsage += quantity;
1478
1854
  if (typeof updatedCheck.featureAllocation === "number") {
1479
1855
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1503,6 +1879,9 @@ var Schematic = class {
1503
1879
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1504
1880
  }
1505
1881
  });
1882
+ if (dirty) {
1883
+ this.persistContextToCache(contextString(this.context));
1884
+ }
1506
1885
  };
1507
1886
  /**
1508
1887
  * Event processing
@@ -1605,6 +1984,206 @@ var Schematic = class {
1605
1984
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1606
1985
  return generatedAnonymousId;
1607
1986
  };
1987
+ hasCachedValuesForContext = (contextStr) => {
1988
+ const cachedChecks = this.checks[contextStr];
1989
+ if (cachedChecks !== void 0 && Object.keys(cachedChecks).length > 0) {
1990
+ return true;
1991
+ }
1992
+ if (this.planChecks[contextStr] !== void 0) {
1993
+ return true;
1994
+ }
1995
+ const cachedCreditBalances = this.creditBalances[contextStr];
1996
+ return cachedCreditBalances !== void 0 && Object.keys(cachedCreditBalances).length > 0;
1997
+ };
1998
+ readFlagStateCache = () => {
1999
+ if (!this.persistFlagState || this.storage === void 0) {
2000
+ return null;
2001
+ }
2002
+ let raw;
2003
+ try {
2004
+ raw = this.storage.getItem(this.flagStateCacheKey);
2005
+ } catch (error) {
2006
+ this.debug("Failed to read flag state cache:", error);
2007
+ return null;
2008
+ }
2009
+ if (raw === null || raw === void 0) {
2010
+ return null;
2011
+ }
2012
+ let parsed;
2013
+ try {
2014
+ parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
2015
+ } catch (error) {
2016
+ this.debug("Flag state cache is not valid JSON, ignoring:", error);
2017
+ return null;
2018
+ }
2019
+ if (parsed === null || typeof parsed !== "object" || parsed.version !== cacheVersion || typeof parsed.contexts !== "object" || parsed.contexts === null) {
2020
+ this.debug(
2021
+ `Flag state cache invalid or version mismatch (expected ${cacheVersion}), ignoring`,
2022
+ {
2023
+ parsedType: typeof parsed,
2024
+ version: parsed?.version
2025
+ }
2026
+ );
2027
+ return null;
2028
+ }
2029
+ return parsed;
2030
+ };
2031
+ reviveCachedCheck = (raw) => {
2032
+ if (raw === null || typeof raw !== "object") return null;
2033
+ const obj = raw;
2034
+ if (typeof obj.flag !== "string" || typeof obj.value !== "boolean" || typeof obj.reason !== "string") {
2035
+ return null;
2036
+ }
2037
+ const revived = { ...obj };
2038
+ if (typeof revived.featureUsageResetAt === "string") {
2039
+ const d = new Date(revived.featureUsageResetAt);
2040
+ revived.featureUsageResetAt = isNaN(d.getTime()) ? void 0 : d;
2041
+ }
2042
+ return revived;
2043
+ };
2044
+ reviveCachedPlan = (raw) => {
2045
+ if (raw === null || typeof raw !== "object") return null;
2046
+ const obj = raw;
2047
+ if (typeof obj.id !== "string" || typeof obj.name !== "string") {
2048
+ return null;
2049
+ }
2050
+ const revived = { ...obj };
2051
+ if (typeof revived.trialEndDate === "string") {
2052
+ const d = new Date(revived.trialEndDate);
2053
+ revived.trialEndDate = isNaN(d.getTime()) ? void 0 : d;
2054
+ }
2055
+ return revived;
2056
+ };
2057
+ reviveCachedCreditBalances = (raw) => {
2058
+ if (raw === null || typeof raw !== "object") return null;
2059
+ const revived = {};
2060
+ for (const [creditId, value] of Object.entries(
2061
+ raw
2062
+ )) {
2063
+ if (value === null || typeof value !== "object") continue;
2064
+ const obj = value;
2065
+ if (typeof obj.remaining !== "number" || typeof obj.reserved !== "number" || typeof obj.settled !== "number") {
2066
+ continue;
2067
+ }
2068
+ revived[creditId] = {
2069
+ remaining: obj.remaining,
2070
+ reserved: obj.reserved,
2071
+ settled: obj.settled
2072
+ };
2073
+ }
2074
+ return Object.keys(revived).length > 0 ? revived : null;
2075
+ };
2076
+ hydrateFlagStateFromCache = () => {
2077
+ const parsed = this.readFlagStateCache();
2078
+ if (parsed === null) return;
2079
+ const cutoff = Date.now() - this.flagStateCacheMaxAgeMs;
2080
+ const fresh = {};
2081
+ let contextsLoaded = 0;
2082
+ let contextsExpired = 0;
2083
+ for (const [contextStr, entry] of Object.entries(parsed.contexts)) {
2084
+ if (entry === null || typeof entry !== "object") continue;
2085
+ if (typeof entry.updatedAt !== "number") continue;
2086
+ if (entry.updatedAt < cutoff) {
2087
+ contextsExpired += 1;
2088
+ continue;
2089
+ }
2090
+ const revivedChecks = {};
2091
+ if (entry.checks !== null && typeof entry.checks === "object") {
2092
+ for (const [flagKey, rawCheck] of Object.entries(entry.checks)) {
2093
+ const revived = this.reviveCachedCheck(rawCheck);
2094
+ if (revived === null || revived.flag !== flagKey) continue;
2095
+ revivedChecks[flagKey] = revived;
2096
+ if (typeof revived.featureUsageEvent === "string") {
2097
+ this.updateFeatureUsageEventMap(revived);
2098
+ }
2099
+ }
2100
+ if (Object.keys(revivedChecks).length > 0) {
2101
+ this.checks[contextStr] = revivedChecks;
2102
+ }
2103
+ }
2104
+ let revivedPlan;
2105
+ if (entry.plan !== void 0 && entry.plan !== null) {
2106
+ const p = this.reviveCachedPlan(entry.plan);
2107
+ if (p !== null) {
2108
+ revivedPlan = p;
2109
+ this.planChecks[contextStr] = p;
2110
+ }
2111
+ }
2112
+ let revivedCreditBalances;
2113
+ if (entry.creditBalances !== void 0 && entry.creditBalances !== null) {
2114
+ const balances = this.reviveCachedCreditBalances(entry.creditBalances);
2115
+ if (balances !== null) {
2116
+ revivedCreditBalances = balances;
2117
+ this.creditBalances[contextStr] = balances;
2118
+ }
2119
+ }
2120
+ fresh[contextStr] = {
2121
+ checks: revivedChecks,
2122
+ plan: revivedPlan,
2123
+ creditBalances: revivedCreditBalances,
2124
+ updatedAt: entry.updatedAt
2125
+ };
2126
+ contextsLoaded += 1;
2127
+ }
2128
+ this.cachedFlagState = { version: cacheVersion, contexts: fresh };
2129
+ this.debug(
2130
+ `Hydrated flag state cache: ${contextsLoaded} loaded, ${contextsExpired} expired`
2131
+ );
2132
+ };
2133
+ persistContextToCache = (contextStr) => {
2134
+ const checksForContext = this.checks[contextStr];
2135
+ const planForContext = this.planChecks[contextStr];
2136
+ const creditBalancesForContext = this.creditBalances[contextStr];
2137
+ if (checksForContext === void 0 && planForContext === void 0 && creditBalancesForContext === void 0) {
2138
+ return;
2139
+ }
2140
+ if (this.cachedFlagState === null) {
2141
+ this.cachedFlagState = (this.persistFlagState ? this.readFlagStateCache() : null) ?? {
2142
+ version: cacheVersion,
2143
+ contexts: {}
2144
+ };
2145
+ }
2146
+ const cleanChecks = {};
2147
+ if (checksForContext !== void 0) {
2148
+ for (const [flagKey, check] of Object.entries(checksForContext)) {
2149
+ if (check !== void 0) cleanChecks[flagKey] = check;
2150
+ }
2151
+ }
2152
+ this.cachedFlagState.contexts[contextStr] = {
2153
+ checks: cleanChecks,
2154
+ plan: planForContext,
2155
+ creditBalances: creditBalancesForContext,
2156
+ updatedAt: Date.now()
2157
+ };
2158
+ const entries = Object.entries(this.cachedFlagState.contexts);
2159
+ if (entries.length > flagStateCacheMaxContexts) {
2160
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
2161
+ const survivors = entries.slice(0, flagStateCacheMaxContexts);
2162
+ const survivorKeys = new Set(survivors.map(([k]) => k));
2163
+ for (const key of Object.keys(this.checks)) {
2164
+ if (!survivorKeys.has(key)) delete this.checks[key];
2165
+ }
2166
+ for (const key of Object.keys(this.planChecks)) {
2167
+ if (!survivorKeys.has(key)) delete this.planChecks[key];
2168
+ }
2169
+ for (const key of Object.keys(this.creditBalances)) {
2170
+ if (!survivorKeys.has(key)) delete this.creditBalances[key];
2171
+ }
2172
+ this.cachedFlagState = {
2173
+ version: cacheVersion,
2174
+ contexts: Object.fromEntries(survivors)
2175
+ };
2176
+ }
2177
+ if (!this.persistFlagState || this.storage === void 0) return;
2178
+ try {
2179
+ this.storage.setItem(
2180
+ this.flagStateCacheKey,
2181
+ JSON.stringify(this.cachedFlagState)
2182
+ );
2183
+ } catch (error) {
2184
+ this.debug("Failed to write flag state cache:", error);
2185
+ }
2186
+ };
1608
2187
  handleEvent = (eventType, eventBody) => {
1609
2188
  const event = {
1610
2189
  api_key: this.apiKey,
@@ -1865,6 +2444,9 @@ var Schematic = class {
1865
2444
  if (this.conn !== null) {
1866
2445
  try {
1867
2446
  const socket = await this.conn;
2447
+ if (this.currentWebSocket === socket) {
2448
+ this.currentWebSocket = null;
2449
+ }
1868
2450
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1869
2451
  socket.close();
1870
2452
  }
@@ -2030,6 +2612,7 @@ var Schematic = class {
2030
2612
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2031
2613
  let timeoutId = null;
2032
2614
  let isResolved = false;
2615
+ let didOpen = false;
2033
2616
  timeoutId = setTimeout(() => {
2034
2617
  if (!isResolved) {
2035
2618
  isResolved = true;
@@ -2043,6 +2626,7 @@ var Schematic = class {
2043
2626
  webSocket.onopen = () => {
2044
2627
  if (isResolved) return;
2045
2628
  isResolved = true;
2629
+ didOpen = true;
2046
2630
  if (timeoutId !== null) {
2047
2631
  clearTimeout(timeoutId);
2048
2632
  }
@@ -2068,11 +2652,12 @@ var Schematic = class {
2068
2652
  }
2069
2653
  this.debug(`WebSocket connection ${connectionId} closed`);
2070
2654
  this.conn = null;
2655
+ const wasCurrent = this.currentWebSocket === webSocket;
2071
2656
  if (this.currentWebSocket === webSocket) {
2072
2657
  this.currentWebSocket = null;
2073
2658
  this.isConnecting = false;
2074
2659
  }
2075
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2660
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2076
2661
  this.attemptReconnect();
2077
2662
  }
2078
2663
  };
@@ -2168,6 +2753,16 @@ var Schematic = class {
2168
2753
  const plan = this.planChecks[contextStr];
2169
2754
  return plan;
2170
2755
  };
2756
+ /** Get the company's lease-aware credit balances for the current context, keyed by credit ID */
2757
+ getCreditBalances = () => {
2758
+ const contextStr = contextString(this.context);
2759
+ return this.creditBalances[contextStr] ?? emptyCreditBalances;
2760
+ };
2761
+ /** Get the company's lease-aware balance for a single credit type in the current context */
2762
+ getCreditBalance = (creditId) => {
2763
+ const contextStr = contextString(this.context);
2764
+ return this.creditBalances[contextStr]?.[creditId];
2765
+ };
2171
2766
  // flag checks state
2172
2767
  getFlagCheck = (flagKey) => {
2173
2768
  const contextStr = contextString(this.context);
@@ -2227,6 +2822,13 @@ var Schematic = class {
2227
2822
  this.planListeners.delete(listener);
2228
2823
  };
2229
2824
  };
2825
+ /** Register an event listener that will be notified with the company's credit balances (keyed by credit ID) whenever they change */
2826
+ addCreditBalanceListener = (listener) => {
2827
+ this.creditBalanceListeners.add(listener);
2828
+ return () => {
2829
+ this.creditBalanceListeners.delete(listener);
2830
+ };
2831
+ };
2230
2832
  notifyFlagCheckListeners = (flagKey, check) => {
2231
2833
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2232
2834
  if (listeners.size > 0) {
@@ -2290,6 +2892,17 @@ var Schematic = class {
2290
2892
  });
2291
2893
  });
2292
2894
  };
2895
+ notifyCreditBalanceListeners = (value) => {
2896
+ const listeners = this.creditBalanceListeners ?? [];
2897
+ if (listeners.size > 0) {
2898
+ this.debug(`Notifying ${listeners.size} credit balance listeners`, {
2899
+ value
2900
+ });
2901
+ }
2902
+ listeners.forEach(
2903
+ (listener) => notifyCreditBalanceListener(listener, value)
2904
+ );
2905
+ };
2293
2906
  };
2294
2907
  var notifyPendingListener = (listener, value) => {
2295
2908
  if (listener.length > 0) {
@@ -2319,4 +2932,11 @@ var notifyPlanListener = (listener, value) => {
2319
2932
  listener();
2320
2933
  }
2321
2934
  };
2935
+ var notifyCreditBalanceListener = (listener, value) => {
2936
+ if (listener.length > 0) {
2937
+ listener(value);
2938
+ } else {
2939
+ listener();
2940
+ }
2941
+ };
2322
2942
  /* @preserve */