@schematichq/schematic-js 1.3.2 → 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
  };
@@ -793,6 +1081,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
793
1081
  var CheckFlagReturnFromJSON = (json) => {
794
1082
  const {
795
1083
  companyId,
1084
+ entitlement,
796
1085
  error,
797
1086
  featureAllocation,
798
1087
  featureUsage,
@@ -810,20 +1099,30 @@ var CheckFlagReturnFromJSON = (json) => {
810
1099
  const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
811
1100
  (ruleType == "company_override_usage_exceeded" /* COMPANY_OVERRIDE_USAGE_EXCEEDED */ || // if the rule type is one of these, then we have exceeded usage
812
1101
  ruleType == "plan_entitlement_usage_exceeded" /* PLAN_ENTITLEMENT_USAGE_EXCEEDED */);
1102
+ const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
1103
+ const resolvedUsage = entitlement?.usage ?? featureUsage;
1104
+ const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
1105
+ const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
1106
+ const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
813
1107
  return {
814
1108
  featureUsageExceeded,
815
1109
  companyId: companyId == null ? void 0 : companyId,
1110
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
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,
816
1114
  error: error == null ? void 0 : error,
817
- featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
818
- featureUsage: featureUsage == null ? void 0 : featureUsage,
819
- featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
820
- featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
821
- featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
1115
+ featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
1116
+ featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
1117
+ featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
1118
+ featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
1119
+ featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
822
1120
  flag,
823
1121
  flagId: flagId == null ? void 0 : flagId,
824
1122
  reason,
825
1123
  ruleId: ruleId == null ? void 0 : ruleId,
826
1124
  ruleType: ruleType == null ? void 0 : ruleType,
1125
+ softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
827
1126
  userId: userId == null ? void 0 : userId,
828
1127
  value
829
1128
  };
@@ -837,6 +1136,24 @@ var CheckPlanReturnFromJSON = (json) => {
837
1136
  trialStatus: trialStatus == null ? void 0 : trialStatus
838
1137
  };
839
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";
840
1157
 
841
1158
  // src/utils.ts
842
1159
  function contextString(context) {
@@ -855,10 +1172,14 @@ function contextString(context) {
855
1172
  }
856
1173
 
857
1174
  // src/version.ts
858
- var version = "1.3.2";
1175
+ var version = "1.5.0";
859
1176
 
860
1177
  // src/index.ts
861
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({});
862
1183
  var Schematic = class {
863
1184
  additionalHeaders = {};
864
1185
  apiKey;
@@ -875,11 +1196,17 @@ var Schematic = class {
875
1196
  isPending = true;
876
1197
  isPendingListeners = /* @__PURE__ */ new Set();
877
1198
  planListeners = /* @__PURE__ */ new Set();
1199
+ creditBalanceListeners = /* @__PURE__ */ new Set();
878
1200
  storage;
1201
+ persistFlagState = true;
1202
+ flagStateCacheKey;
1203
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1204
+ cachedFlagState = null;
879
1205
  useWebSocket = false;
880
1206
  checks = {};
881
1207
  featureUsageEventMap = {};
882
1208
  planChecks = {};
1209
+ creditBalances = {};
883
1210
  webSocketUrl = "wss://api.schematichq.com";
884
1211
  webSocketConnectionTimeout = 1e4;
885
1212
  webSocketReconnect = true;
@@ -908,11 +1235,16 @@ var Schematic = class {
908
1235
  fallbackCheckCache = {};
909
1236
  constructor(apiKey, options) {
910
1237
  this.apiKey = apiKey;
1238
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
911
1239
  this.eventQueue = [];
912
1240
  this.contextDependentEventQueue = [];
913
1241
  this.useWebSocket = options?.useWebSocket ?? false;
914
1242
  this.debugEnabled = options?.debug ?? false;
915
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
+ }
916
1248
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
917
1249
  const params = new URLSearchParams(window.location.search);
918
1250
  const debugParam = params.get("schematic_debug");
@@ -945,6 +1277,7 @@ var Schematic = class {
945
1277
  } catch {
946
1278
  }
947
1279
  }
1280
+ this.hydrateFlagStateFromCache();
948
1281
  if (options?.apiUrl !== void 0) {
949
1282
  this.apiUrl = options.apiUrl;
950
1283
  }
@@ -1238,6 +1571,29 @@ var Schematic = class {
1238
1571
  });
1239
1572
  this.notifyPlanListeners(plan);
1240
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));
1241
1597
  this.flushContextDependentEventQueue();
1242
1598
  this.setIsPending(false);
1243
1599
  };
@@ -1345,7 +1701,32 @@ var Schematic = class {
1345
1701
  return;
1346
1702
  }
1347
1703
  try {
1348
- 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
+ }
1349
1730
  if (!this.conn) {
1350
1731
  if (this.isConnecting) {
1351
1732
  this.debug(
@@ -1356,7 +1737,7 @@ var Schematic = class {
1356
1737
  }
1357
1738
  if (this.conn !== null) {
1358
1739
  const socket2 = await this.conn;
1359
- await this.wsSendMessage(socket2, context);
1740
+ await this.wsSendMessage(socket2, context, cacheHit);
1360
1741
  return;
1361
1742
  }
1362
1743
  }
@@ -1372,7 +1753,7 @@ var Schematic = class {
1372
1753
  this.conn = this.wsConnect();
1373
1754
  const socket2 = await this.conn;
1374
1755
  this.isConnecting = false;
1375
- await this.wsSendMessage(socket2, context);
1756
+ await this.wsSendMessage(socket2, context, cacheHit);
1376
1757
  return;
1377
1758
  } catch (error) {
1378
1759
  this.isConnecting = false;
@@ -1380,7 +1761,7 @@ var Schematic = class {
1380
1761
  }
1381
1762
  }
1382
1763
  const socket = await this.conn;
1383
- await this.wsSendMessage(socket, context);
1764
+ await this.wsSendMessage(socket, context, cacheHit);
1384
1765
  } catch (error) {
1385
1766
  console.warn("Failed to establish WebSocket connection:", error);
1386
1767
  this.context = context;
@@ -1440,10 +1821,12 @@ var Schematic = class {
1440
1821
  `Optimistically updating feature usage for event: ${eventName}`,
1441
1822
  { quantity }
1442
1823
  );
1824
+ let dirty = false;
1443
1825
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1444
1826
  if (check === void 0) return;
1445
1827
  const updatedCheck = { ...check };
1446
1828
  if (typeof updatedCheck.featureUsage === "number") {
1829
+ dirty = true;
1447
1830
  updatedCheck.featureUsage += quantity;
1448
1831
  if (typeof updatedCheck.featureAllocation === "number") {
1449
1832
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1473,6 +1856,9 @@ var Schematic = class {
1473
1856
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1474
1857
  }
1475
1858
  });
1859
+ if (dirty) {
1860
+ this.persistContextToCache(contextString(this.context));
1861
+ }
1476
1862
  };
1477
1863
  /**
1478
1864
  * Event processing
@@ -1575,6 +1961,206 @@ var Schematic = class {
1575
1961
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1576
1962
  return generatedAnonymousId;
1577
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
+ };
1578
2164
  handleEvent = (eventType, eventBody) => {
1579
2165
  const event = {
1580
2166
  api_key: this.apiKey,
@@ -1835,6 +2421,9 @@ var Schematic = class {
1835
2421
  if (this.conn !== null) {
1836
2422
  try {
1837
2423
  const socket = await this.conn;
2424
+ if (this.currentWebSocket === socket) {
2425
+ this.currentWebSocket = null;
2426
+ }
1838
2427
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1839
2428
  socket.close();
1840
2429
  }
@@ -2000,6 +2589,7 @@ var Schematic = class {
2000
2589
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2001
2590
  let timeoutId = null;
2002
2591
  let isResolved = false;
2592
+ let didOpen = false;
2003
2593
  timeoutId = setTimeout(() => {
2004
2594
  if (!isResolved) {
2005
2595
  isResolved = true;
@@ -2013,6 +2603,7 @@ var Schematic = class {
2013
2603
  webSocket.onopen = () => {
2014
2604
  if (isResolved) return;
2015
2605
  isResolved = true;
2606
+ didOpen = true;
2016
2607
  if (timeoutId !== null) {
2017
2608
  clearTimeout(timeoutId);
2018
2609
  }
@@ -2038,11 +2629,12 @@ var Schematic = class {
2038
2629
  }
2039
2630
  this.debug(`WebSocket connection ${connectionId} closed`);
2040
2631
  this.conn = null;
2632
+ const wasCurrent = this.currentWebSocket === webSocket;
2041
2633
  if (this.currentWebSocket === webSocket) {
2042
2634
  this.currentWebSocket = null;
2043
2635
  this.isConnecting = false;
2044
2636
  }
2045
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2637
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2046
2638
  this.attemptReconnect();
2047
2639
  }
2048
2640
  };
@@ -2138,6 +2730,16 @@ var Schematic = class {
2138
2730
  const plan = this.planChecks[contextStr];
2139
2731
  return plan;
2140
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
+ };
2141
2743
  // flag checks state
2142
2744
  getFlagCheck = (flagKey) => {
2143
2745
  const contextStr = contextString(this.context);
@@ -2197,6 +2799,13 @@ var Schematic = class {
2197
2799
  this.planListeners.delete(listener);
2198
2800
  };
2199
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
+ };
2200
2809
  notifyFlagCheckListeners = (flagKey, check) => {
2201
2810
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2202
2811
  if (listeners.size > 0) {
@@ -2260,6 +2869,17 @@ var Schematic = class {
2260
2869
  });
2261
2870
  });
2262
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
+ };
2263
2883
  };
2264
2884
  var notifyPendingListener = (listener, value) => {
2265
2885
  if (listener.length > 0) {
@@ -2289,11 +2909,19 @@ var notifyPlanListener = (listener, value) => {
2289
2909
  listener();
2290
2910
  }
2291
2911
  };
2912
+ var notifyCreditBalanceListener = (listener, value) => {
2913
+ if (listener.length > 0) {
2914
+ listener(value);
2915
+ } else {
2916
+ listener();
2917
+ }
2918
+ };
2292
2919
  export {
2293
2920
  CheckFlagResponseFromJSON,
2294
2921
  CheckFlagReturnFromJSON,
2295
2922
  CheckFlagsResponseFromJSON,
2296
2923
  CheckPlanReturnFromJSON,
2924
+ CreditBalancesFromJSON,
2297
2925
  DatastreamCompanyPlanFromJSON,
2298
2926
  EventBodyFlagCheckToJSON,
2299
2927
  RuleType,