@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.
@@ -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
  };
@@ -815,6 +1104,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
815
1104
  var CheckFlagReturnFromJSON = (json) => {
816
1105
  const {
817
1106
  companyId,
1107
+ entitlement,
818
1108
  error,
819
1109
  featureAllocation,
820
1110
  featureUsage,
@@ -832,20 +1122,30 @@ var CheckFlagReturnFromJSON = (json) => {
832
1122
  const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
833
1123
  (ruleType == "company_override_usage_exceeded" /* COMPANY_OVERRIDE_USAGE_EXCEEDED */ || // if the rule type is one of these, then we have exceeded usage
834
1124
  ruleType == "plan_entitlement_usage_exceeded" /* PLAN_ENTITLEMENT_USAGE_EXCEEDED */);
1125
+ const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
1126
+ const resolvedUsage = entitlement?.usage ?? featureUsage;
1127
+ const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
1128
+ const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
1129
+ const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
835
1130
  return {
836
1131
  featureUsageExceeded,
837
1132
  companyId: companyId == null ? void 0 : companyId,
1133
+ creditId: entitlement?.creditId == null ? void 0 : entitlement.creditId,
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,
838
1137
  error: error == null ? void 0 : error,
839
- featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
840
- featureUsage: featureUsage == null ? void 0 : featureUsage,
841
- featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
842
- featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
843
- featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
1138
+ featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
1139
+ featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
1140
+ featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
1141
+ featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
1142
+ featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
844
1143
  flag,
845
1144
  flagId: flagId == null ? void 0 : flagId,
846
1145
  reason,
847
1146
  ruleId: ruleId == null ? void 0 : ruleId,
848
1147
  ruleType: ruleType == null ? void 0 : ruleType,
1148
+ softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
849
1149
  userId: userId == null ? void 0 : userId,
850
1150
  value
851
1151
  };
@@ -859,6 +1159,24 @@ var CheckPlanReturnFromJSON = (json) => {
859
1159
  trialStatus: trialStatus == null ? void 0 : trialStatus
860
1160
  };
861
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";
862
1180
 
863
1181
  // src/utils.ts
864
1182
  function contextString(context) {
@@ -877,10 +1195,14 @@ function contextString(context) {
877
1195
  }
878
1196
 
879
1197
  // src/version.ts
880
- var version = "1.3.2";
1198
+ var version = "1.5.0";
881
1199
 
882
1200
  // src/index.ts
883
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({});
884
1206
  var Schematic = class {
885
1207
  additionalHeaders = {};
886
1208
  apiKey;
@@ -897,11 +1219,17 @@ var Schematic = class {
897
1219
  isPending = true;
898
1220
  isPendingListeners = /* @__PURE__ */ new Set();
899
1221
  planListeners = /* @__PURE__ */ new Set();
1222
+ creditBalanceListeners = /* @__PURE__ */ new Set();
900
1223
  storage;
1224
+ persistFlagState = true;
1225
+ flagStateCacheKey;
1226
+ flagStateCacheMaxAgeMs = flagStateCacheDefaultMaxAgeMs;
1227
+ cachedFlagState = null;
901
1228
  useWebSocket = false;
902
1229
  checks = {};
903
1230
  featureUsageEventMap = {};
904
1231
  planChecks = {};
1232
+ creditBalances = {};
905
1233
  webSocketUrl = "wss://api.schematichq.com";
906
1234
  webSocketConnectionTimeout = 1e4;
907
1235
  webSocketReconnect = true;
@@ -930,11 +1258,16 @@ var Schematic = class {
930
1258
  fallbackCheckCache = {};
931
1259
  constructor(apiKey, options) {
932
1260
  this.apiKey = apiKey;
1261
+ this.flagStateCacheKey = `${flagStateCachePrefix}:${apiKey}`;
933
1262
  this.eventQueue = [];
934
1263
  this.contextDependentEventQueue = [];
935
1264
  this.useWebSocket = options?.useWebSocket ?? false;
936
1265
  this.debugEnabled = options?.debug ?? false;
937
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
+ }
938
1271
  if (typeof window !== "undefined" && typeof window.location !== "undefined") {
939
1272
  const params = new URLSearchParams(window.location.search);
940
1273
  const debugParam = params.get("schematic_debug");
@@ -967,6 +1300,7 @@ var Schematic = class {
967
1300
  } catch {
968
1301
  }
969
1302
  }
1303
+ this.hydrateFlagStateFromCache();
970
1304
  if (options?.apiUrl !== void 0) {
971
1305
  this.apiUrl = options.apiUrl;
972
1306
  }
@@ -1260,6 +1594,29 @@ var Schematic = class {
1260
1594
  });
1261
1595
  this.notifyPlanListeners(plan);
1262
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));
1263
1620
  this.flushContextDependentEventQueue();
1264
1621
  this.setIsPending(false);
1265
1622
  };
@@ -1367,7 +1724,32 @@ var Schematic = class {
1367
1724
  return;
1368
1725
  }
1369
1726
  try {
1370
- 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
+ }
1371
1753
  if (!this.conn) {
1372
1754
  if (this.isConnecting) {
1373
1755
  this.debug(
@@ -1378,7 +1760,7 @@ var Schematic = class {
1378
1760
  }
1379
1761
  if (this.conn !== null) {
1380
1762
  const socket2 = await this.conn;
1381
- await this.wsSendMessage(socket2, context);
1763
+ await this.wsSendMessage(socket2, context, cacheHit);
1382
1764
  return;
1383
1765
  }
1384
1766
  }
@@ -1394,7 +1776,7 @@ var Schematic = class {
1394
1776
  this.conn = this.wsConnect();
1395
1777
  const socket2 = await this.conn;
1396
1778
  this.isConnecting = false;
1397
- await this.wsSendMessage(socket2, context);
1779
+ await this.wsSendMessage(socket2, context, cacheHit);
1398
1780
  return;
1399
1781
  } catch (error) {
1400
1782
  this.isConnecting = false;
@@ -1402,7 +1784,7 @@ var Schematic = class {
1402
1784
  }
1403
1785
  }
1404
1786
  const socket = await this.conn;
1405
- await this.wsSendMessage(socket, context);
1787
+ await this.wsSendMessage(socket, context, cacheHit);
1406
1788
  } catch (error) {
1407
1789
  console.warn("Failed to establish WebSocket connection:", error);
1408
1790
  this.context = context;
@@ -1462,10 +1844,12 @@ var Schematic = class {
1462
1844
  `Optimistically updating feature usage for event: ${eventName}`,
1463
1845
  { quantity }
1464
1846
  );
1847
+ let dirty = false;
1465
1848
  Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1466
1849
  if (check === void 0) return;
1467
1850
  const updatedCheck = { ...check };
1468
1851
  if (typeof updatedCheck.featureUsage === "number") {
1852
+ dirty = true;
1469
1853
  updatedCheck.featureUsage += quantity;
1470
1854
  if (typeof updatedCheck.featureAllocation === "number") {
1471
1855
  const wasExceeded = updatedCheck.featureUsageExceeded === true;
@@ -1495,6 +1879,9 @@ var Schematic = class {
1495
1879
  this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1496
1880
  }
1497
1881
  });
1882
+ if (dirty) {
1883
+ this.persistContextToCache(contextString(this.context));
1884
+ }
1498
1885
  };
1499
1886
  /**
1500
1887
  * Event processing
@@ -1597,6 +1984,206 @@ var Schematic = class {
1597
1984
  this.storage.setItem(anonymousIdKey, generatedAnonymousId);
1598
1985
  return generatedAnonymousId;
1599
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
+ };
1600
2187
  handleEvent = (eventType, eventBody) => {
1601
2188
  const event = {
1602
2189
  api_key: this.apiKey,
@@ -1857,6 +2444,9 @@ var Schematic = class {
1857
2444
  if (this.conn !== null) {
1858
2445
  try {
1859
2446
  const socket = await this.conn;
2447
+ if (this.currentWebSocket === socket) {
2448
+ this.currentWebSocket = null;
2449
+ }
1860
2450
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1861
2451
  socket.close();
1862
2452
  }
@@ -2022,6 +2612,7 @@ var Schematic = class {
2022
2612
  this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2023
2613
  let timeoutId = null;
2024
2614
  let isResolved = false;
2615
+ let didOpen = false;
2025
2616
  timeoutId = setTimeout(() => {
2026
2617
  if (!isResolved) {
2027
2618
  isResolved = true;
@@ -2035,6 +2626,7 @@ var Schematic = class {
2035
2626
  webSocket.onopen = () => {
2036
2627
  if (isResolved) return;
2037
2628
  isResolved = true;
2629
+ didOpen = true;
2038
2630
  if (timeoutId !== null) {
2039
2631
  clearTimeout(timeoutId);
2040
2632
  }
@@ -2060,11 +2652,12 @@ var Schematic = class {
2060
2652
  }
2061
2653
  this.debug(`WebSocket connection ${connectionId} closed`);
2062
2654
  this.conn = null;
2655
+ const wasCurrent = this.currentWebSocket === webSocket;
2063
2656
  if (this.currentWebSocket === webSocket) {
2064
2657
  this.currentWebSocket = null;
2065
2658
  this.isConnecting = false;
2066
2659
  }
2067
- if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2660
+ if (wasCurrent && didOpen && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2068
2661
  this.attemptReconnect();
2069
2662
  }
2070
2663
  };
@@ -2160,6 +2753,16 @@ var Schematic = class {
2160
2753
  const plan = this.planChecks[contextStr];
2161
2754
  return plan;
2162
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
+ };
2163
2766
  // flag checks state
2164
2767
  getFlagCheck = (flagKey) => {
2165
2768
  const contextStr = contextString(this.context);
@@ -2219,6 +2822,13 @@ var Schematic = class {
2219
2822
  this.planListeners.delete(listener);
2220
2823
  };
2221
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
+ };
2222
2832
  notifyFlagCheckListeners = (flagKey, check) => {
2223
2833
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
2224
2834
  if (listeners.size > 0) {
@@ -2282,6 +2892,17 @@ var Schematic = class {
2282
2892
  });
2283
2893
  });
2284
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
+ };
2285
2906
  };
2286
2907
  var notifyPendingListener = (listener, value) => {
2287
2908
  if (listener.length > 0) {
@@ -2311,4 +2932,11 @@ var notifyPlanListener = (listener, value) => {
2311
2932
  listener();
2312
2933
  }
2313
2934
  };
2935
+ var notifyCreditBalanceListener = (listener, value) => {
2936
+ if (listener.length > 0) {
2937
+ listener(value);
2938
+ } else {
2939
+ listener();
2940
+ }
2941
+ };
2314
2942
  /* @preserve */