@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.
@@ -606,6 +606,36 @@ function v4(options, buf, offset) {
606
606
  }
607
607
  var v4_default = v4;
608
608
  var import_polyfill = __toESM(require_browser_polyfill());
609
+ function EntitlementValueTypeFromJSON(json) {
610
+ return EntitlementValueTypeFromJSONTyped(json, false);
611
+ }
612
+ function EntitlementValueTypeFromJSONTyped(json, ignoreDiscriminator) {
613
+ return json;
614
+ }
615
+ function FeatureEntitlementFromJSON(json) {
616
+ return FeatureEntitlementFromJSONTyped(json, false);
617
+ }
618
+ function FeatureEntitlementFromJSONTyped(json, ignoreDiscriminator) {
619
+ if (json == null) {
620
+ return json;
621
+ }
622
+ return {
623
+ allocation: json["allocation"] == null ? void 0 : json["allocation"],
624
+ creditId: json["credit_id"] == null ? void 0 : json["credit_id"],
625
+ creditRemaining: json["credit_remaining"] == null ? void 0 : json["credit_remaining"],
626
+ creditTotal: json["credit_total"] == null ? void 0 : json["credit_total"],
627
+ creditUsed: json["credit_used"] == null ? void 0 : json["credit_used"],
628
+ eventName: json["event_name"] == null ? void 0 : json["event_name"],
629
+ featureId: json["feature_id"],
630
+ featureKey: json["feature_key"],
631
+ metricPeriod: json["metric_period"] == null ? void 0 : json["metric_period"],
632
+ metricResetAt: json["metric_reset_at"] == null ? void 0 : new Date(json["metric_reset_at"]),
633
+ monthReset: json["month_reset"] == null ? void 0 : json["month_reset"],
634
+ softLimit: json["soft_limit"] == null ? void 0 : json["soft_limit"],
635
+ usage: json["usage"] == null ? void 0 : json["usage"],
636
+ valueType: EntitlementValueTypeFromJSON(json["value_type"])
637
+ };
638
+ }
609
639
  function CheckFlagResponseDataFromJSON(json) {
610
640
  return CheckFlagResponseDataFromJSONTyped(json, false);
611
641
  }
@@ -615,6 +645,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
615
645
  }
616
646
  return {
617
647
  companyId: json["company_id"] == null ? void 0 : json["company_id"],
648
+ entitlement: json["entitlement"] == null ? void 0 : FeatureEntitlementFromJSON(json["entitlement"]),
618
649
  error: json["error"] == null ? void 0 : json["error"],
619
650
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
620
651
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
@@ -630,26 +661,6 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
630
661
  value: json["value"]
631
662
  };
632
663
  }
633
- function EventBodyFlagCheckToJSON(json) {
634
- return EventBodyFlagCheckToJSONTyped(json, false);
635
- }
636
- function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
637
- if (value == null) {
638
- return value;
639
- }
640
- return {
641
- company_id: value["companyId"],
642
- error: value["error"],
643
- flag_id: value["flagId"],
644
- flag_key: value["flagKey"],
645
- reason: value["reason"],
646
- req_company: value["reqCompany"],
647
- req_user: value["reqUser"],
648
- rule_id: value["ruleId"],
649
- user_id: value["userId"],
650
- value: value["value"]
651
- };
652
- }
653
664
  function CheckFlagResponseFromJSON(json) {
654
665
  return CheckFlagResponseFromJSONTyped(json, false);
655
666
  }
@@ -662,6 +673,26 @@ function CheckFlagResponseFromJSONTyped(json, ignoreDiscriminator) {
662
673
  params: json["params"]
663
674
  };
664
675
  }
676
+ function TrialStatusFromJSON(json) {
677
+ return TrialStatusFromJSONTyped(json, false);
678
+ }
679
+ function TrialStatusFromJSONTyped(json, ignoreDiscriminator) {
680
+ return json;
681
+ }
682
+ function DatastreamCompanyPlanFromJSON(json) {
683
+ return DatastreamCompanyPlanFromJSONTyped(json, false);
684
+ }
685
+ function DatastreamCompanyPlanFromJSONTyped(json, ignoreDiscriminator) {
686
+ if (json == null) {
687
+ return json;
688
+ }
689
+ return {
690
+ id: json["id"],
691
+ name: json["name"],
692
+ trialEndDate: json["trial_end_date"] == null ? void 0 : new Date(json["trial_end_date"]),
693
+ trialStatus: json["trial_status"] == null ? void 0 : TrialStatusFromJSON(json["trial_status"])
694
+ };
695
+ }
665
696
  function CheckFlagsResponseDataFromJSON(json) {
666
697
  return CheckFlagsResponseDataFromJSONTyped(json, false);
667
698
  }
@@ -670,7 +701,8 @@ function CheckFlagsResponseDataFromJSONTyped(json, ignoreDiscriminator) {
670
701
  return json;
671
702
  }
672
703
  return {
673
- flags: json["flags"].map(CheckFlagResponseDataFromJSON)
704
+ flags: json["flags"].map(CheckFlagResponseDataFromJSON),
705
+ plan: json["plan"] == null ? void 0 : DatastreamCompanyPlanFromJSON(json["plan"])
674
706
  };
675
707
  }
676
708
  function CheckFlagsResponseFromJSON(json) {
@@ -685,6 +717,26 @@ function CheckFlagsResponseFromJSONTyped(json, ignoreDiscriminator) {
685
717
  params: json["params"]
686
718
  };
687
719
  }
720
+ function EventBodyFlagCheckToJSON(json) {
721
+ return EventBodyFlagCheckToJSONTyped(json, false);
722
+ }
723
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
724
+ if (value == null) {
725
+ return value;
726
+ }
727
+ return {
728
+ company_id: value["companyId"],
729
+ error: value["error"],
730
+ flag_id: value["flagId"],
731
+ flag_key: value["flagKey"],
732
+ reason: value["reason"],
733
+ req_company: value["reqCompany"],
734
+ req_user: value["reqUser"],
735
+ rule_id: value["ruleId"],
736
+ user_id: value["userId"],
737
+ value: value["value"]
738
+ };
739
+ }
688
740
  var RuleType = /* @__PURE__ */ ((RuleType2) => {
689
741
  RuleType2["GLOBAL_OVERRIDE"] = "global_override";
690
742
  RuleType2["COMPANY_OVERRIDE"] = "company_override";
@@ -705,6 +757,7 @@ var UsagePeriod = /* @__PURE__ */ ((UsagePeriod2) => {
705
757
  var CheckFlagReturnFromJSON = (json) => {
706
758
  const {
707
759
  companyId,
760
+ entitlement,
708
761
  error,
709
762
  featureAllocation,
710
763
  featureUsage,
@@ -722,24 +775,40 @@ var CheckFlagReturnFromJSON = (json) => {
722
775
  const featureUsageExceeded = !value && // if flag is not false, then we haven't exceeded usage
723
776
  (ruleType == "company_override_usage_exceeded" || // if the rule type is one of these, then we have exceeded usage
724
777
  ruleType == "plan_entitlement_usage_exceeded");
778
+ const resolvedAllocation = entitlement?.allocation ?? featureAllocation;
779
+ const resolvedUsage = entitlement?.usage ?? featureUsage;
780
+ const resolvedEvent = entitlement?.eventName ?? featureUsageEvent;
781
+ const resolvedPeriod = entitlement?.metricPeriod ?? featureUsagePeriod;
782
+ const resolvedResetAt = entitlement?.metricResetAt ?? featureUsageResetAt;
725
783
  return {
726
784
  featureUsageExceeded,
727
785
  companyId: companyId == null ? void 0 : companyId,
786
+ creditRemaining: entitlement?.creditRemaining == null ? void 0 : entitlement.creditRemaining,
728
787
  error: error == null ? void 0 : error,
729
- featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
730
- featureUsage: featureUsage == null ? void 0 : featureUsage,
731
- featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
732
- featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
733
- featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
788
+ featureAllocation: resolvedAllocation == null ? void 0 : resolvedAllocation,
789
+ featureUsage: resolvedUsage == null ? void 0 : resolvedUsage,
790
+ featureUsageEvent: resolvedEvent == null ? void 0 : resolvedEvent,
791
+ featureUsagePeriod: resolvedPeriod == null ? void 0 : resolvedPeriod,
792
+ featureUsageResetAt: resolvedResetAt == null ? void 0 : resolvedResetAt,
734
793
  flag,
735
794
  flagId: flagId == null ? void 0 : flagId,
736
795
  reason,
737
796
  ruleId: ruleId == null ? void 0 : ruleId,
738
797
  ruleType: ruleType == null ? void 0 : ruleType,
798
+ softLimit: entitlement?.softLimit == null ? void 0 : entitlement.softLimit,
739
799
  userId: userId == null ? void 0 : userId,
740
800
  value
741
801
  };
742
802
  };
803
+ var CheckPlanReturnFromJSON = (json) => {
804
+ const { id, name, trialEndDate, trialStatus } = DatastreamCompanyPlanFromJSON(json);
805
+ return {
806
+ id,
807
+ name,
808
+ trialEndDate: trialEndDate == null ? void 0 : trialEndDate,
809
+ trialStatus: trialStatus == null ? void 0 : trialStatus
810
+ };
811
+ };
743
812
  function contextString(context) {
744
813
  const sortedContext = Object.keys(context).reduce((acc, key) => {
745
814
  const sortedKeys = Object.keys(
@@ -754,7 +823,7 @@ function contextString(context) {
754
823
  }, {});
755
824
  return JSON.stringify(sortedContext);
756
825
  }
757
- var version = "1.2.6";
826
+ var version = "1.4.0";
758
827
  var anonymousIdKey = "schematicId";
759
828
  var Schematic = class {
760
829
  additionalHeaders = {};
@@ -771,11 +840,38 @@ var Schematic = class {
771
840
  flagValueListeners = {};
772
841
  isPending = true;
773
842
  isPendingListeners = /* @__PURE__ */ new Set();
843
+ planListeners = /* @__PURE__ */ new Set();
774
844
  storage;
775
845
  useWebSocket = false;
776
846
  checks = {};
777
847
  featureUsageEventMap = {};
848
+ planChecks = {};
778
849
  webSocketUrl = "wss://api.schematichq.com";
850
+ webSocketConnectionTimeout = 1e4;
851
+ webSocketReconnect = true;
852
+ webSocketMaxReconnectAttempts = 7;
853
+ // Max attempts after connection disrupted
854
+ webSocketMaxConnectionAttempts = 3;
855
+ // Max attempts for initial connection
856
+ webSocketInitialRetryDelay = 1e3;
857
+ webSocketMaxRetryDelay = 3e4;
858
+ wsReconnectAttempts = 0;
859
+ wsReconnectTimer = null;
860
+ wsIntentionalDisconnect = false;
861
+ currentWebSocket = null;
862
+ isConnecting = false;
863
+ maxEventQueueSize = 100;
864
+ // Prevent memory issues with very long network outages
865
+ maxEventRetries = 5;
866
+ // Maximum retry attempts for failed events
867
+ eventRetryInitialDelay = 1e3;
868
+ // Initial retry delay in ms
869
+ eventRetryMaxDelay = 3e4;
870
+ // Maximum retry delay in ms
871
+ retryTimer = null;
872
+ flagValueDefaults = {};
873
+ flagCheckDefaults = {};
874
+ fallbackCheckCache = {};
779
875
  constructor(apiKey, options) {
780
876
  this.apiKey = apiKey;
781
877
  this.eventQueue = [];
@@ -807,8 +903,13 @@ var Schematic = class {
807
903
  };
808
904
  if (options?.storage) {
809
905
  this.storage = options.storage;
810
- } else if (typeof localStorage !== "undefined") {
811
- this.storage = localStorage;
906
+ } else {
907
+ try {
908
+ if (typeof localStorage !== "undefined") {
909
+ this.storage = localStorage;
910
+ }
911
+ } catch {
912
+ }
812
913
  }
813
914
  if (options?.apiUrl !== void 0) {
814
915
  this.apiUrl = options.apiUrl;
@@ -819,11 +920,54 @@ var Schematic = class {
819
920
  if (options?.webSocketUrl !== void 0) {
820
921
  this.webSocketUrl = options.webSocketUrl;
821
922
  }
923
+ if (options?.webSocketConnectionTimeout !== void 0) {
924
+ this.webSocketConnectionTimeout = options.webSocketConnectionTimeout;
925
+ }
926
+ if (options?.webSocketReconnect !== void 0) {
927
+ this.webSocketReconnect = options.webSocketReconnect;
928
+ }
929
+ if (options?.webSocketMaxReconnectAttempts !== void 0) {
930
+ this.webSocketMaxReconnectAttempts = options.webSocketMaxReconnectAttempts;
931
+ }
932
+ if (options?.webSocketInitialRetryDelay !== void 0) {
933
+ this.webSocketInitialRetryDelay = options.webSocketInitialRetryDelay;
934
+ }
935
+ if (options?.webSocketMaxRetryDelay !== void 0) {
936
+ this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
937
+ }
938
+ if (options?.maxEventQueueSize !== void 0) {
939
+ this.maxEventQueueSize = options.maxEventQueueSize;
940
+ }
941
+ if (options?.maxEventRetries !== void 0) {
942
+ this.maxEventRetries = options.maxEventRetries;
943
+ }
944
+ if (options?.eventRetryInitialDelay !== void 0) {
945
+ this.eventRetryInitialDelay = options.eventRetryInitialDelay;
946
+ }
947
+ if (options?.eventRetryMaxDelay !== void 0) {
948
+ this.eventRetryMaxDelay = options.eventRetryMaxDelay;
949
+ }
950
+ if (options?.flagValueDefaults !== void 0) {
951
+ this.flagValueDefaults = options.flagValueDefaults;
952
+ }
953
+ if (options?.flagCheckDefaults !== void 0) {
954
+ this.flagCheckDefaults = options.flagCheckDefaults;
955
+ }
822
956
  if (typeof window !== "undefined" && window?.addEventListener) {
823
957
  window.addEventListener("beforeunload", () => {
824
958
  this.flushEventQueue();
825
959
  this.flushContextDependentEventQueue();
826
960
  });
961
+ if (this.useWebSocket) {
962
+ window.addEventListener("offline", () => {
963
+ this.debug("Browser went offline, closing WebSocket connection");
964
+ this.handleNetworkOffline();
965
+ });
966
+ window.addEventListener("online", () => {
967
+ this.debug("Browser came online, attempting to reconnect WebSocket");
968
+ this.handleNetworkOnline();
969
+ });
970
+ }
827
971
  }
828
972
  if (this.offlineEnabled) {
829
973
  this.debug(
@@ -833,24 +977,89 @@ var Schematic = class {
833
977
  this.debug("Initialized with debug mode enabled");
834
978
  }
835
979
  }
980
+ /**
981
+ * Resolve fallback value according to priority order:
982
+ * 1. Callsite fallback value (if provided)
983
+ * 2. Boolean value from flagCheckDefaults initialization option
984
+ * 3. Boolean value from flagValueDefaults initialization option
985
+ * 4. Default to false
986
+ */
987
+ resolveFallbackValue(key, callsiteFallback) {
988
+ if (callsiteFallback !== void 0) {
989
+ return callsiteFallback;
990
+ }
991
+ if (key in this.flagCheckDefaults) {
992
+ return this.flagCheckDefaults[key].value;
993
+ }
994
+ if (key in this.flagValueDefaults) {
995
+ return this.flagValueDefaults[key];
996
+ }
997
+ return false;
998
+ }
999
+ /**
1000
+ * Resolve complete CheckFlagReturn object according to priority order:
1001
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
1002
+ * 2. Use flagCheckDefaults if available for this flag
1003
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
1004
+ * 4. Default CheckFlagReturn with value: false
1005
+ */
1006
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
1007
+ if (callsiteFallback !== void 0) {
1008
+ return {
1009
+ flag: key,
1010
+ value: callsiteFallback,
1011
+ reason,
1012
+ error
1013
+ };
1014
+ }
1015
+ if (key in this.flagCheckDefaults) {
1016
+ const defaultReturn = this.flagCheckDefaults[key];
1017
+ return {
1018
+ ...defaultReturn,
1019
+ flag: key,
1020
+ // Ensure flag matches the requested key
1021
+ reason: error !== void 0 ? reason : defaultReturn.reason,
1022
+ error
1023
+ };
1024
+ }
1025
+ if (key in this.flagValueDefaults) {
1026
+ return {
1027
+ flag: key,
1028
+ value: this.flagValueDefaults[key],
1029
+ reason,
1030
+ error
1031
+ };
1032
+ }
1033
+ return {
1034
+ flag: key,
1035
+ value: false,
1036
+ reason,
1037
+ error
1038
+ };
1039
+ }
836
1040
  /**
837
1041
  * Get value for a single flag.
838
1042
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
839
- * new connection and then returns the requestedvalue. Falls back to REST API if WebSocket
840
- * connection fails.
1043
+ * new connection and then returns the requested value. Falls back to preconfigured fallback
1044
+ * values if WebSocket connection fails.
841
1045
  * In REST mode, makes an API call for each check.
842
1046
  */
843
1047
  async checkFlag(options) {
844
- const { fallback = false, key } = options;
1048
+ const { fallback, key } = options;
845
1049
  const context = options.context || this.context;
846
1050
  const contextStr = contextString(context);
847
1051
  this.debug(`checkFlag: ${key}`, { context, fallback });
848
1052
  if (this.isOffline()) {
1053
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
1054
+ key,
1055
+ fallback,
1056
+ "Offline mode - using initialization defaults"
1057
+ );
849
1058
  this.debug(`checkFlag offline result: ${key}`, {
850
- value: fallback,
1059
+ value: resolvedFallbackResult.value,
851
1060
  offlineMode: true
852
1061
  });
853
- return fallback;
1062
+ return resolvedFallbackResult.value;
854
1063
  }
855
1064
  if (!this.useWebSocket) {
856
1065
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -877,15 +1086,15 @@ var Schematic = class {
877
1086
  this.submitFlagCheckEvent(key, result, context);
878
1087
  return result.value;
879
1088
  }).catch((error) => {
880
- console.error("There was a problem with the fetch operation:", error);
881
- const errorResult = {
882
- flag: key,
883
- value: fallback,
884
- reason: "API request failed",
885
- error: error instanceof Error ? error.message : String(error)
886
- };
1089
+ console.warn("There was a problem with the fetch operation:", error);
1090
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1091
+ key,
1092
+ fallback,
1093
+ "API request failed",
1094
+ error instanceof Error ? error.message : String(error)
1095
+ );
887
1096
  this.submitFlagCheckEvent(key, errorResult, context);
888
- return fallback;
1097
+ return errorResult.value;
889
1098
  });
890
1099
  }
891
1100
  try {
@@ -895,38 +1104,52 @@ var Schematic = class {
895
1104
  return existingVals[key].value;
896
1105
  }
897
1106
  if (this.isOffline()) {
898
- return fallback;
1107
+ return this.resolveFallbackValue(key, fallback);
899
1108
  }
900
1109
  try {
901
1110
  await this.setContext(context);
902
1111
  } catch (error) {
903
- console.error(
904
- "WebSocket connection failed, falling back to REST:",
1112
+ console.warn(
1113
+ "WebSocket connection failed, using fallback value:",
905
1114
  error
906
1115
  );
907
- return this.fallbackToRest(key, context, fallback);
1116
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1117
+ key,
1118
+ fallback,
1119
+ "WebSocket connection failed",
1120
+ error instanceof Error ? error.message : String(error)
1121
+ );
1122
+ this.submitFlagCheckEvent(key, errorResult, context);
1123
+ return errorResult.value;
908
1124
  }
909
1125
  const contextVals = this.checks[contextStr] ?? {};
910
1126
  const flagCheck = contextVals[key];
911
- const result = flagCheck?.value ?? fallback;
1127
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
912
1128
  this.debug(
913
1129
  `checkFlag WebSocket result: ${key}`,
914
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1130
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
915
1131
  );
916
1132
  if (typeof flagCheck !== "undefined") {
917
1133
  this.submitFlagCheckEvent(key, flagCheck, context);
1134
+ } else {
1135
+ const fallbackResult = this.resolveFallbackCheckFlagReturn(
1136
+ key,
1137
+ fallback,
1138
+ "No flag values available"
1139
+ );
1140
+ this.submitFlagCheckEvent(key, fallbackResult, context);
918
1141
  }
919
1142
  return result;
920
1143
  } catch (error) {
921
1144
  console.error("Unexpected error in checkFlag:", error);
922
- const errorResult = {
923
- flag: key,
924
- value: fallback,
925
- reason: "Unexpected error in flag check",
926
- error: error instanceof Error ? error.message : String(error)
927
- };
1145
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1146
+ key,
1147
+ fallback,
1148
+ "Unexpected error in flag check",
1149
+ error instanceof Error ? error.message : String(error)
1150
+ );
928
1151
  this.submitFlagCheckEvent(key, errorResult, context);
929
- return fallback;
1152
+ return errorResult.value;
930
1153
  }
931
1154
  }
932
1155
  /**
@@ -938,6 +1161,53 @@ var Schematic = class {
938
1161
  console.log(`[Schematic] ${message}`, ...args);
939
1162
  }
940
1163
  }
1164
+ /**
1165
+ * Create a persistent message handler for websocket flag updates
1166
+ */
1167
+ createPersistentMessageHandler(context) {
1168
+ return (event) => {
1169
+ const message = JSON.parse(event.data);
1170
+ this.debug(`WebSocket persistent message received:`, message);
1171
+ if (!(contextString(context) in this.checks)) {
1172
+ this.checks[contextString(context)] = {};
1173
+ }
1174
+ (message.flags ?? []).forEach((flag) => {
1175
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1176
+ const contextStr = contextString(context);
1177
+ if (this.checks[contextStr] === void 0) {
1178
+ this.checks[contextStr] = {};
1179
+ }
1180
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1181
+ if (typeof flagCheck.featureUsageEvent === "string") {
1182
+ this.updateFeatureUsageEventMap(flagCheck);
1183
+ }
1184
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1185
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1186
+ }
1187
+ this.debug(
1188
+ `WebSocket flag update received. Notifying listeners for ${flag.flag}`,
1189
+ {
1190
+ flag: flag.flag,
1191
+ value: flagCheck.value,
1192
+ flagCheck
1193
+ }
1194
+ );
1195
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1196
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1197
+ });
1198
+ if (message.plan !== void 0 && message.plan !== null) {
1199
+ const plan = CheckPlanReturnFromJSON(message.plan);
1200
+ const contextStr = contextString(context);
1201
+ this.planChecks[contextStr] = plan;
1202
+ this.debug(`WebSocket plan update received. Notifying listeners`, {
1203
+ plan
1204
+ });
1205
+ this.notifyPlanListeners(plan);
1206
+ }
1207
+ this.flushContextDependentEventQueue();
1208
+ this.setIsPending(false);
1209
+ };
1210
+ }
941
1211
  /**
942
1212
  * Helper function to check if client is in offline mode
943
1213
  */
@@ -964,52 +1234,6 @@ var Schematic = class {
964
1234
  this.debug(`submitting flag check event:`, eventBody);
965
1235
  return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
966
1236
  }
967
- /**
968
- * Helper method for falling back to REST API when WebSocket connection fails
969
- */
970
- async fallbackToRest(key, context, fallback) {
971
- if (this.isOffline()) {
972
- this.debug(`fallbackToRest offline result: ${key}`, {
973
- value: fallback,
974
- offlineMode: true
975
- });
976
- return fallback;
977
- }
978
- try {
979
- const requestUrl = `${this.apiUrl}/flags/${key}/check`;
980
- const response = await fetch(requestUrl, {
981
- method: "POST",
982
- headers: {
983
- ...this.additionalHeaders ?? {},
984
- "Content-Type": "application/json;charset=UTF-8",
985
- "X-Schematic-Api-Key": this.apiKey
986
- },
987
- body: JSON.stringify(context)
988
- });
989
- if (!response.ok) {
990
- throw new Error("Network response was not ok");
991
- }
992
- const responseJson = await response.json();
993
- const data = CheckFlagResponseFromJSON(responseJson);
994
- this.debug(`fallbackToRest result: ${key}`, data);
995
- const result = CheckFlagReturnFromJSON(data.data);
996
- if (typeof result.featureUsageEvent === "string") {
997
- this.updateFeatureUsageEventMap(result);
998
- }
999
- this.submitFlagCheckEvent(key, result, context);
1000
- return result.value;
1001
- } catch (error) {
1002
- console.error("REST API call failed, using fallback value:", error);
1003
- const errorResult = {
1004
- flag: key,
1005
- value: fallback,
1006
- reason: "API request failed (fallback)",
1007
- error: error instanceof Error ? error.message : String(error)
1008
- };
1009
- this.submitFlagCheckEvent(key, errorResult, context);
1010
- return fallback;
1011
- }
1012
- }
1013
1237
  /**
1014
1238
  * Make an API call to fetch all flag values for a given context.
1015
1239
  * Recommended for use in REST mode only.
@@ -1048,7 +1272,7 @@ var Schematic = class {
1048
1272
  {}
1049
1273
  );
1050
1274
  }).catch((error) => {
1051
- console.error("There was a problem with the fetch operation:", error);
1275
+ console.warn("There was a problem with the fetch operation:", error);
1052
1276
  return {};
1053
1277
  });
1054
1278
  };
@@ -1059,14 +1283,12 @@ var Schematic = class {
1059
1283
  */
1060
1284
  identify = (body) => {
1061
1285
  this.debug(`identify:`, body);
1062
- try {
1063
- this.setContext({
1064
- company: body.company?.keys,
1065
- user: body.keys
1066
- });
1067
- } catch (error) {
1068
- console.error("Error setting context:", error);
1069
- }
1286
+ this.setContext({
1287
+ company: body.company?.keys,
1288
+ user: body.keys
1289
+ }).catch((error) => {
1290
+ console.warn("Error setting context:", error);
1291
+ });
1070
1292
  return this.handleEvent("identify", body);
1071
1293
  };
1072
1294
  /**
@@ -1085,16 +1307,52 @@ var Schematic = class {
1085
1307
  this.setIsPending(false);
1086
1308
  return Promise.resolve();
1087
1309
  }
1310
+ if (contextString(context) === contextString(this.context) && this.conn !== null && !this.isPending) {
1311
+ return;
1312
+ }
1088
1313
  try {
1089
1314
  this.setIsPending(true);
1090
1315
  if (!this.conn) {
1091
- this.conn = this.wsConnect();
1316
+ if (this.isConnecting) {
1317
+ this.debug(
1318
+ `Connection already in progress, waiting for it to complete`
1319
+ );
1320
+ while (this.isConnecting && this.conn === null) {
1321
+ await new Promise((resolve) => setTimeout(resolve, 10));
1322
+ }
1323
+ if (this.conn !== null) {
1324
+ const socket2 = await this.conn;
1325
+ await this.wsSendMessage(socket2, context);
1326
+ return;
1327
+ }
1328
+ }
1329
+ if (this.wsReconnectTimer !== null) {
1330
+ this.debug(
1331
+ `Cancelling scheduled reconnection, connecting immediately`
1332
+ );
1333
+ clearTimeout(this.wsReconnectTimer);
1334
+ this.wsReconnectTimer = null;
1335
+ }
1336
+ this.isConnecting = true;
1337
+ try {
1338
+ this.conn = this.wsConnect();
1339
+ const socket2 = await this.conn;
1340
+ this.isConnecting = false;
1341
+ await this.wsSendMessage(socket2, context);
1342
+ return;
1343
+ } catch (error) {
1344
+ this.isConnecting = false;
1345
+ throw error;
1346
+ }
1092
1347
  }
1093
1348
  const socket = await this.conn;
1094
1349
  await this.wsSendMessage(socket, context);
1095
1350
  } catch (error) {
1096
- console.error("Failed to establish WebSocket connection:", error);
1097
- throw error;
1351
+ console.warn("Failed to establish WebSocket connection:", error);
1352
+ this.context = context;
1353
+ this.flushContextDependentEventQueue();
1354
+ this.setIsPending(false);
1355
+ this.attemptReconnect();
1098
1356
  }
1099
1357
  };
1100
1358
  /**
@@ -1217,11 +1475,57 @@ var Schematic = class {
1217
1475
  }
1218
1476
  }
1219
1477
  };
1220
- flushEventQueue = () => {
1221
- while (this.eventQueue.length > 0) {
1222
- const event = this.eventQueue.shift();
1223
- if (event) {
1224
- this.sendEvent(event);
1478
+ startRetryTimer = () => {
1479
+ if (this.retryTimer !== null) {
1480
+ return;
1481
+ }
1482
+ this.retryTimer = setInterval(() => {
1483
+ this.flushEventQueue().catch((error) => {
1484
+ this.debug("Error in retry timer flush:", error);
1485
+ });
1486
+ if (this.eventQueue.length === 0) {
1487
+ this.stopRetryTimer();
1488
+ }
1489
+ }, 5e3);
1490
+ this.debug("Started retry timer");
1491
+ };
1492
+ stopRetryTimer = () => {
1493
+ if (this.retryTimer !== null) {
1494
+ clearInterval(this.retryTimer);
1495
+ this.retryTimer = null;
1496
+ this.debug("Stopped retry timer");
1497
+ }
1498
+ };
1499
+ flushEventQueue = async () => {
1500
+ if (this.eventQueue.length === 0) {
1501
+ return;
1502
+ }
1503
+ const now = Date.now();
1504
+ const readyEvents = [];
1505
+ const notReadyEvents = [];
1506
+ for (const event of this.eventQueue) {
1507
+ if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
1508
+ readyEvents.push(event);
1509
+ } else {
1510
+ notReadyEvents.push(event);
1511
+ }
1512
+ }
1513
+ if (readyEvents.length === 0) {
1514
+ this.debug(
1515
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1516
+ );
1517
+ return;
1518
+ }
1519
+ this.debug(
1520
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1521
+ );
1522
+ this.eventQueue = notReadyEvents;
1523
+ for (const event of readyEvents) {
1524
+ try {
1525
+ await this.sendEvent(event);
1526
+ this.debug(`Queued event sent successfully:`, event.type);
1527
+ } catch (error) {
1528
+ this.debug(`Failed to send queued event:`, error);
1225
1529
  }
1226
1530
  }
1227
1531
  };
@@ -1269,12 +1573,47 @@ var Schematic = class {
1269
1573
  },
1270
1574
  body: payload
1271
1575
  });
1576
+ if (!response.ok) {
1577
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1578
+ }
1272
1579
  this.debug(`event sent:`, {
1273
1580
  status: response.status,
1274
1581
  statusText: response.statusText
1275
1582
  });
1276
1583
  } catch (error) {
1277
- console.error("Error sending Schematic event: ", error);
1584
+ const retryCount = (event.retry_count ?? 0) + 1;
1585
+ if (retryCount <= this.maxEventRetries) {
1586
+ this.debug(
1587
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1588
+ error
1589
+ );
1590
+ const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1591
+ const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1592
+ const nextRetryAt = Date.now() + jitterDelay;
1593
+ const retryEvent = {
1594
+ ...event,
1595
+ retry_count: retryCount,
1596
+ next_retry_at: nextRetryAt
1597
+ };
1598
+ if (this.eventQueue.length < this.maxEventQueueSize) {
1599
+ this.eventQueue.push(retryEvent);
1600
+ this.debug(
1601
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1602
+ );
1603
+ } else {
1604
+ this.debug(
1605
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1606
+ );
1607
+ this.eventQueue.shift();
1608
+ this.eventQueue.push(retryEvent);
1609
+ }
1610
+ this.startRetryTimer();
1611
+ } else {
1612
+ this.debug(
1613
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1614
+ error
1615
+ );
1616
+ }
1278
1617
  }
1279
1618
  return Promise.resolve();
1280
1619
  };
@@ -1285,6 +1624,130 @@ var Schematic = class {
1285
1624
  /**
1286
1625
  * Websocket management
1287
1626
  */
1627
+ /**
1628
+ * Force an immediate WebSocket reconnection.
1629
+ * This is useful when the application returns from a background state (e.g., mobile app
1630
+ * coming back to foreground) and wants to immediately re-establish the connection
1631
+ * rather than waiting for the exponential backoff timer.
1632
+ *
1633
+ * This method will:
1634
+ * - Cancel any pending reconnection timer
1635
+ * - Reset the reconnection attempt counter
1636
+ * - Close any existing connection
1637
+ * - Immediately attempt to reconnect
1638
+ * - Re-send the current context to get fresh flag values
1639
+ *
1640
+ * Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
1641
+ *
1642
+ * @example
1643
+ * ```typescript
1644
+ * // React Native example: reconnect when app comes to foreground
1645
+ * useEffect(() => {
1646
+ * const subscription = AppState.addEventListener("change", (state) => {
1647
+ * if (state === "active") {
1648
+ * client.forceReconnect();
1649
+ * }
1650
+ * });
1651
+ * return () => subscription.remove();
1652
+ * }, [client]);
1653
+ * ```
1654
+ */
1655
+ forceReconnect = async () => {
1656
+ return this.reconnect({ force: true });
1657
+ };
1658
+ /**
1659
+ * Reconnect the WebSocket connection only if the current connection is unhealthy.
1660
+ * This is useful when the application returns from a background state and wants to
1661
+ * ensure a healthy connection exists, but doesn't need to force a reconnection if
1662
+ * the connection is still active.
1663
+ *
1664
+ * This method will:
1665
+ * - Check if an existing connection is healthy (readyState === OPEN)
1666
+ * - If healthy, return immediately without reconnecting
1667
+ * - If unhealthy, perform the same reconnection logic as forceReconnect()
1668
+ *
1669
+ * Use this when you want efficient reconnection that avoids unnecessary disconnects.
1670
+ *
1671
+ * @example
1672
+ * ```typescript
1673
+ * // React Native example: reconnect only if needed when app comes to foreground
1674
+ * useEffect(() => {
1675
+ * const subscription = AppState.addEventListener("change", (state) => {
1676
+ * if (state === "active") {
1677
+ * client.reconnectIfNeeded();
1678
+ * }
1679
+ * });
1680
+ * return () => subscription.remove();
1681
+ * }, [client]);
1682
+ * ```
1683
+ */
1684
+ reconnectIfNeeded = async () => {
1685
+ return this.reconnect({ force: false });
1686
+ };
1687
+ /**
1688
+ * Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
1689
+ */
1690
+ reconnect = async (options) => {
1691
+ const { force } = options;
1692
+ const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
1693
+ if (this.isOffline()) {
1694
+ this.debug(`${methodName}: skipped (offline mode)`);
1695
+ return Promise.resolve();
1696
+ }
1697
+ if (!force && this.conn !== null) {
1698
+ try {
1699
+ const existingSocket = await this.conn;
1700
+ if (existingSocket.readyState === WebSocket.OPEN) {
1701
+ this.debug(`${methodName}: connection is healthy, skipping`);
1702
+ return Promise.resolve();
1703
+ }
1704
+ } catch {
1705
+ }
1706
+ }
1707
+ this.debug(
1708
+ `${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
1709
+ );
1710
+ this.wsIntentionalDisconnect = false;
1711
+ if (this.wsReconnectTimer !== null) {
1712
+ this.debug(`${methodName}: cancelling pending reconnection timer`);
1713
+ clearTimeout(this.wsReconnectTimer);
1714
+ this.wsReconnectTimer = null;
1715
+ }
1716
+ this.wsReconnectAttempts = 0;
1717
+ if (this.conn !== null) {
1718
+ this.debug(`${methodName}: closing existing connection`);
1719
+ try {
1720
+ const existingSocket = await this.conn;
1721
+ if (this.currentWebSocket === existingSocket) {
1722
+ this.currentWebSocket = null;
1723
+ }
1724
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1725
+ existingSocket.close();
1726
+ }
1727
+ } catch (error) {
1728
+ this.debug(`${methodName}: error closing existing connection:`, error);
1729
+ }
1730
+ this.conn = null;
1731
+ this.isConnecting = false;
1732
+ }
1733
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1734
+ this.debug(`${methodName}: reconnecting with existing context`);
1735
+ try {
1736
+ this.isConnecting = true;
1737
+ this.conn = this.wsConnect();
1738
+ const socket = await this.conn;
1739
+ this.isConnecting = false;
1740
+ await this.wsSendMessage(socket, this.context, true);
1741
+ this.debug(`${methodName}: reconnection successful`);
1742
+ } catch (error) {
1743
+ this.isConnecting = false;
1744
+ this.debug(`${methodName}: reconnection failed:`, error);
1745
+ }
1746
+ } else {
1747
+ this.debug(`${methodName}: no context set, skipping reconnection`);
1748
+ }
1749
+ return Promise.resolve();
1750
+ };
1288
1751
  /**
1289
1752
  * If using websocket mode, close the connection when done.
1290
1753
  * In offline mode, this is a no-op.
@@ -1294,95 +1757,307 @@ var Schematic = class {
1294
1757
  this.debug("cleanup: skipped (offline mode)");
1295
1758
  return Promise.resolve();
1296
1759
  }
1760
+ this.wsIntentionalDisconnect = true;
1761
+ if (this.wsReconnectTimer !== null) {
1762
+ clearTimeout(this.wsReconnectTimer);
1763
+ this.wsReconnectTimer = null;
1764
+ }
1765
+ this.stopRetryTimer();
1297
1766
  if (this.conn) {
1298
1767
  try {
1299
1768
  const socket = await this.conn;
1769
+ if (this.currentWebSocket === socket) {
1770
+ this.debug(`Cleaning up current websocket tracking`);
1771
+ this.currentWebSocket = null;
1772
+ }
1300
1773
  socket.close();
1301
1774
  } catch (error) {
1302
- console.error("Error during cleanup:", error);
1775
+ console.warn("Error during cleanup:", error);
1303
1776
  } finally {
1304
1777
  this.conn = null;
1778
+ this.currentWebSocket = null;
1779
+ this.isConnecting = false;
1305
1780
  }
1306
1781
  }
1307
1782
  };
1308
- // Open a websocket connection
1309
- wsConnect = () => {
1783
+ /**
1784
+ * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
1785
+ * This helps prevent dogpiling when the server recovers from an outage.
1786
+ */
1787
+ calculateReconnectDelay = () => {
1788
+ const exponentialDelay = this.webSocketInitialRetryDelay * Math.pow(2, this.wsReconnectAttempts);
1789
+ const cappedDelay = Math.min(exponentialDelay, this.webSocketMaxRetryDelay);
1790
+ const jitter = Math.random() * cappedDelay * 0.5;
1791
+ const totalDelay = cappedDelay + jitter;
1792
+ this.debug(
1793
+ `Reconnect delay calculated: ${totalDelay.toFixed(0)}ms (attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts})`
1794
+ );
1795
+ return totalDelay;
1796
+ };
1797
+ /**
1798
+ * Handle browser going offline
1799
+ */
1800
+ handleNetworkOffline = async () => {
1801
+ if (this.conn !== null) {
1802
+ try {
1803
+ const socket = await this.conn;
1804
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1805
+ socket.close();
1806
+ }
1807
+ } catch (error) {
1808
+ this.debug("Error closing connection on offline:", error);
1809
+ }
1810
+ this.conn = null;
1811
+ }
1812
+ if (this.wsReconnectTimer !== null) {
1813
+ clearTimeout(this.wsReconnectTimer);
1814
+ this.wsReconnectTimer = null;
1815
+ }
1816
+ };
1817
+ /**
1818
+ * Handle browser coming back online
1819
+ */
1820
+ handleNetworkOnline = () => {
1821
+ this.debug(
1822
+ "Network online, attempting reconnection and flushing queued events"
1823
+ );
1824
+ this.wsReconnectAttempts = 0;
1825
+ if (this.wsReconnectTimer !== null) {
1826
+ clearTimeout(this.wsReconnectTimer);
1827
+ this.wsReconnectTimer = null;
1828
+ }
1829
+ this.flushEventQueue().catch((error) => {
1830
+ this.debug("Error flushing event queue on network online:", error);
1831
+ });
1832
+ this.attemptReconnect();
1833
+ };
1834
+ /**
1835
+ * Attempt to reconnect the WebSocket connection with exponential backoff.
1836
+ * Called automatically when the connection closes unexpectedly.
1837
+ */
1838
+ attemptReconnect = () => {
1839
+ if (this.wsReconnectAttempts >= this.webSocketMaxReconnectAttempts) {
1840
+ this.debug(
1841
+ `Maximum reconnection attempts (${this.webSocketMaxReconnectAttempts}) reached, giving up`
1842
+ );
1843
+ return;
1844
+ }
1845
+ if (this.wsReconnectTimer !== null) {
1846
+ this.debug(
1847
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1848
+ );
1849
+ return;
1850
+ }
1851
+ const delay = this.calculateReconnectDelay();
1852
+ this.debug(
1853
+ `Scheduling reconnection attempt ${this.wsReconnectAttempts + 1}/${this.webSocketMaxReconnectAttempts} in ${delay.toFixed(0)}ms`
1854
+ );
1855
+ this.wsReconnectTimer = setTimeout(async () => {
1856
+ this.wsReconnectTimer = null;
1857
+ this.wsReconnectAttempts++;
1858
+ this.debug(
1859
+ `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1860
+ );
1861
+ try {
1862
+ if (this.conn !== null) {
1863
+ this.debug(`Cleaning up existing connection before reconnection`);
1864
+ try {
1865
+ const existingSocket = await this.conn;
1866
+ if (this.currentWebSocket === existingSocket) {
1867
+ this.debug(`Existing websocket is current, will be replaced`);
1868
+ this.currentWebSocket = null;
1869
+ }
1870
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1871
+ existingSocket.close();
1872
+ }
1873
+ } catch (error) {
1874
+ this.debug(`Error cleaning up existing connection:`, error);
1875
+ }
1876
+ this.conn = null;
1877
+ this.currentWebSocket = null;
1878
+ this.isConnecting = false;
1879
+ }
1880
+ this.isConnecting = true;
1881
+ try {
1882
+ this.conn = this.wsConnect();
1883
+ const socket = await this.conn;
1884
+ this.isConnecting = false;
1885
+ this.debug(`Reconnection context check:`, {
1886
+ hasCompany: this.context.company !== void 0,
1887
+ hasUser: this.context.user !== void 0,
1888
+ context: this.context
1889
+ });
1890
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1891
+ this.debug(`Reconnected, force re-sending context`);
1892
+ await this.wsSendMessage(socket, this.context, true);
1893
+ } else {
1894
+ this.debug(
1895
+ `No context to re-send after reconnection - websocket ready for new context`
1896
+ );
1897
+ this.debug(
1898
+ `Setting up tracking for reconnected websocket (no context to send)`
1899
+ );
1900
+ this.currentWebSocket = socket;
1901
+ }
1902
+ this.flushEventQueue().catch((error) => {
1903
+ this.debug(
1904
+ "Error flushing event queue after websocket reconnection:",
1905
+ error
1906
+ );
1907
+ });
1908
+ this.debug(`Reconnection successful`);
1909
+ } catch (error) {
1910
+ this.isConnecting = false;
1911
+ throw error;
1912
+ }
1913
+ } catch (error) {
1914
+ this.debug(`Reconnection attempt failed:`, error);
1915
+ }
1916
+ }, delay);
1917
+ };
1918
+ // Open a websocket connection with retry logic for timeouts
1919
+ wsConnect = async () => {
1310
1920
  if (this.isOffline()) {
1311
1921
  this.debug("wsConnect: skipped (offline mode)");
1312
- return Promise.reject(
1313
- new Error("WebSocket connection skipped in offline mode")
1314
- );
1922
+ throw new Error("WebSocket connection skipped in offline mode");
1315
1923
  }
1924
+ let lastError = null;
1925
+ const maxAttempts = this.webSocketMaxConnectionAttempts;
1926
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1927
+ try {
1928
+ const socket = await this.wsConnectOnce();
1929
+ this.wsReconnectAttempts = 0;
1930
+ return socket;
1931
+ } catch (error) {
1932
+ lastError = error instanceof Error ? error : new Error(String(error));
1933
+ const isTimeout = lastError.message === "WebSocket connection timeout";
1934
+ if (!isTimeout) {
1935
+ this.debug(
1936
+ `WebSocket connection failed with non-timeout error, not retrying:`,
1937
+ lastError.message
1938
+ );
1939
+ throw lastError;
1940
+ }
1941
+ if (attempt < maxAttempts - 1) {
1942
+ const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
1943
+ const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
1944
+ const jitter = cappedDelay * 0.2 * Math.random();
1945
+ const delay = cappedDelay + jitter;
1946
+ this.debug(
1947
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
1948
+ );
1949
+ await new Promise((resolve) => setTimeout(resolve, delay));
1950
+ } else {
1951
+ this.debug(
1952
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
1953
+ );
1954
+ }
1955
+ }
1956
+ }
1957
+ throw lastError ?? new Error("WebSocket connection failed");
1958
+ };
1959
+ // Single attempt to open a websocket connection (no retry logic)
1960
+ wsConnectOnce = () => {
1316
1961
  return new Promise((resolve, reject) => {
1317
1962
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1318
1963
  this.debug(`connecting to WebSocket:`, wsUrl);
1319
1964
  const webSocket = new WebSocket(wsUrl);
1965
+ const connectionId = Math.random().toString(36).substring(7);
1966
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1967
+ let timeoutId = null;
1968
+ let isResolved = false;
1969
+ timeoutId = setTimeout(() => {
1970
+ if (!isResolved) {
1971
+ isResolved = true;
1972
+ this.debug(
1973
+ `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1974
+ );
1975
+ webSocket.close();
1976
+ reject(new Error("WebSocket connection timeout"));
1977
+ }
1978
+ }, this.webSocketConnectionTimeout);
1320
1979
  webSocket.onopen = () => {
1321
- this.debug(`WebSocket connection opened`);
1980
+ if (isResolved) return;
1981
+ isResolved = true;
1982
+ if (timeoutId !== null) {
1983
+ clearTimeout(timeoutId);
1984
+ }
1985
+ this.wsIntentionalDisconnect = false;
1986
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1322
1987
  resolve(webSocket);
1323
1988
  };
1324
1989
  webSocket.onerror = (error) => {
1325
- this.debug(`WebSocket connection error:`, error);
1326
- reject(error);
1990
+ if (isResolved) return;
1991
+ isResolved = true;
1992
+ if (timeoutId !== null) {
1993
+ clearTimeout(timeoutId);
1994
+ }
1995
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1996
+ const wrappedError = new Error(
1997
+ "WebSocket connection failed during handshake"
1998
+ );
1999
+ reject(wrappedError);
1327
2000
  };
1328
2001
  webSocket.onclose = () => {
1329
- this.debug(`WebSocket connection closed`);
2002
+ if (timeoutId !== null) {
2003
+ clearTimeout(timeoutId);
2004
+ }
2005
+ this.debug(`WebSocket connection ${connectionId} closed`);
1330
2006
  this.conn = null;
2007
+ if (this.currentWebSocket === webSocket) {
2008
+ this.currentWebSocket = null;
2009
+ this.isConnecting = false;
2010
+ }
2011
+ if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
2012
+ this.attemptReconnect();
2013
+ }
1331
2014
  };
1332
2015
  });
1333
2016
  };
1334
2017
  // Send a message on the websocket indicating interest in a particular evaluation context
1335
2018
  // and wait for the initial set of flag values to be returned
1336
- wsSendMessage = (socket, context) => {
2019
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1337
2020
  if (this.isOffline()) {
1338
2021
  this.debug("wsSendMessage: skipped (offline mode)");
1339
2022
  this.setIsPending(false);
1340
2023
  return Promise.resolve();
1341
2024
  }
1342
2025
  return new Promise((resolve, reject) => {
1343
- if (contextString(context) == contextString(this.context)) {
2026
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1344
2027
  this.debug(`WebSocket context unchanged, skipping update`);
1345
2028
  return resolve(this.setIsPending(false));
1346
2029
  }
1347
- this.debug(`WebSocket context updated:`, context);
2030
+ this.debug(
2031
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
2032
+ context
2033
+ );
1348
2034
  this.context = context;
1349
2035
  const sendMessage = () => {
1350
2036
  let resolved = false;
2037
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1351
2038
  const messageHandler = (event) => {
1352
- const message = JSON.parse(event.data);
1353
- this.debug(`WebSocket message received:`, message);
1354
- if (!(contextString(context) in this.checks)) {
1355
- this.checks[contextString(context)] = {};
1356
- }
1357
- (message.flags ?? []).forEach((flag) => {
1358
- const flagCheck = CheckFlagReturnFromJSON(flag);
1359
- const contextStr = contextString(context);
1360
- if (this.checks[contextStr] === void 0) {
1361
- this.checks[contextStr] = {};
1362
- }
1363
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1364
- this.debug(`WebSocket flag update:`, {
1365
- flag: flagCheck.flag,
1366
- value: flagCheck.value,
1367
- flagCheck
1368
- });
1369
- if (typeof flagCheck.featureUsageEvent === "string") {
1370
- this.updateFeatureUsageEventMap(flagCheck);
1371
- }
1372
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1373
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1374
- }
1375
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1376
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1377
- });
1378
- this.flushContextDependentEventQueue();
1379
- this.setIsPending(false);
2039
+ persistentMessageHandler(event);
1380
2040
  if (!resolved) {
1381
2041
  resolved = true;
1382
2042
  resolve();
1383
2043
  }
1384
2044
  };
1385
2045
  socket.addEventListener("message", messageHandler);
2046
+ socket.addEventListener("close", (event) => {
2047
+ if (!resolved) {
2048
+ resolved = true;
2049
+ if (event.code === 4001) {
2050
+ reject(
2051
+ new Error(
2052
+ `Authentication failed: ${event.reason !== "" ? event.reason : "Invalid API key"}`
2053
+ )
2054
+ );
2055
+ } else {
2056
+ reject(new Error("WebSocket connection closed unexpectedly"));
2057
+ }
2058
+ }
2059
+ });
2060
+ this.currentWebSocket = socket;
1386
2061
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1387
2062
  const messagePayload = {
1388
2063
  apiKey: this.apiKey,
@@ -1418,21 +2093,49 @@ var Schematic = class {
1418
2093
  };
1419
2094
  };
1420
2095
  setIsPending = (isPending) => {
2096
+ if (this.isPending === isPending) return;
1421
2097
  this.isPending = isPending;
1422
2098
  this.isPendingListeners.forEach(
1423
2099
  (listener) => notifyPendingListener(listener, isPending)
1424
2100
  );
1425
2101
  };
2102
+ getPlan = () => {
2103
+ const contextStr = contextString(this.context);
2104
+ const plan = this.planChecks[contextStr];
2105
+ return plan;
2106
+ };
1426
2107
  // flag checks state
1427
2108
  getFlagCheck = (flagKey) => {
1428
2109
  const contextStr = contextString(this.context);
1429
2110
  const checks = this.checks[contextStr] ?? {};
1430
- return checks[flagKey];
2111
+ const check = checks[flagKey];
2112
+ if (check !== void 0) {
2113
+ return check;
2114
+ }
2115
+ if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
2116
+ if (!(flagKey in this.fallbackCheckCache)) {
2117
+ this.fallbackCheckCache[flagKey] = this.resolveFallbackCheckFlagReturn(
2118
+ flagKey,
2119
+ void 0,
2120
+ "Default value used"
2121
+ );
2122
+ }
2123
+ return this.fallbackCheckCache[flagKey];
2124
+ }
2125
+ return void 0;
1431
2126
  };
1432
2127
  // flagValues state
1433
2128
  getFlagValue = (flagKey) => {
1434
- const check = this.getFlagCheck(flagKey);
1435
- return check?.value;
2129
+ const contextStr = contextString(this.context);
2130
+ const checks = this.checks[contextStr] ?? {};
2131
+ const check = checks[flagKey];
2132
+ if (check?.value !== void 0) {
2133
+ return check.value;
2134
+ }
2135
+ if (flagKey in this.flagCheckDefaults || flagKey in this.flagValueDefaults) {
2136
+ return this.resolveFallbackValue(flagKey);
2137
+ }
2138
+ return void 0;
1436
2139
  };
1437
2140
  /** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
1438
2141
  addFlagValueListener = (flagKey, listener) => {
@@ -1454,6 +2157,12 @@ var Schematic = class {
1454
2157
  this.flagCheckListeners[flagKey].delete(listener);
1455
2158
  };
1456
2159
  };
2160
+ addPlanListener = (listener) => {
2161
+ this.planListeners.add(listener);
2162
+ return () => {
2163
+ this.planListeners.delete(listener);
2164
+ };
2165
+ };
1457
2166
  notifyFlagCheckListeners = (flagKey, check) => {
1458
2167
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1459
2168
  if (listeners.size > 0) {
@@ -1490,7 +2199,32 @@ var Schematic = class {
1490
2199
  { value }
1491
2200
  );
1492
2201
  }
1493
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
2202
+ listeners.forEach((listener, index) => {
2203
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
2204
+ flagKey,
2205
+ value
2206
+ });
2207
+ notifyFlagValueListener(listener, value);
2208
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
2209
+ flagKey,
2210
+ value
2211
+ });
2212
+ });
2213
+ };
2214
+ notifyPlanListeners = (value) => {
2215
+ const listeners = this.planListeners ?? [];
2216
+ if (listeners.size > 0) {
2217
+ this.debug(`Notifying ${listeners.size} plan listeners`, { value });
2218
+ }
2219
+ listeners.forEach((listener, index) => {
2220
+ this.debug(`Calling listener ${index} for plan`, {
2221
+ value
2222
+ });
2223
+ notifyPlanListener(listener, value);
2224
+ this.debug(`Listener ${index} for plan completed`, {
2225
+ value
2226
+ });
2227
+ });
1494
2228
  };
1495
2229
  };
1496
2230
  var notifyPendingListener = (listener, value) => {
@@ -1514,12 +2248,19 @@ var notifyFlagValueListener = (listener, value) => {
1514
2248
  listener();
1515
2249
  }
1516
2250
  };
2251
+ var notifyPlanListener = (listener, value) => {
2252
+ if (listener.length > 0) {
2253
+ listener(value);
2254
+ } else {
2255
+ listener();
2256
+ }
2257
+ };
1517
2258
 
1518
2259
  // src/context/schematic.ts
1519
2260
  import { inject, onScopeDispose } from "vue";
1520
2261
 
1521
2262
  // src/version.ts
1522
- var version2 = "0.1.0";
2263
+ var version2 = "1.4.0";
1523
2264
 
1524
2265
  // src/context/schematic.ts
1525
2266
  var SchematicSymbol = Symbol("schematic");
@@ -1635,6 +2376,23 @@ var useSchematicEntitlement = (key, opts) => {
1635
2376
  userId: computed(() => flagCheck.value.userId)
1636
2377
  };
1637
2378
  };
2379
+ var useSchematicPlan = (opts) => {
2380
+ const client = useSchematicClient(opts);
2381
+ const fallback = opts?.fallback;
2382
+ const plan = ref(
2383
+ client.getPlan() ?? fallback
2384
+ );
2385
+ let unsubscribe = null;
2386
+ onMounted(() => {
2387
+ unsubscribe = client.addPlanListener((value) => {
2388
+ plan.value = value;
2389
+ });
2390
+ });
2391
+ onScopeDispose2(() => {
2392
+ unsubscribe?.();
2393
+ });
2394
+ return plan;
2395
+ };
1638
2396
  var useSchematicIsPending = (opts) => {
1639
2397
  const client = useSchematicClient(opts);
1640
2398
  const isPending = ref(client.getIsPending());
@@ -1660,7 +2418,8 @@ export {
1660
2418
  useSchematicEntitlement,
1661
2419
  useSchematicEvents,
1662
2420
  useSchematicFlag,
1663
- useSchematicIsPending
2421
+ useSchematicIsPending,
2422
+ useSchematicPlan
1664
2423
  };
1665
2424
  /*! Bundled license information:
1666
2425