@schematichq/schematic-vue 0.1.0 → 1.4.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.
@@ -30,7 +30,8 @@ __export(index_exports, {
30
30
  useSchematicEntitlement: () => useSchematicEntitlement,
31
31
  useSchematicEvents: () => useSchematicEvents,
32
32
  useSchematicFlag: () => useSchematicFlag,
33
- useSchematicIsPending: () => useSchematicIsPending
33
+ useSchematicIsPending: () => useSchematicIsPending,
34
+ useSchematicPlan: () => useSchematicPlan
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
@@ -642,6 +643,36 @@ function v4(options, buf, offset) {
642
643
  }
643
644
  var v4_default = v4;
644
645
  var import_polyfill = __toESM(require_browser_polyfill());
646
+ function EntitlementValueTypeFromJSON(json) {
647
+ return EntitlementValueTypeFromJSONTyped(json, false);
648
+ }
649
+ function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
650
+ return json;
651
+ }
652
+ function FeatureEntitlementFromJSON(json) {
653
+ return FeatureEntitlementFromJSONTyped(json, false);
654
+ }
655
+ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
656
+ if (json == null) {
657
+ return json;
658
+ }
659
+ return {
660
+ allocation: json["allocation"] == null ? void 0 : json["allocation"],
661
+ creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
662
+ creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
663
+ creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
664
+ creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
665
+ eventName: json["event_name"] == null ? void 0 : json["event_name"],
666
+ featureId: json["feature_id"],
667
+ featureKey: json["feature_key"],
668
+ metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
669
+ 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"],
671
+ softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
672
+ usage: json["usage"] == null ? void 0 : json["usage"],
673
+ valueType: EntitlementValueTypeFromJSON(json["value_type"])
674
+ };
675
+ }
645
676
  function CheckFlagResponseDataFromJSON(json) {
646
677
  return CheckFlagResponseDataFromJSONTyped(json, false);
647
678
  }
@@ -651,6 +682,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
651
682
  }
652
683
  return {
653
684
  companyId: json["company_id"] == null ? void 0 : json["company_id"],
685
+ entitlement: json["entitlement"] == null ? void 0 : FeatureEntitlementFromJSON(json["entitlement"]),
654
686
  error: json["error"] == null ? void 0 : json["error"],
655
687
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
656
688
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
@@ -666,26 +698,6 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
666
698
  value: json["value"]
667
699
  };
668
700
  }
669
- function EventBodyFlagCheckToJSON(json) {
670
- return EventBodyFlagCheckToJSONTyped(json, false);
671
- }
672
- function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
673
- if (value == null) {
674
- return value;
675
- }
676
- return {
677
- company_id: value["companyId"],
678
- error: value["error"],
679
- flag_id: value["flagId"],
680
- flag_key: value["flagKey"],
681
- reason: value["reason"],
682
- req_company: value["reqCompany"],
683
- req_user: value["reqUser"],
684
- rule_id: value["ruleId"],
685
- user_id: value["userId"],
686
- value: value["value"]
687
- };
688
- }
689
701
  function CheckFlagResponseFromJSON(json) {
690
702
  return CheckFlagResponseFromJSONTyped(json, false);
691
703
  }
@@ -698,6 +710,26 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
698
710
  params: json["params"]
699
711
  };
700
712
  }
713
+ function TrialStatusFromJSON(json) {
714
+ return TrialStatusFromJSONTyped(json, false);
715
+ }
716
+ function TrialStatusFromJSONTyped(json, ignoreDiscriminator) {
717
+ return json;
718
+ }
719
+ function DatastreamCompanyPlanFromJSON(json) {
720
+ return DatastreamCompanyPlanFromJSONTyped(json, false);
721
+ }
722
+ function DatastreamCompanyPlanFromJSONTyped(json, ignoreDiscriminator) {
723
+ if (json == null) {
724
+ return json;
725
+ }
726
+ return {
727
+ id: json["id"],
728
+ name: json["name"],
729
+ trialEndDate: json["trial_end_date"] == null ? void 0 : new Date(json["trial_end_date"]),
730
+ trialStatus: json["trial_status"] == null ? void 0 : TrialStatusFromJSON(json["trial_status"])
731
+ };
732
+ }
701
733
  function CheckFlagsResponseDataFromJSON(json) {
702
734
  return CheckFlagsResponseDataFromJSONTyped(json, false);
703
735
  }
@@ -706,7 +738,8 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
706
738
  return json;
707
739
  }
708
740
  return {
709
- flags: json["flags"].map(CheckFlagResponseDataFromJSON)
741
+ flags: json["flags"].map(CheckFlagResponseDataFromJSON),
742
+ plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
710
743
  };
711
744
  }
712
745
  function CheckFlagsResponseFromJSON(json) {
@@ -721,6 +754,26 @@ function CheckFlagsResponseFromJSONTyped(json, ignoreDiscriminator) {
721
754
  params: json["params"]
722
755
  };
723
756
  }
757
+ function EventBodyFlagCheckToJSON(json) {
758
+ return EventBodyFlagCheckToJSONTyped(json, false);
759
+ }
760
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
761
+ if (value == null) {
762
+ return value;
763
+ }
764
+ return {
765
+ company_id: value["companyId"],
766
+ error: value["error"],
767
+ flag_id: value["flagId"],
768
+ flag_key: value["flagKey"],
769
+ reason: value["reason"],
770
+ req_company: value["reqCompany"],
771
+ req_user: value["reqUser"],
772
+ rule_id: value["ruleId"],
773
+ user_id: value["userId"],
774
+ value: value["value"]
775
+ };
776
+ }
724
777
  var RuleType = /* @__PURE__ */ ((RuleType2) => {
725
778
  RuleType2["GLOBAL_OVERRIDE"] = "global_override";
726
779
  RuleType2["COMPANY_OVERRIDE"] = "company_override";
@@ -741,6 +794,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
741
794
  var CheckFlagReturnFromJSON = (json) => {
742
795
  const {
743
796
  companyId,
797
+ entitlement,
744
798
  error,
745
799
  featureAllocation,
746
800
  featureUsage,
@@ -758,24 +812,40 @@ var CheckFlagReturnFromJSON = (json) => {
758
812
  const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
759
813
  (ruleType == "company_override_usage_exceeded" || // if the rule type is one of these, then we have exceeded usage
760
814
  ruleType == "plan_entitlement_usage_exceeded");
815
+ const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
816
+ const resolvedUsage = entitlement?.usage ?? featureUsage;
817
+ const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
818
+ const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
819
+ const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
761
820
  return {
762
821
  featureUsageExceeded,
763
822
  companyId: companyId == null ? void 0 : companyId,
823
+ creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
764
824
  error: error == null ? void 0 : error,
765
- featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
766
- featureUsage: featureUsage == null ? void 0 : featureUsage,
767
- featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
768
- featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
769
- featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
825
+ featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
826
+ featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
827
+ featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
828
+ featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
829
+ featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
770
830
  flag,
771
831
  flagId: flagId == null ? void 0 : flagId,
772
832
  reason,
773
833
  ruleId: ruleId == null ? void 0 : ruleId,
774
834
  ruleType: ruleType == null ? void 0 : ruleType,
835
+ softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
775
836
  userId: userId == null ? void 0 : userId,
776
837
  value
777
838
  };
778
839
  };
840
+ var CheckPlanReturnFromJSON = (json) => {
841
+ const { id, name, trialEndDate, trialStatus } = DatastreamCompanyPlanFromJSON(json);
842
+ return {
843
+ id,
844
+ name,
845
+ trialEndDate: trialEndDate == null ? void 0 : trialEndDate,
846
+ trialStatus: trialStatus == null ? void 0 : trialStatus
847
+ };
848
+ };
779
849
  function contextString(context) {
780
850
  const sortedContext = Object.keys(context).reduce((acc, key) => {
781
851
  const sortedKeys = Object.keys(
@@ -790,7 +860,7 @@ function contextString(context) {
790
860
  }, {});
791
861
  return JSON.stringify(sortedContext);
792
862
  }
793
- var version = "1.2.6";
863
+ var version = "1.4.0";
794
864
  var anonymousIdKey = "schematicId";
795
865
  var Schematic = class {
796
866
  additionalHeaders = {};
@@ -807,11 +877,38 @@ var Schematic = class {
807
877
  flagValueListeners = {};
808
878
  isPending = true;
809
879
  isPendingListeners = /* @__PURE__ */ new Set();
880
+ planListeners = /* @__PURE__ */ new Set();
810
881
  storage;
811
882
  useWebSocket = false;
812
883
  checks = {};
813
884
  featureUsageEventMap = {};
885
+ planChecks = {};
814
886
  webSocketUrl = "wss://api.schematichq.com";
887
+ webSocketConnectionTimeout = 1e4;
888
+ webSocketReconnect = true;
889
+ webSocketMaxReconnectAttempts = 7;
890
+ // Max attempts after connection disrupted
891
+ webSocketMaxConnectionAttempts = 3;
892
+ // Max attempts for initial connection
893
+ webSocketInitialRetryDelay = 1e3;
894
+ webSocketMaxRetryDelay = 3e4;
895
+ wsReconnectAttempts = 0;
896
+ wsReconnectTimer = null;
897
+ wsIntentionalDisconnect = false;
898
+ currentWebSocket = null;
899
+ isConnecting = false;
900
+ maxEventQueueSize = 100;
901
+ // Prevent memory issues with very long network outages
902
+ maxEventRetries = 5;
903
+ // Maximum retry attempts for failed events
904
+ eventRetryInitialDelay = 1e3;
905
+ // Initial retry delay in ms
906
+ eventRetryMaxDelay = 3e4;
907
+ // Maximum retry delay in ms
908
+ retryTimer = null;
909
+ flagValueDefaults = {};
910
+ flagCheckDefaults = {};
911
+ fallbackCheckCache = {};
815
912
  constructor(apiKey, options) {
816
913
  this.apiKey = apiKey;
817
914
  this.eventQueue = [];
@@ -843,8 +940,13 @@ var Schematic = class {
843
940
  };
844
941
  if (options?.storage) {
845
942
  this.storage = options.storage;
846
- } else if (typeof localStorage !== "undefined") {
847
- this.storage = localStorage;
943
+ } else {
944
+ try {
945
+ if (typeof localStorage !== "undefined") {
946
+ this.storage = localStorage;
947
+ }
948
+ } catch {
949
+ }
848
950
  }
849
951
  if (options?.apiUrl !== void 0) {
850
952
  this.apiUrl = options.apiUrl;
@@ -855,11 +957,54 @@ var Schematic = class {
855
957
  if (options?.webSocketUrl !== void 0) {
856
958
  this.webSocketUrl = options.webSocketUrl;
857
959
  }
960
+ if (options?.webSocketConnectionTimeout !== void 0) {
961
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
962
+ }
963
+ if (options?.webSocketReconnect !== void 0) {
964
+ this.webSocketReconnect = options.webSocketReconnect;
965
+ }
966
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
967
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
968
+ }
969
+ if (options?.webSocketInitialRetryDelay !== void 0) {
970
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
971
+ }
972
+ if (options?.webSocketMaxRetryDelay !== void 0) {
973
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
974
+ }
975
+ if (options?.maxEventQueueSize !== void 0) {
976
+ this.maxEventQueueSize = options.maxEventQueueSize;
977
+ }
978
+ if (options?.maxEventRetries !== void 0) {
979
+ this.maxEventRetries = options.maxEventRetries;
980
+ }
981
+ if (options?.eventRetryInitialDelay !== void 0) {
982
+ this.eventRetryInitialDelay = options.eventRetryInitialDelay;
983
+ }
984
+ if (options?.eventRetryMaxDelay !== void 0) {
985
+ this.eventRetryMaxDelay = options.eventRetryMaxDelay;
986
+ }
987
+ if (options?.flagValueDefaults !== void 0) {
988
+ this.flagValueDefaults = options.flagValueDefaults;
989
+ }
990
+ if (options?.flagCheckDefaults !== void 0) {
991
+ this.flagCheckDefaults = options.flagCheckDefaults;
992
+ }
858
993
  if (typeof window !== "undefined" && window?.addEventListener) {
859
994
  window.addEventListener("beforeunload", () => {
860
995
  this.flushEventQueue();
861
996
  this.flushContextDependentEventQueue();
862
997
  });
998
+ if (this.useWebSocket) {
999
+ window.addEventListener("offline", () => {
1000
+ this.debug("Browser went offline, closing WebSocket connection");
1001
+ this.handleNetworkOffline();
1002
+ });
1003
+ window.addEventListener("online", () => {
1004
+ this.debug("Browser came online, attempting to reconnect WebSocket");
1005
+ this.handleNetworkOnline();
1006
+ });
1007
+ }
863
1008
  }
864
1009
  if (this.offlineEnabled) {
865
1010
  this.debug(
@@ -869,24 +1014,89 @@ var Schematic = class {
869
1014
  this.debug("Initialized with debug mode enabled");
870
1015
  }
871
1016
  }
1017
+ /**
1018
+ * Resolve fallback value according to priority order:
1019
+ * 1. Callsite fallback value (if provided)
1020
+ * 2. Boolean value from flagCheckDefaults initialization option
1021
+ * 3. Boolean value from flagValueDefaults initialization option
1022
+ * 4. Default to false
1023
+ */
1024
+ resolveFallbackValue(key, callsiteFallback) {
1025
+ if (callsiteFallback !== void 0) {
1026
+ return callsiteFallback;
1027
+ }
1028
+ if (key in this.flagCheckDefaults) {
1029
+ return this.flagCheckDefaults[key].value;
1030
+ }
1031
+ if (key in this.flagValueDefaults) {
1032
+ return this.flagValueDefaults[key];
1033
+ }
1034
+ return false;
1035
+ }
1036
+ /**
1037
+ * Resolve complete CheckFlagReturn object according to priority order:
1038
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
1039
+ * 2. Use flagCheckDefaults if available for this flag
1040
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
1041
+ * 4. Default CheckFlagReturn with value: false
1042
+ */
1043
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
1044
+ if (callsiteFallback !== void 0) {
1045
+ return {
1046
+ flag: key,
1047
+ value: callsiteFallback,
1048
+ reason,
1049
+ error
1050
+ };
1051
+ }
1052
+ if (key in this.flagCheckDefaults) {
1053
+ const defaultReturn = this.flagCheckDefaults[key];
1054
+ return {
1055
+ ...defaultReturn,
1056
+ flag: key,
1057
+ // Ensure flag matches the requested key
1058
+ reason: error !== void 0 ? reason : defaultReturn.reason,
1059
+ error
1060
+ };
1061
+ }
1062
+ if (key in this.flagValueDefaults) {
1063
+ return {
1064
+ flag: key,
1065
+ value: this.flagValueDefaults[key],
1066
+ reason,
1067
+ error
1068
+ };
1069
+ }
1070
+ return {
1071
+ flag: key,
1072
+ value: false,
1073
+ reason,
1074
+ error
1075
+ };
1076
+ }
872
1077
  /**
873
1078
  * Get value for a single flag.
874
1079
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
875
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
876
- * connection fails.
1080
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
1081
+ * values if WebSocket connection fails.
877
1082
  * In REST mode, makes an API call for each check.
878
1083
  */
879
1084
  async checkFlag(options) {
880
- const { fallback = false, key } = options;
1085
+ const { fallback, key } = options;
881
1086
  const context = options.context || this.context;
882
1087
  const contextStr = contextString(context);
883
1088
  this.debug(`checkFlag: ${key}`, { context, fallback });
884
1089
  if (this.isOffline()) {
1090
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
1091
+ key,
1092
+ fallback,
1093
+ "Offline mode - using initialization defaults"
1094
+ );
885
1095
  this.debug(`checkFlag offline result: ${key}`, {
886
- value: fallback,
1096
+ value: resolvedFallbackResult.value,
887
1097
  offlineMode: true
888
1098
  });
889
- return fallback;
1099
+ return resolvedFallbackResult.value;
890
1100
  }
891
1101
  if (!this.useWebSocket) {
892
1102
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -913,15 +1123,15 @@ var Schematic = class {
913
1123
  this.submitFlagCheckEvent(key, result, context);
914
1124
  return result.value;
915
1125
  }).catch((error) => {
916
- console.error("There was a problem with the fetch operation:", error);
917
- const errorResult = {
918
- flag: key,
919
- value: fallback,
920
- reason: "API request failed",
921
- error: error instanceof Error ? error.message : String(error)
922
- };
1126
+ console.warn("There was a problem with the fetch operation:", error);
1127
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1128
+ key,
1129
+ fallback,
1130
+ "API request failed",
1131
+ error instanceof Error ? error.message : String(error)
1132
+ );
923
1133
  this.submitFlagCheckEvent(key, errorResult, context);
924
- return fallback;
1134
+ return errorResult.value;
925
1135
  });
926
1136
  }
927
1137
  try {
@@ -931,38 +1141,52 @@ var Schematic = class {
931
1141
  return existingVals[key].value;
932
1142
  }
933
1143
  if (this.isOffline()) {
934
- return fallback;
1144
+ return this.resolveFallbackValue(key, fallback);
935
1145
  }
936
1146
  try {
937
1147
  await this.setContext(context);
938
1148
  } catch (error) {
939
- console.error(
940
- "WebSocket connection failed, falling back to REST:",
1149
+ console.warn(
1150
+ "WebSocket connection failed, using fallback value:",
941
1151
  error
942
1152
  );
943
- return this.fallbackToRest(key, context, fallback);
1153
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1154
+ key,
1155
+ fallback,
1156
+ "WebSocket connection failed",
1157
+ error instanceof Error ? error.message : String(error)
1158
+ );
1159
+ this.submitFlagCheckEvent(key, errorResult, context);
1160
+ return errorResult.value;
944
1161
  }
945
1162
  const contextVals = this.checks[contextStr] ?? {};
946
1163
  const flagCheck = contextVals[key];
947
- const result = flagCheck?.value ?? fallback;
1164
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
948
1165
  this.debug(
949
1166
  `checkFlag WebSocket result: ${key}`,
950
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1167
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
951
1168
  );
952
1169
  if (typeof flagCheck !== "undefined") {
953
1170
  this.submitFlagCheckEvent(key, flagCheck, context);
1171
+ } else {
1172
+ const fallbackResult = this.resolveFallbackCheckFlagReturn(
1173
+ key,
1174
+ fallback,
1175
+ "No flag values available"
1176
+ );
1177
+ this.submitFlagCheckEvent(key, fallbackResult, context);
954
1178
  }
955
1179
  return result;
956
1180
  } catch (error) {
957
1181
  console.error("Unexpected error in checkFlag:", error);
958
- const errorResult = {
959
- flag: key,
960
- value: fallback,
961
- reason: "Unexpected error in flag check",
962
- error: error instanceof Error ? error.message : String(error)
963
- };
1182
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1183
+ key,
1184
+ fallback,
1185
+ "Unexpected error in flag check",
1186
+ error instanceof Error ? error.message : String(error)
1187
+ );
964
1188
  this.submitFlagCheckEvent(key, errorResult, context);
965
- return fallback;
1189
+ return errorResult.value;
966
1190
  }
967
1191
  }
968
1192
  /**
@@ -974,6 +1198,53 @@ var Schematic = class {
974
1198
  console.log(`[Schematic] ${message}`, ...args);
975
1199
  }
976
1200
  }
1201
+ /**
1202
+ * Create a persistent message handler for websocket flag updates
1203
+ */
1204
+ createPersistentMessageHandler(context) {
1205
+ return (event) => {
1206
+ const message = JSON.parse(event.data);
1207
+ this.debug(`WebSocket persistent message received:`, message);
1208
+ if (!(contextString(context) in this.checks)) {
1209
+ this.checks[contextString(context)] = {};
1210
+ }
1211
+ (message.flags ?? []).forEach((flag) => {
1212
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1213
+ const contextStr = contextString(context);
1214
+ if (this.checks[contextStr] === void 0) {
1215
+ this.checks[contextStr] = {};
1216
+ }
1217
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1218
+ if (typeof flagCheck.featureUsageEvent === "string") {
1219
+ this.updateFeatureUsageEventMap(flagCheck);
1220
+ }
1221
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1222
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1223
+ }
1224
+ this.debug(
1225
+ `WebSocket flag update received. Notifying listeners for ${flag.flag}`,
1226
+ {
1227
+ flag: flag.flag,
1228
+ value: flagCheck.value,
1229
+ flagCheck
1230
+ }
1231
+ );
1232
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1233
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1234
+ });
1235
+ if (message.plan !== void 0 && message.plan !== null) {
1236
+ const plan = CheckPlanReturnFromJSON(message.plan);
1237
+ const contextStr = contextString(context);
1238
+ this.planChecks[contextStr] = plan;
1239
+ this.debug(`WebSocket plan update received. Notifying listeners`, {
1240
+ plan
1241
+ });
1242
+ this.notifyPlanListeners(plan);
1243
+ }
1244
+ this.flushContextDependentEventQueue();
1245
+ this.setIsPending(false);
1246
+ };
1247
+ }
977
1248
  /**
978
1249
  * Helper function to check if client is in offline mode
979
1250
  */
@@ -1000,52 +1271,6 @@ var Schematic = class {
1000
1271
  this.debug(`submitting flag check event:`, eventBody);
1001
1272
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1002
1273
  }
1003
- /**
1004
- * Helper method for falling back to REST API when WebSocket connection fails
1005
- */
1006
- async fallbackToRest(key, context, fallback) {
1007
- if (this.isOffline()) {
1008
- this.debug(`fallbackToRest offline result: ${key}`, {
1009
- value: fallback,
1010
- offlineMode: true
1011
- });
1012
- return fallback;
1013
- }
1014
- try {
1015
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
1016
- const response = await fetch(requestUrl, {
1017
- method: "POST",
1018
- headers: {
1019
- ...this.additionalHeaders ?? {},
1020
- "Content-Type": "application/json;charset=UTF-8",
1021
- "X-Schematic-Api-Key": this.apiKey
1022
- },
1023
- body: JSON.stringify(context)
1024
- });
1025
- if (!response.ok) {
1026
- throw new Error("Network response was not ok");
1027
- }
1028
- const responseJson = await response.json();
1029
- const data = CheckFlagResponseFromJSON(responseJson);
1030
- this.debug(`fallbackToRest result: ${key}`, data);
1031
- const result = CheckFlagReturnFromJSON(data.data);
1032
- if (typeof result.featureUsageEvent === "string") {
1033
- this.updateFeatureUsageEventMap(result);
1034
- }
1035
- this.submitFlagCheckEvent(key, result, context);
1036
- return result.value;
1037
- } catch (error) {
1038
- console.error("REST API call failed, using fallback value:", error);
1039
- const errorResult = {
1040
- flag: key,
1041
- value: fallback,
1042
- reason: "API request failed (fallback)",
1043
- error: error instanceof Error ? error.message : String(error)
1044
- };
1045
- this.submitFlagCheckEvent(key, errorResult, context);
1046
- return fallback;
1047
- }
1048
- }
1049
1274
  /**
1050
1275
  * Make an API call to fetch all flag values for a given context.
1051
1276
  * Recommended for use in REST mode only.
@@ -1084,7 +1309,7 @@ var Schematic = class {
1084
1309
  {}
1085
1310
  );
1086
1311
  }).catch((error) => {
1087
- console.error("There was a problem with the fetch operation:", error);
1312
+ console.warn("There was a problem with the fetch operation:", error);
1088
1313
  return {};
1089
1314
  });
1090
1315
  };
@@ -1095,14 +1320,12 @@ var Schematic = class {
1095
1320
  */
1096
1321
  identify = (body) => {
1097
1322
  this.debug(`identify:`, body);
1098
- try {
1099
- this.setContext({
1100
- company: body.company?.keys,
1101
- user: body.keys
1102
- });
1103
- } catch (error) {
1104
- console.error("Error setting context:", error);
1105
- }
1323
+ this.setContext({
1324
+ company: body.company?.keys,
1325
+ user: body.keys
1326
+ }).catch((error) => {
1327
+ console.warn("Error setting context:", error);
1328
+ });
1106
1329
  return this.handleEvent("identify", body);
1107
1330
  };
1108
1331
  /**
@@ -1121,16 +1344,52 @@ var Schematic = class {
1121
1344
  this.setIsPending(false);
1122
1345
  return Promise.resolve();
1123
1346
  }
1347
+ if (contextString(context) === contextString(this.context) && this.conn !== null && !this.isPending) {
1348
+ return;
1349
+ }
1124
1350
  try {
1125
1351
  this.setIsPending(true);
1126
1352
  if (!this.conn) {
1127
- this.conn = this.wsConnect();
1353
+ if (this.isConnecting) {
1354
+ this.debug(
1355
+ `Connection already in progress, waiting for it to complete`
1356
+ );
1357
+ while (this.isConnecting && this.conn === null) {
1358
+ await new Promise((resolve) => setTimeout(resolve, 10));
1359
+ }
1360
+ if (this.conn !== null) {
1361
+ const socket2 = await this.conn;
1362
+ await this.wsSendMessage(socket2, context);
1363
+ return;
1364
+ }
1365
+ }
1366
+ if (this.wsReconnectTimer !== null) {
1367
+ this.debug(
1368
+ `Cancelling scheduled reconnection, connecting immediately`
1369
+ );
1370
+ clearTimeout(this.wsReconnectTimer);
1371
+ this.wsReconnectTimer = null;
1372
+ }
1373
+ this.isConnecting = true;
1374
+ try {
1375
+ this.conn = this.wsConnect();
1376
+ const socket2 = await this.conn;
1377
+ this.isConnecting = false;
1378
+ await this.wsSendMessage(socket2, context);
1379
+ return;
1380
+ } catch (error) {
1381
+ this.isConnecting = false;
1382
+ throw error;
1383
+ }
1128
1384
  }
1129
1385
  const socket = await this.conn;
1130
1386
  await this.wsSendMessage(socket, context);
1131
1387
  } catch (error) {
1132
- console.error("Failed to establish WebSocket connection:", error);
1133
- throw error;
1388
+ console.warn("Failed to establish WebSocket connection:", error);
1389
+ this.context = context;
1390
+ this.flushContextDependentEventQueue();
1391
+ this.setIsPending(false);
1392
+ this.attemptReconnect();
1134
1393
  }
1135
1394
  };
1136
1395
  /**
@@ -1253,11 +1512,57 @@ var Schematic = class {
1253
1512
  }
1254
1513
  }
1255
1514
  };
1256
- flushEventQueue = () => {
1257
- while (this.eventQueue.length > 0) {
1258
- const event = this.eventQueue.shift();
1259
- if (event) {
1260
- this.sendEvent(event);
1515
+ startRetryTimer = () => {
1516
+ if (this.retryTimer !== null) {
1517
+ return;
1518
+ }
1519
+ this.retryTimer = setInterval(() => {
1520
+ this.flushEventQueue().catch((error) => {
1521
+ this.debug("Error in retry timer flush:", error);
1522
+ });
1523
+ if (this.eventQueue.length === 0) {
1524
+ this.stopRetryTimer();
1525
+ }
1526
+ }, 5e3);
1527
+ this.debug("Started retry timer");
1528
+ };
1529
+ stopRetryTimer = () => {
1530
+ if (this.retryTimer !== null) {
1531
+ clearInterval(this.retryTimer);
1532
+ this.retryTimer = null;
1533
+ this.debug("Stopped retry timer");
1534
+ }
1535
+ };
1536
+ flushEventQueue = async () => {
1537
+ if (this.eventQueue.length === 0) {
1538
+ return;
1539
+ }
1540
+ const now = Date.now();
1541
+ const readyEvents = [];
1542
+ const notReadyEvents = [];
1543
+ for (const event of this.eventQueue) {
1544
+ if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
1545
+ readyEvents.push(event);
1546
+ } else {
1547
+ notReadyEvents.push(event);
1548
+ }
1549
+ }
1550
+ if (readyEvents.length === 0) {
1551
+ this.debug(
1552
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1553
+ );
1554
+ return;
1555
+ }
1556
+ this.debug(
1557
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1558
+ );
1559
+ this.eventQueue = notReadyEvents;
1560
+ for (const event of readyEvents) {
1561
+ try {
1562
+ await this.sendEvent(event);
1563
+ this.debug(`Queued event sent successfully:`, event.type);
1564
+ } catch (error) {
1565
+ this.debug(`Failed to send queued event:`, error);
1261
1566
  }
1262
1567
  }
1263
1568
  };
@@ -1305,12 +1610,47 @@ var Schematic = class {
1305
1610
  },
1306
1611
  body: payload
1307
1612
  });
1613
+ if (!response.ok) {
1614
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1615
+ }
1308
1616
  this.debug(`event sent:`, {
1309
1617
  status: response.status,
1310
1618
  statusText: response.statusText
1311
1619
  });
1312
1620
  } catch (error) {
1313
- console.error("Error sending Schematic event: ", error);
1621
+ const retryCount = (event.retry_count ?? 0) + 1;
1622
+ if (retryCount <= this.maxEventRetries) {
1623
+ this.debug(
1624
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1625
+ error
1626
+ );
1627
+ const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1628
+ const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1629
+ const nextRetryAt = Date.now() + jitterDelay;
1630
+ const retryEvent = {
1631
+ ...event,
1632
+ retry_count: retryCount,
1633
+ next_retry_at: nextRetryAt
1634
+ };
1635
+ if (this.eventQueue.length < this.maxEventQueueSize) {
1636
+ this.eventQueue.push(retryEvent);
1637
+ this.debug(
1638
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1639
+ );
1640
+ } else {
1641
+ this.debug(
1642
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1643
+ );
1644
+ this.eventQueue.shift();
1645
+ this.eventQueue.push(retryEvent);
1646
+ }
1647
+ this.startRetryTimer();
1648
+ } else {
1649
+ this.debug(
1650
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1651
+ error
1652
+ );
1653
+ }
1314
1654
  }
1315
1655
  return Promise.resolve();
1316
1656
  };
@@ -1321,6 +1661,130 @@ var Schematic = class {
1321
1661
  /**
1322
1662
  * Websocket management
1323
1663
  */
1664
+ /**
1665
+ * Force an immediate WebSocket reconnection.
1666
+ * This is useful when the application returns from a background state (e.g., mobile app
1667
+ * coming back to foreground) and wants to immediately re-establish the connection
1668
+ * rather than waiting for the exponential backoff timer.
1669
+ *
1670
+ * This method will:
1671
+ * - Cancel any pending reconnection timer
1672
+ * - Reset the reconnection attempt counter
1673
+ * - Close any existing connection
1674
+ * - Immediately attempt to reconnect
1675
+ * - Re-send the current context to get fresh flag values
1676
+ *
1677
+ * Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
1678
+ *
1679
+ * @example
1680
+ * ```typescript
1681
+ * // React Native example: reconnect when app comes to foreground
1682
+ * useEffect(() => {
1683
+ * const subscription = AppState.addEventListener("change", (state) => {
1684
+ * if (state === "active") {
1685
+ * client.forceReconnect();
1686
+ * }
1687
+ * });
1688
+ * return () => subscription.remove();
1689
+ * }, [client]);
1690
+ * ```
1691
+ */
1692
+ forceReconnect = async () => {
1693
+ return this.reconnect({ force: true });
1694
+ };
1695
+ /**
1696
+ * Reconnect the WebSocket connection only if the current connection is unhealthy.
1697
+ * This is useful when the application returns from a background state and wants to
1698
+ * ensure a healthy connection exists, but doesn't need to force a reconnection if
1699
+ * the connection is still active.
1700
+ *
1701
+ * This method will:
1702
+ * - Check if an existing connection is healthy (readyState === OPEN)
1703
+ * - If healthy, return immediately without reconnecting
1704
+ * - If unhealthy, perform the same reconnection logic as forceReconnect()
1705
+ *
1706
+ * Use this when you want efficient reconnection that avoids unnecessary disconnects.
1707
+ *
1708
+ * @example
1709
+ * ```typescript
1710
+ * // React Native example: reconnect only if needed when app comes to foreground
1711
+ * useEffect(() => {
1712
+ * const subscription = AppState.addEventListener("change", (state) => {
1713
+ * if (state === "active") {
1714
+ * client.reconnectIfNeeded();
1715
+ * }
1716
+ * });
1717
+ * return () => subscription.remove();
1718
+ * }, [client]);
1719
+ * ```
1720
+ */
1721
+ reconnectIfNeeded = async () => {
1722
+ return this.reconnect({ force: false });
1723
+ };
1724
+ /**
1725
+ * Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
1726
+ */
1727
+ reconnect = async (options) => {
1728
+ const { force } = options;
1729
+ const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
1730
+ if (this.isOffline()) {
1731
+ this.debug(`${methodName}: skipped (offline mode)`);
1732
+ return Promise.resolve();
1733
+ }
1734
+ if (!force && this.conn !== null) {
1735
+ try {
1736
+ const existingSocket = await this.conn;
1737
+ if (existingSocket.readyState === WebSocket.OPEN) {
1738
+ this.debug(`${methodName}: connection is healthy, skipping`);
1739
+ return Promise.resolve();
1740
+ }
1741
+ } catch {
1742
+ }
1743
+ }
1744
+ this.debug(
1745
+ `${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
1746
+ );
1747
+ this.wsIntentionalDisconnect = false;
1748
+ if (this.wsReconnectTimer !== null) {
1749
+ this.debug(`${methodName}: cancelling pending reconnection timer`);
1750
+ clearTimeout(this.wsReconnectTimer);
1751
+ this.wsReconnectTimer = null;
1752
+ }
1753
+ this.wsReconnectAttempts = 0;
1754
+ if (this.conn !== null) {
1755
+ this.debug(`${methodName}: closing existing connection`);
1756
+ try {
1757
+ const existingSocket = await this.conn;
1758
+ if (this.currentWebSocket === existingSocket) {
1759
+ this.currentWebSocket = null;
1760
+ }
1761
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1762
+ existingSocket.close();
1763
+ }
1764
+ } catch (error) {
1765
+ this.debug(`${methodName}: error closing existing connection:`, error);
1766
+ }
1767
+ this.conn = null;
1768
+ this.isConnecting = false;
1769
+ }
1770
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1771
+ this.debug(`${methodName}: reconnecting with existing context`);
1772
+ try {
1773
+ this.isConnecting = true;
1774
+ this.conn = this.wsConnect();
1775
+ const socket = await this.conn;
1776
+ this.isConnecting = false;
1777
+ await this.wsSendMessage(socket, this.context, true);
1778
+ this.debug(`${methodName}: reconnection successful`);
1779
+ } catch (error) {
1780
+ this.isConnecting = false;
1781
+ this.debug(`${methodName}: reconnection failed:`, error);
1782
+ }
1783
+ } else {
1784
+ this.debug(`${methodName}: no context set, skipping reconnection`);
1785
+ }
1786
+ return Promise.resolve();
1787
+ };
1324
1788
  /**
1325
1789
  * If using websocket mode, close the connection when done.
1326
1790
  * In offline mode, this is a no-op.
@@ -1330,95 +1794,307 @@ var Schematic = class {
1330
1794
  this.debug("cleanup: skipped (offline mode)");
1331
1795
  return Promise.resolve();
1332
1796
  }
1797
+ this.wsIntentionalDisconnect = true;
1798
+ if (this.wsReconnectTimer !== null) {
1799
+ clearTimeout(this.wsReconnectTimer);
1800
+ this.wsReconnectTimer = null;
1801
+ }
1802
+ this.stopRetryTimer();
1333
1803
  if (this.conn) {
1334
1804
  try {
1335
1805
  const socket = await this.conn;
1806
+ if (this.currentWebSocket === socket) {
1807
+ this.debug(`Cleaning up current websocket tracking`);
1808
+ this.currentWebSocket = null;
1809
+ }
1336
1810
  socket.close();
1337
1811
  } catch (error) {
1338
- console.error("Error during cleanup:", error);
1812
+ console.warn("Error during cleanup:", error);
1339
1813
  } finally {
1340
1814
  this.conn = null;
1815
+ this.currentWebSocket = null;
1816
+ this.isConnecting = false;
1341
1817
  }
1342
1818
  }
1343
1819
  };
1344
- // Open a websocket connection
1345
- wsConnect = () => {
1820
+ /**
1821
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1822
+ * This helps prevent dogpiling when the server recovers from an outage.
1823
+ */
1824
+ calculateReconnectDelay = () => {
1825
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1826
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1827
+ const jitter = Math.random() * cappedDelay * 0.5;
1828
+ const totalDelay = cappedDelay + jitter;
1829
+ this.debug(
1830
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1831
+ );
1832
+ return totalDelay;
1833
+ };
1834
+ /**
1835
+ * Handle browser going offline
1836
+ */
1837
+ handleNetworkOffline = async () => {
1838
+ if (this.conn !== null) {
1839
+ try {
1840
+ const socket = await this.conn;
1841
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1842
+ socket.close();
1843
+ }
1844
+ } catch (error) {
1845
+ this.debug("Error closing connection on offline:", error);
1846
+ }
1847
+ this.conn = null;
1848
+ }
1849
+ if (this.wsReconnectTimer !== null) {
1850
+ clearTimeout(this.wsReconnectTimer);
1851
+ this.wsReconnectTimer = null;
1852
+ }
1853
+ };
1854
+ /**
1855
+ * Handle browser coming back online
1856
+ */
1857
+ handleNetworkOnline = () => {
1858
+ this.debug(
1859
+ "Network online, attempting reconnection and flushing queued events"
1860
+ );
1861
+ this.wsReconnectAttempts = 0;
1862
+ if (this.wsReconnectTimer !== null) {
1863
+ clearTimeout(this.wsReconnectTimer);
1864
+ this.wsReconnectTimer = null;
1865
+ }
1866
+ this.flushEventQueue().catch((error) => {
1867
+ this.debug("Error flushing event queue on network online:", error);
1868
+ });
1869
+ this.attemptReconnect();
1870
+ };
1871
+ /**
1872
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1873
+ * Called automatically when the connection closes unexpectedly.
1874
+ */
1875
+ attemptReconnect = () => {
1876
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1877
+ this.debug(
1878
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1879
+ );
1880
+ return;
1881
+ }
1882
+ if (this.wsReconnectTimer !== null) {
1883
+ this.debug(
1884
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1885
+ );
1886
+ return;
1887
+ }
1888
+ const delay = this.calculateReconnectDelay();
1889
+ this.debug(
1890
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1891
+ );
1892
+ this.wsReconnectTimer = setTimeout(async () => {
1893
+ this.wsReconnectTimer = null;
1894
+ this.wsReconnectAttempts++;
1895
+ this.debug(
1896
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1897
+ );
1898
+ try {
1899
+ if (this.conn !== null) {
1900
+ this.debug(`Cleaning up existing connection before reconnection`);
1901
+ try {
1902
+ const existingSocket = await this.conn;
1903
+ if (this.currentWebSocket === existingSocket) {
1904
+ this.debug(`Existing websocket is current, will be replaced`);
1905
+ this.currentWebSocket = null;
1906
+ }
1907
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1908
+ existingSocket.close();
1909
+ }
1910
+ } catch (error) {
1911
+ this.debug(`Error cleaning up existing connection:`, error);
1912
+ }
1913
+ this.conn = null;
1914
+ this.currentWebSocket = null;
1915
+ this.isConnecting = false;
1916
+ }
1917
+ this.isConnecting = true;
1918
+ try {
1919
+ this.conn = this.wsConnect();
1920
+ const socket = await this.conn;
1921
+ this.isConnecting = false;
1922
+ this.debug(`Reconnection context check:`, {
1923
+ hasCompany: this.context.company !== void 0,
1924
+ hasUser: this.context.user !== void 0,
1925
+ context: this.context
1926
+ });
1927
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1928
+ this.debug(`Reconnected, force re-sending context`);
1929
+ await this.wsSendMessage(socket, this.context, true);
1930
+ } else {
1931
+ this.debug(
1932
+ `No context to re-send after reconnection - websocket ready for new context`
1933
+ );
1934
+ this.debug(
1935
+ `Setting up tracking for reconnected websocket (no context to send)`
1936
+ );
1937
+ this.currentWebSocket = socket;
1938
+ }
1939
+ this.flushEventQueue().catch((error) => {
1940
+ this.debug(
1941
+ "Error flushing event queue after websocket reconnection:",
1942
+ error
1943
+ );
1944
+ });
1945
+ this.debug(`Reconnection successful`);
1946
+ } catch (error) {
1947
+ this.isConnecting = false;
1948
+ throw error;
1949
+ }
1950
+ } catch (error) {
1951
+ this.debug(`Reconnection attempt failed:`, error);
1952
+ }
1953
+ }, delay);
1954
+ };
1955
+ // Open a websocket connection with retry logic for timeouts
1956
+ wsConnect = async () => {
1346
1957
  if (this.isOffline()) {
1347
1958
  this.debug("wsConnect: skipped (offline mode)");
1348
- return Promise.reject(
1349
- new Error("WebSocket connection skipped in offline mode")
1350
- );
1959
+ throw new Error("WebSocket connection skipped in offline mode");
1351
1960
  }
1961
+ let lastError = null;
1962
+ const maxAttempts = this.webSocketMaxConnectionAttempts;
1963
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1964
+ try {
1965
+ const socket = await this.wsConnectOnce();
1966
+ this.wsReconnectAttempts = 0;
1967
+ return socket;
1968
+ } catch (error) {
1969
+ lastError = error instanceof Error ? error : new Error(String(error));
1970
+ const isTimeout = lastError.message === "WebSocket connection timeout";
1971
+ if (!isTimeout) {
1972
+ this.debug(
1973
+ `WebSocket connection failed with non-timeout error, not retrying:`,
1974
+ lastError.message
1975
+ );
1976
+ throw lastError;
1977
+ }
1978
+ if (attempt < maxAttempts - 1) {
1979
+ const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
1980
+ const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
1981
+ const jitter = cappedDelay * 0.2 * Math.random();
1982
+ const delay = cappedDelay + jitter;
1983
+ this.debug(
1984
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
1985
+ );
1986
+ await new Promise((resolve) => setTimeout(resolve, delay));
1987
+ } else {
1988
+ this.debug(
1989
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
1990
+ );
1991
+ }
1992
+ }
1993
+ }
1994
+ throw lastError ?? new Error("WebSocket connection failed");
1995
+ };
1996
+ // Single attempt to open a websocket connection (no retry logic)
1997
+ wsConnectOnce = () => {
1352
1998
  return new Promise((resolve, reject) => {
1353
1999
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1354
2000
  this.debug(`connecting to WebSocket:`, wsUrl);
1355
2001
  const webSocket = new WebSocket(wsUrl);
2002
+ const connectionId = Math.random().toString(36).substring(7);
2003
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
2004
+ let timeoutId = null;
2005
+ let isResolved = false;
2006
+ timeoutId = setTimeout(() => {
2007
+ if (!isResolved) {
2008
+ isResolved = true;
2009
+ this.debug(
2010
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
2011
+ );
2012
+ webSocket.close();
2013
+ reject(new Error("WebSocket connection timeout"));
2014
+ }
2015
+ }, this.webSocketConnectionTimeout);
1356
2016
  webSocket.onopen = () => {
1357
- this.debug(`WebSocket connection opened`);
2017
+ if (isResolved) return;
2018
+ isResolved = true;
2019
+ if (timeoutId !== null) {
2020
+ clearTimeout(timeoutId);
2021
+ }
2022
+ this.wsIntentionalDisconnect = false;
2023
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1358
2024
  resolve(webSocket);
1359
2025
  };
1360
2026
  webSocket.onerror = (error) => {
1361
- this.debug(`WebSocket connection error:`, error);
1362
- reject(error);
2027
+ if (isResolved) return;
2028
+ isResolved = true;
2029
+ if (timeoutId !== null) {
2030
+ clearTimeout(timeoutId);
2031
+ }
2032
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
2033
+ const wrappedError = new Error(
2034
+ "WebSocket connection failed during handshake"
2035
+ );
2036
+ reject(wrappedError);
1363
2037
  };
1364
2038
  webSocket.onclose = () => {
1365
- this.debug(`WebSocket connection closed`);
2039
+ if (timeoutId !== null) {
2040
+ clearTimeout(timeoutId);
2041
+ }
2042
+ this.debug(`WebSocket connection ${connectionId} closed`);
1366
2043
  this.conn = null;
2044
+ if (this.currentWebSocket === webSocket) {
2045
+ this.currentWebSocket = null;
2046
+ this.isConnecting = false;
2047
+ }
2048
+ if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2049
+ this.attemptReconnect();
2050
+ }
1367
2051
  };
1368
2052
  });
1369
2053
  };
1370
2054
  // Send a message on the websocket indicating interest in a particular evaluation context
1371
2055
  // and wait for the initial set of flag values to be returned
1372
- wsSendMessage = (socket, context) => {
2056
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1373
2057
  if (this.isOffline()) {
1374
2058
  this.debug("wsSendMessage: skipped (offline mode)");
1375
2059
  this.setIsPending(false);
1376
2060
  return Promise.resolve();
1377
2061
  }
1378
2062
  return new Promise((resolve, reject) => {
1379
- if (contextString(context) == contextString(this.context)) {
2063
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1380
2064
  this.debug(`WebSocket context unchanged, skipping update`);
1381
2065
  return resolve(this.setIsPending(false));
1382
2066
  }
1383
- this.debug(`WebSocket context updated:`, context);
2067
+ this.debug(
2068
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
2069
+ context
2070
+ );
1384
2071
  this.context = context;
1385
2072
  const sendMessage = () => {
1386
2073
  let resolved = false;
2074
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1387
2075
  const messageHandler = (event) => {
1388
- const message = JSON.parse(event.data);
1389
- this.debug(`WebSocket message received:`, message);
1390
- if (!(contextString(context) in this.checks)) {
1391
- this.checks[contextString(context)] = {};
1392
- }
1393
- (message.flags ?? []).forEach((flag) => {
1394
- const flagCheck = CheckFlagReturnFromJSON(flag);
1395
- const contextStr = contextString(context);
1396
- if (this.checks[contextStr] === void 0) {
1397
- this.checks[contextStr] = {};
1398
- }
1399
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1400
- this.debug(`WebSocket flag update:`, {
1401
- flag: flagCheck.flag,
1402
- value: flagCheck.value,
1403
- flagCheck
1404
- });
1405
- if (typeof flagCheck.featureUsageEvent === "string") {
1406
- this.updateFeatureUsageEventMap(flagCheck);
1407
- }
1408
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1409
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1410
- }
1411
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1412
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1413
- });
1414
- this.flushContextDependentEventQueue();
1415
- this.setIsPending(false);
2076
+ persistentMessageHandler(event);
1416
2077
  if (!resolved) {
1417
2078
  resolved = true;
1418
2079
  resolve();
1419
2080
  }
1420
2081
  };
1421
2082
  socket.addEventListener("message", messageHandler);
2083
+ socket.addEventListener("close", (event) => {
2084
+ if (!resolved) {
2085
+ resolved = true;
2086
+ if (event.code === 4001) {
2087
+ reject(
2088
+ new Error(
2089
+ `Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
2090
+ )
2091
+ );
2092
+ } else {
2093
+ reject(new Error("WebSocket connection closed unexpectedly"));
2094
+ }
2095
+ }
2096
+ });
2097
+ this.currentWebSocket = socket;
1422
2098
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1423
2099
  const messagePayload = {
1424
2100
  apiKey: this.apiKey,
@@ -1454,21 +2130,49 @@ var Schematic = class {
1454
2130
  };
1455
2131
  };
1456
2132
  setIsPending = (isPending) => {
2133
+ if (this.isPending === isPending) return;
1457
2134
  this.isPending = isPending;
1458
2135
  this.isPendingListeners.forEach(
1459
2136
  (listener) => notifyPendingListener(listener, isPending)
1460
2137
  );
1461
2138
  };
2139
+ getPlan = () => {
2140
+ const contextStr = contextString(this.context);
2141
+ const plan = this.planChecks[contextStr];
2142
+ return plan;
2143
+ };
1462
2144
  // flag checks state
1463
2145
  getFlagCheck = (flagKey) => {
1464
2146
  const contextStr = contextString(this.context);
1465
2147
  const checks = this.checks[contextStr] ?? {};
1466
- return checks[flagKey];
2148
+ const check = checks[flagKey];
2149
+ if (check !== void 0) {
2150
+ return check;
2151
+ }
2152
+ if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
2153
+ if (!(flagKey in this.fallbackCheckCache)) {
2154
+ this.fallbackCheckCache[flagKey] = this.resolveFallbackCheckFlagReturn(
2155
+ flagKey,
2156
+ void 0,
2157
+ "Default value used"
2158
+ );
2159
+ }
2160
+ return this.fallbackCheckCache[flagKey];
2161
+ }
2162
+ return void 0;
1467
2163
  };
1468
2164
  // flagValues state
1469
2165
  getFlagValue = (flagKey) => {
1470
- const check = this.getFlagCheck(flagKey);
1471
- return check?.value;
2166
+ const contextStr = contextString(this.context);
2167
+ const checks = this.checks[contextStr] ?? {};
2168
+ const check = checks[flagKey];
2169
+ if (check?.value !== void 0) {
2170
+ return check.value;
2171
+ }
2172
+ if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
2173
+ return this.resolveFallbackValue(flagKey);
2174
+ }
2175
+ return void 0;
1472
2176
  };
1473
2177
  /** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
1474
2178
  addFlagValueListener = (flagKey, listener) => {
@@ -1490,6 +2194,12 @@ var Schematic = class {
1490
2194
  this.flagCheckListeners[flagKey].delete(listener);
1491
2195
  };
1492
2196
  };
2197
+ addPlanListener = (listener) => {
2198
+ this.planListeners.add(listener);
2199
+ return () => {
2200
+ this.planListeners.delete(listener);
2201
+ };
2202
+ };
1493
2203
  notifyFlagCheckListeners = (flagKey, check) => {
1494
2204
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1495
2205
  if (listeners.size > 0) {
@@ -1526,7 +2236,32 @@ var Schematic = class {
1526
2236
  { value }
1527
2237
  );
1528
2238
  }
1529
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
2239
+ listeners.forEach((listener, index) => {
2240
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
2241
+ flagKey,
2242
+ value
2243
+ });
2244
+ notifyFlagValueListener(listener, value);
2245
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
2246
+ flagKey,
2247
+ value
2248
+ });
2249
+ });
2250
+ };
2251
+ notifyPlanListeners = (value) => {
2252
+ const listeners = this.planListeners ?? [];
2253
+ if (listeners.size > 0) {
2254
+ this.debug(`Notifying ${listeners.size} plan listeners`, { value });
2255
+ }
2256
+ listeners.forEach((listener, index) => {
2257
+ this.debug(`Calling listener ${index} for plan`, {
2258
+ value
2259
+ });
2260
+ notifyPlanListener(listener, value);
2261
+ this.debug(`Listener ${index} for plan completed`, {
2262
+ value
2263
+ });
2264
+ });
1530
2265
  };
1531
2266
  };
1532
2267
  var notifyPendingListener = (listener, value) => {
@@ -1550,12 +2285,19 @@ var notifyFlagValueListener = (listener, value) => {
1550
2285
  listener();
1551
2286
  }
1552
2287
  };
2288
+ var notifyPlanListener = (listener, value) => {
2289
+ if (listener.length > 0) {
2290
+ listener(value);
2291
+ } else {
2292
+ listener();
2293
+ }
2294
+ };
1553
2295
 
1554
2296
  // src/context/schematic.ts
1555
2297
  var import_vue = require("vue");
1556
2298
 
1557
2299
  // src/version.ts
1558
- var version2 = "0.1.0";
2300
+ var version2 = "1.4.0";
1559
2301
 
1560
2302
  // src/context/schematic.ts
1561
2303
  var SchematicSymbol = Symbol("schematic");
@@ -1671,6 +2413,23 @@ var useSchematicEntitlement = (key, opts) => {
1671
2413
  userId: (0, import_vue2.computed)(() => flagCheck.value.userId)
1672
2414
  };
1673
2415
  };
2416
+ var useSchematicPlan = (opts) => {
2417
+ const client = useSchematicClient(opts);
2418
+ const fallback = opts?.fallback;
2419
+ const plan = (0, import_vue2.ref)(
2420
+ client.getPlan() ?? fallback
2421
+ );
2422
+ let unsubscribe = null;
2423
+ (0, import_vue2.onMounted)(() => {
2424
+ unsubscribe = client.addPlanListener((value) => {
2425
+ plan.value = value;
2426
+ });
2427
+ });
2428
+ (0, import_vue2.onScopeDispose)(() => {
2429
+ unsubscribe?.();
2430
+ });
2431
+ return plan;
2432
+ };
1674
2433
  var useSchematicIsPending = (opts) => {
1675
2434
  const client = useSchematicClient(opts);
1676
2435
  const isPending = (0, import_vue2.ref)(client.getIsPending());