@schematichq/schematic-react 1.2.7 → 1.2.9

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.
@@ -799,7 +799,7 @@ function contextString(context) {
799
799
  }, {});
800
800
  return JSON.stringify(sortedContext);
801
801
  }
802
- var version = "1.2.7";
802
+ var version = "1.2.9";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -829,6 +829,17 @@ var Schematic = class {
829
829
  wsReconnectAttempts = 0;
830
830
  wsReconnectTimer = null;
831
831
  wsIntentionalDisconnect = false;
832
+ maxEventQueueSize = 100;
833
+ // Prevent memory issues with very long network outages
834
+ maxEventRetries = 5;
835
+ // Maximum retry attempts for failed events
836
+ eventRetryInitialDelay = 1e3;
837
+ // Initial retry delay in ms
838
+ eventRetryMaxDelay = 3e4;
839
+ // Maximum retry delay in ms
840
+ retryTimer = null;
841
+ flagValueDefaults = {};
842
+ flagCheckDefaults = {};
832
843
  constructor(apiKey, options) {
833
844
  this.apiKey = apiKey;
834
845
  this.eventQueue = [];
@@ -887,6 +898,24 @@ var Schematic = class {
887
898
  if (options?.webSocketMaxRetryDelay !== void 0) {
888
899
  this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
889
900
  }
901
+ if (options?.maxEventQueueSize !== void 0) {
902
+ this.maxEventQueueSize = options.maxEventQueueSize;
903
+ }
904
+ if (options?.maxEventRetries !== void 0) {
905
+ this.maxEventRetries = options.maxEventRetries;
906
+ }
907
+ if (options?.eventRetryInitialDelay !== void 0) {
908
+ this.eventRetryInitialDelay = options.eventRetryInitialDelay;
909
+ }
910
+ if (options?.eventRetryMaxDelay !== void 0) {
911
+ this.eventRetryMaxDelay = options.eventRetryMaxDelay;
912
+ }
913
+ if (options?.flagValueDefaults !== void 0) {
914
+ this.flagValueDefaults = options.flagValueDefaults;
915
+ }
916
+ if (options?.flagCheckDefaults !== void 0) {
917
+ this.flagCheckDefaults = options.flagCheckDefaults;
918
+ }
890
919
  if (typeof window !== "undefined" && window?.addEventListener) {
891
920
  window.addEventListener("beforeunload", () => {
892
921
  this.flushEventQueue();
@@ -911,6 +940,62 @@ var Schematic = class {
911
940
  this.debug("Initialized with debug mode enabled");
912
941
  }
913
942
  }
943
+ /**
944
+ * Resolve fallback value according to priority order:
945
+ * 1. Callsite fallback value (if provided)
946
+ * 2. Initialization fallback value (flagValueDefaults)
947
+ * 3. Default to false
948
+ */
949
+ resolveFallbackValue(key, callsiteFallback) {
950
+ if (callsiteFallback !== void 0) {
951
+ return callsiteFallback;
952
+ }
953
+ if (key in this.flagValueDefaults) {
954
+ return this.flagValueDefaults[key];
955
+ }
956
+ return false;
957
+ }
958
+ /**
959
+ * Resolve complete CheckFlagReturn object according to priority order:
960
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
961
+ * 2. Use flagCheckDefaults if available for this flag
962
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
963
+ * 4. Default CheckFlagReturn with value: false
964
+ */
965
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
966
+ if (callsiteFallback !== void 0) {
967
+ return {
968
+ flag: key,
969
+ value: callsiteFallback,
970
+ reason,
971
+ error
972
+ };
973
+ }
974
+ if (key in this.flagCheckDefaults) {
975
+ const defaultReturn = this.flagCheckDefaults[key];
976
+ return {
977
+ ...defaultReturn,
978
+ flag: key,
979
+ // Ensure flag matches the requested key
980
+ reason: error !== void 0 ? reason : defaultReturn.reason,
981
+ error
982
+ };
983
+ }
984
+ if (key in this.flagValueDefaults) {
985
+ return {
986
+ flag: key,
987
+ value: this.flagValueDefaults[key],
988
+ reason,
989
+ error
990
+ };
991
+ }
992
+ return {
993
+ flag: key,
994
+ value: false,
995
+ reason,
996
+ error
997
+ };
998
+ }
914
999
  /**
915
1000
  * Get value for a single flag.
916
1001
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -919,16 +1004,21 @@ var Schematic = class {
919
1004
  * In REST mode, makes an API call for each check.
920
1005
  */
921
1006
  async checkFlag(options) {
922
- const { fallback = false, key } = options;
1007
+ const { fallback, key } = options;
923
1008
  const context = options.context || this.context;
924
1009
  const contextStr = contextString(context);
925
1010
  this.debug(`checkFlag: ${key}`, { context, fallback });
926
1011
  if (this.isOffline()) {
1012
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
1013
+ key,
1014
+ fallback,
1015
+ "Offline mode - using initialization defaults"
1016
+ );
927
1017
  this.debug(`checkFlag offline result: ${key}`, {
928
- value: fallback,
1018
+ value: resolvedFallbackResult.value,
929
1019
  offlineMode: true
930
1020
  });
931
- return fallback;
1021
+ return resolvedFallbackResult.value;
932
1022
  }
933
1023
  if (!this.useWebSocket) {
934
1024
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -956,14 +1046,14 @@ var Schematic = class {
956
1046
  return result.value;
957
1047
  }).catch((error) => {
958
1048
  console.error("There was a problem with the fetch operation:", error);
959
- const errorResult = {
960
- flag: key,
961
- value: fallback,
962
- reason: "API request failed",
963
- error: error instanceof Error ? error.message : String(error)
964
- };
1049
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1050
+ key,
1051
+ fallback,
1052
+ "API request failed",
1053
+ error instanceof Error ? error.message : String(error)
1054
+ );
965
1055
  this.submitFlagCheckEvent(key, errorResult, context);
966
- return fallback;
1056
+ return errorResult.value;
967
1057
  });
968
1058
  }
969
1059
  try {
@@ -973,7 +1063,7 @@ var Schematic = class {
973
1063
  return existingVals[key].value;
974
1064
  }
975
1065
  if (this.isOffline()) {
976
- return fallback;
1066
+ return this.resolveFallbackValue(key, fallback);
977
1067
  }
978
1068
  try {
979
1069
  await this.setContext(context);
@@ -986,10 +1076,10 @@ var Schematic = class {
986
1076
  }
987
1077
  const contextVals = this.checks[contextStr] ?? {};
988
1078
  const flagCheck = contextVals[key];
989
- const result = flagCheck?.value ?? fallback;
1079
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
990
1080
  this.debug(
991
1081
  `checkFlag WebSocket result: ${key}`,
992
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1082
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
993
1083
  );
994
1084
  if (typeof flagCheck !== "undefined") {
995
1085
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -997,14 +1087,14 @@ var Schematic = class {
997
1087
  return result;
998
1088
  } catch (error) {
999
1089
  console.error("Unexpected error in checkFlag:", error);
1000
- const errorResult = {
1001
- flag: key,
1002
- value: fallback,
1003
- reason: "Unexpected error in flag check",
1004
- error: error instanceof Error ? error.message : String(error)
1005
- };
1090
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1091
+ key,
1092
+ fallback,
1093
+ "Unexpected error in flag check",
1094
+ error instanceof Error ? error.message : String(error)
1095
+ );
1006
1096
  this.submitFlagCheckEvent(key, errorResult, context);
1007
- return fallback;
1097
+ return errorResult.value;
1008
1098
  }
1009
1099
  }
1010
1100
  /**
@@ -1047,11 +1137,12 @@ var Schematic = class {
1047
1137
  */
1048
1138
  async fallbackToRest(key, context, fallback) {
1049
1139
  if (this.isOffline()) {
1140
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1050
1141
  this.debug(`fallbackToRest offline result: ${key}`, {
1051
- value: fallback,
1142
+ value: resolvedFallback,
1052
1143
  offlineMode: true
1053
1144
  });
1054
- return fallback;
1145
+ return resolvedFallback;
1055
1146
  }
1056
1147
  try {
1057
1148
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1078,14 +1169,14 @@ var Schematic = class {
1078
1169
  return result.value;
1079
1170
  } catch (error) {
1080
1171
  console.error("REST API call failed, using fallback value:", error);
1081
- const errorResult = {
1082
- flag: key,
1083
- value: fallback,
1084
- reason: "API request failed (fallback)",
1085
- error: error instanceof Error ? error.message : String(error)
1086
- };
1172
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1173
+ key,
1174
+ fallback,
1175
+ "API request failed (fallback)",
1176
+ error instanceof Error ? error.message : String(error)
1177
+ );
1087
1178
  this.submitFlagCheckEvent(key, errorResult, context);
1088
- return fallback;
1179
+ return errorResult.value;
1089
1180
  }
1090
1181
  }
1091
1182
  /**
@@ -1302,11 +1393,53 @@ var Schematic = class {
1302
1393
  }
1303
1394
  }
1304
1395
  };
1305
- flushEventQueue = () => {
1306
- while (this.eventQueue.length > 0) {
1307
- const event = this.eventQueue.shift();
1308
- if (event) {
1309
- this.sendEvent(event);
1396
+ startRetryTimer = () => {
1397
+ if (this.retryTimer !== null) {
1398
+ return;
1399
+ }
1400
+ this.retryTimer = setInterval(() => {
1401
+ this.flushEventQueue().catch((error) => {
1402
+ this.debug("Error in retry timer flush:", error);
1403
+ });
1404
+ if (this.eventQueue.length === 0) {
1405
+ this.stopRetryTimer();
1406
+ }
1407
+ }, 5e3);
1408
+ this.debug("Started retry timer");
1409
+ };
1410
+ stopRetryTimer = () => {
1411
+ if (this.retryTimer !== null) {
1412
+ clearInterval(this.retryTimer);
1413
+ this.retryTimer = null;
1414
+ this.debug("Stopped retry timer");
1415
+ }
1416
+ };
1417
+ flushEventQueue = async () => {
1418
+ if (this.eventQueue.length === 0) {
1419
+ return;
1420
+ }
1421
+ const now = Date.now();
1422
+ const readyEvents = [];
1423
+ const notReadyEvents = [];
1424
+ for (const event of this.eventQueue) {
1425
+ if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
1426
+ readyEvents.push(event);
1427
+ } else {
1428
+ notReadyEvents.push(event);
1429
+ }
1430
+ }
1431
+ if (readyEvents.length === 0) {
1432
+ this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1433
+ return;
1434
+ }
1435
+ this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1436
+ this.eventQueue = notReadyEvents;
1437
+ for (const event of readyEvents) {
1438
+ try {
1439
+ await this.sendEvent(event);
1440
+ this.debug(`Queued event sent successfully:`, event.type);
1441
+ } catch (error) {
1442
+ this.debug(`Failed to send queued event:`, error);
1310
1443
  }
1311
1444
  }
1312
1445
  };
@@ -1354,12 +1487,37 @@ var Schematic = class {
1354
1487
  },
1355
1488
  body: payload
1356
1489
  });
1490
+ if (!response.ok) {
1491
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1492
+ }
1357
1493
  this.debug(`event sent:`, {
1358
1494
  status: response.status,
1359
1495
  statusText: response.statusText
1360
1496
  });
1361
1497
  } catch (error) {
1362
- console.error("Error sending Schematic event: ", error);
1498
+ const retryCount = (event.retry_count ?? 0) + 1;
1499
+ if (retryCount <= this.maxEventRetries) {
1500
+ this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1501
+ const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1502
+ const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1503
+ const nextRetryAt = Date.now() + jitterDelay;
1504
+ const retryEvent = {
1505
+ ...event,
1506
+ retry_count: retryCount,
1507
+ next_retry_at: nextRetryAt
1508
+ };
1509
+ if (this.eventQueue.length < this.maxEventQueueSize) {
1510
+ this.eventQueue.push(retryEvent);
1511
+ this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1512
+ } else {
1513
+ this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1514
+ this.eventQueue.shift();
1515
+ this.eventQueue.push(retryEvent);
1516
+ }
1517
+ this.startRetryTimer();
1518
+ } else {
1519
+ this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1520
+ }
1363
1521
  }
1364
1522
  return Promise.resolve();
1365
1523
  };
@@ -1384,6 +1542,7 @@ var Schematic = class {
1384
1542
  clearTimeout(this.wsReconnectTimer);
1385
1543
  this.wsReconnectTimer = null;
1386
1544
  }
1545
+ this.stopRetryTimer();
1387
1546
  if (this.conn) {
1388
1547
  try {
1389
1548
  const socket = await this.conn;
@@ -1431,16 +1590,15 @@ var Schematic = class {
1431
1590
  * Handle browser coming back online
1432
1591
  */
1433
1592
  handleNetworkOnline = () => {
1434
- if (this.context.company === void 0 && this.context.user === void 0) {
1435
- this.debug("No context set, skipping reconnection");
1436
- return;
1437
- }
1593
+ this.debug("Network online, attempting reconnection and flushing queued events");
1438
1594
  this.wsReconnectAttempts = 0;
1439
1595
  if (this.wsReconnectTimer !== null) {
1440
1596
  clearTimeout(this.wsReconnectTimer);
1441
1597
  this.wsReconnectTimer = null;
1442
1598
  }
1443
- this.debug("Network online, reconnecting immediately");
1599
+ this.flushEventQueue().catch((error) => {
1600
+ this.debug("Error flushing event queue on network online:", error);
1601
+ });
1444
1602
  this.attemptReconnect();
1445
1603
  };
1446
1604
  /**
@@ -1470,10 +1628,20 @@ var Schematic = class {
1470
1628
  try {
1471
1629
  this.conn = this.wsConnect();
1472
1630
  const socket = await this.conn;
1631
+ this.debug(`Reconnection context check:`, {
1632
+ hasCompany: this.context.company !== void 0,
1633
+ hasUser: this.context.user !== void 0,
1634
+ context: this.context
1635
+ });
1473
1636
  if (this.context.company !== void 0 || this.context.user !== void 0) {
1474
- this.debug(`Reconnected, re-sending context`);
1475
- await this.wsSendMessage(socket, this.context);
1637
+ this.debug(`Reconnected, force re-sending context`);
1638
+ await this.wsSendContextAfterReconnection(socket, this.context);
1639
+ } else {
1640
+ this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1476
1641
  }
1642
+ this.flushEventQueue().catch((error) => {
1643
+ this.debug("Error flushing event queue after websocket reconnection:", error);
1644
+ });
1477
1645
  this.debug(`Reconnection successful`);
1478
1646
  } catch (error) {
1479
1647
  this.debug(`Reconnection attempt failed:`, error);
@@ -1534,6 +1702,61 @@ var Schematic = class {
1534
1702
  };
1535
1703
  });
1536
1704
  };
1705
+ // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1706
+ // because the server has lost all state and needs the initial context
1707
+ wsSendContextAfterReconnection = (socket, context) => {
1708
+ if (this.isOffline()) {
1709
+ this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1710
+ this.setIsPending(false);
1711
+ return Promise.resolve();
1712
+ }
1713
+ return new Promise((resolve) => {
1714
+ this.debug(`WebSocket force sending context after reconnection:`, context);
1715
+ this.context = context;
1716
+ const sendMessage = () => {
1717
+ let resolved = false;
1718
+ const messageHandler = (event) => {
1719
+ const message = JSON.parse(event.data);
1720
+ this.debug(`WebSocket message received after reconnection:`, message);
1721
+ if (!(contextString(context) in this.checks)) {
1722
+ this.checks[contextString(context)] = {};
1723
+ }
1724
+ (message.flags ?? []).forEach((flag) => {
1725
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1726
+ const contextStr = contextString(context);
1727
+ if (this.checks[contextStr] === void 0) {
1728
+ this.checks[contextStr] = {};
1729
+ }
1730
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1731
+ });
1732
+ this.useWebSocket = true;
1733
+ socket.removeEventListener("message", messageHandler);
1734
+ if (!resolved) {
1735
+ resolved = true;
1736
+ resolve(this.setIsPending(false));
1737
+ }
1738
+ };
1739
+ socket.addEventListener("message", messageHandler);
1740
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1741
+ const messagePayload = {
1742
+ apiKey: this.apiKey,
1743
+ clientVersion,
1744
+ data: context
1745
+ };
1746
+ this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1747
+ socket.send(JSON.stringify(messagePayload));
1748
+ };
1749
+ if (socket.readyState === WebSocket.OPEN) {
1750
+ this.debug(`WebSocket already open, sending forced message after reconnection`);
1751
+ sendMessage();
1752
+ } else {
1753
+ socket.addEventListener("open", () => {
1754
+ this.debug(`WebSocket opened, sending forced message after reconnection`);
1755
+ sendMessage();
1756
+ });
1757
+ }
1758
+ });
1759
+ };
1537
1760
  // Send a message on the websocket indicating interest in a particular evaluation context
1538
1761
  // and wait for the initial set of flag values to be returned
1539
1762
  wsSendMessage = (socket, context) => {
@@ -1722,7 +1945,7 @@ var notifyFlagValueListener = (listener, value) => {
1722
1945
  var import_react = __toESM(require("react"));
1723
1946
 
1724
1947
  // src/version.ts
1725
- var version2 = "1.2.7";
1948
+ var version2 = "1.2.9";
1726
1949
 
1727
1950
  // src/context/schematic.tsx
1728
1951
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -754,7 +754,7 @@ function contextString(context) {
754
754
  }, {});
755
755
  return JSON.stringify(sortedContext);
756
756
  }
757
- var version = "1.2.7";
757
+ var version = "1.2.9";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -784,6 +784,17 @@ var Schematic = class {
784
784
  wsReconnectAttempts = 0;
785
785
  wsReconnectTimer = null;
786
786
  wsIntentionalDisconnect = false;
787
+ maxEventQueueSize = 100;
788
+ // Prevent memory issues with very long network outages
789
+ maxEventRetries = 5;
790
+ // Maximum retry attempts for failed events
791
+ eventRetryInitialDelay = 1e3;
792
+ // Initial retry delay in ms
793
+ eventRetryMaxDelay = 3e4;
794
+ // Maximum retry delay in ms
795
+ retryTimer = null;
796
+ flagValueDefaults = {};
797
+ flagCheckDefaults = {};
787
798
  constructor(apiKey, options) {
788
799
  this.apiKey = apiKey;
789
800
  this.eventQueue = [];
@@ -842,6 +853,24 @@ var Schematic = class {
842
853
  if (options?.webSocketMaxRetryDelay !== void 0) {
843
854
  this.webSocketMaxRetryDelay = options.webSocketMaxRetryDelay;
844
855
  }
856
+ if (options?.maxEventQueueSize !== void 0) {
857
+ this.maxEventQueueSize = options.maxEventQueueSize;
858
+ }
859
+ if (options?.maxEventRetries !== void 0) {
860
+ this.maxEventRetries = options.maxEventRetries;
861
+ }
862
+ if (options?.eventRetryInitialDelay !== void 0) {
863
+ this.eventRetryInitialDelay = options.eventRetryInitialDelay;
864
+ }
865
+ if (options?.eventRetryMaxDelay !== void 0) {
866
+ this.eventRetryMaxDelay = options.eventRetryMaxDelay;
867
+ }
868
+ if (options?.flagValueDefaults !== void 0) {
869
+ this.flagValueDefaults = options.flagValueDefaults;
870
+ }
871
+ if (options?.flagCheckDefaults !== void 0) {
872
+ this.flagCheckDefaults = options.flagCheckDefaults;
873
+ }
845
874
  if (typeof window !== "undefined" && window?.addEventListener) {
846
875
  window.addEventListener("beforeunload", () => {
847
876
  this.flushEventQueue();
@@ -866,6 +895,62 @@ var Schematic = class {
866
895
  this.debug("Initialized with debug mode enabled");
867
896
  }
868
897
  }
898
+ /**
899
+ * Resolve fallback value according to priority order:
900
+ * 1. Callsite fallback value (if provided)
901
+ * 2. Initialization fallback value (flagValueDefaults)
902
+ * 3. Default to false
903
+ */
904
+ resolveFallbackValue(key, callsiteFallback) {
905
+ if (callsiteFallback !== void 0) {
906
+ return callsiteFallback;
907
+ }
908
+ if (key in this.flagValueDefaults) {
909
+ return this.flagValueDefaults[key];
910
+ }
911
+ return false;
912
+ }
913
+ /**
914
+ * Resolve complete CheckFlagReturn object according to priority order:
915
+ * 1. Use callsite fallback for boolean value, construct CheckFlagReturn
916
+ * 2. Use flagCheckDefaults if available for this flag
917
+ * 3. Use flagValueDefaults if available for this flag, construct CheckFlagReturn
918
+ * 4. Default CheckFlagReturn with value: false
919
+ */
920
+ resolveFallbackCheckFlagReturn(key, callsiteFallback, reason = "Fallback value used", error) {
921
+ if (callsiteFallback !== void 0) {
922
+ return {
923
+ flag: key,
924
+ value: callsiteFallback,
925
+ reason,
926
+ error
927
+ };
928
+ }
929
+ if (key in this.flagCheckDefaults) {
930
+ const defaultReturn = this.flagCheckDefaults[key];
931
+ return {
932
+ ...defaultReturn,
933
+ flag: key,
934
+ // Ensure flag matches the requested key
935
+ reason: error !== void 0 ? reason : defaultReturn.reason,
936
+ error
937
+ };
938
+ }
939
+ if (key in this.flagValueDefaults) {
940
+ return {
941
+ flag: key,
942
+ value: this.flagValueDefaults[key],
943
+ reason,
944
+ error
945
+ };
946
+ }
947
+ return {
948
+ flag: key,
949
+ value: false,
950
+ reason,
951
+ error
952
+ };
953
+ }
869
954
  /**
870
955
  * Get value for a single flag.
871
956
  * In WebSocket mode, returns cached values if connection is active, otherwise establishes
@@ -874,16 +959,21 @@ var Schematic = class {
874
959
  * In REST mode, makes an API call for each check.
875
960
  */
876
961
  async checkFlag(options) {
877
- const { fallback = false, key } = options;
962
+ const { fallback, key } = options;
878
963
  const context = options.context || this.context;
879
964
  const contextStr = contextString(context);
880
965
  this.debug(`checkFlag: ${key}`, { context, fallback });
881
966
  if (this.isOffline()) {
967
+ const resolvedFallbackResult = this.resolveFallbackCheckFlagReturn(
968
+ key,
969
+ fallback,
970
+ "Offline mode - using initialization defaults"
971
+ );
882
972
  this.debug(`checkFlag offline result: ${key}`, {
883
- value: fallback,
973
+ value: resolvedFallbackResult.value,
884
974
  offlineMode: true
885
975
  });
886
- return fallback;
976
+ return resolvedFallbackResult.value;
887
977
  }
888
978
  if (!this.useWebSocket) {
889
979
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -911,14 +1001,14 @@ var Schematic = class {
911
1001
  return result.value;
912
1002
  }).catch((error) => {
913
1003
  console.error("There was a problem with the fetch operation:", error);
914
- const errorResult = {
915
- flag: key,
916
- value: fallback,
917
- reason: "API request failed",
918
- error: error instanceof Error ? error.message : String(error)
919
- };
1004
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1005
+ key,
1006
+ fallback,
1007
+ "API request failed",
1008
+ error instanceof Error ? error.message : String(error)
1009
+ );
920
1010
  this.submitFlagCheckEvent(key, errorResult, context);
921
- return fallback;
1011
+ return errorResult.value;
922
1012
  });
923
1013
  }
924
1014
  try {
@@ -928,7 +1018,7 @@ var Schematic = class {
928
1018
  return existingVals[key].value;
929
1019
  }
930
1020
  if (this.isOffline()) {
931
- return fallback;
1021
+ return this.resolveFallbackValue(key, fallback);
932
1022
  }
933
1023
  try {
934
1024
  await this.setContext(context);
@@ -941,10 +1031,10 @@ var Schematic = class {
941
1031
  }
942
1032
  const contextVals = this.checks[contextStr] ?? {};
943
1033
  const flagCheck = contextVals[key];
944
- const result = flagCheck?.value ?? fallback;
1034
+ const result = flagCheck?.value ?? this.resolveFallbackValue(key, fallback);
945
1035
  this.debug(
946
1036
  `checkFlag WebSocket result: ${key}`,
947
- typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
1037
+ typeof flagCheck !== "undefined" ? flagCheck : { value: result, fallbackUsed: true }
948
1038
  );
949
1039
  if (typeof flagCheck !== "undefined") {
950
1040
  this.submitFlagCheckEvent(key, flagCheck, context);
@@ -952,14 +1042,14 @@ var Schematic = class {
952
1042
  return result;
953
1043
  } catch (error) {
954
1044
  console.error("Unexpected error in checkFlag:", error);
955
- const errorResult = {
956
- flag: key,
957
- value: fallback,
958
- reason: "Unexpected error in flag check",
959
- error: error instanceof Error ? error.message : String(error)
960
- };
1045
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1046
+ key,
1047
+ fallback,
1048
+ "Unexpected error in flag check",
1049
+ error instanceof Error ? error.message : String(error)
1050
+ );
961
1051
  this.submitFlagCheckEvent(key, errorResult, context);
962
- return fallback;
1052
+ return errorResult.value;
963
1053
  }
964
1054
  }
965
1055
  /**
@@ -1002,11 +1092,12 @@ var Schematic = class {
1002
1092
  */
1003
1093
  async fallbackToRest(key, context, fallback) {
1004
1094
  if (this.isOffline()) {
1095
+ const resolvedFallback = this.resolveFallbackValue(key, fallback);
1005
1096
  this.debug(`fallbackToRest offline result: ${key}`, {
1006
- value: fallback,
1097
+ value: resolvedFallback,
1007
1098
  offlineMode: true
1008
1099
  });
1009
- return fallback;
1100
+ return resolvedFallback;
1010
1101
  }
1011
1102
  try {
1012
1103
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
@@ -1033,14 +1124,14 @@ var Schematic = class {
1033
1124
  return result.value;
1034
1125
  } catch (error) {
1035
1126
  console.error("REST API call failed, using fallback value:", error);
1036
- const errorResult = {
1037
- flag: key,
1038
- value: fallback,
1039
- reason: "API request failed (fallback)",
1040
- error: error instanceof Error ? error.message : String(error)
1041
- };
1127
+ const errorResult = this.resolveFallbackCheckFlagReturn(
1128
+ key,
1129
+ fallback,
1130
+ "API request failed (fallback)",
1131
+ error instanceof Error ? error.message : String(error)
1132
+ );
1042
1133
  this.submitFlagCheckEvent(key, errorResult, context);
1043
- return fallback;
1134
+ return errorResult.value;
1044
1135
  }
1045
1136
  }
1046
1137
  /**
@@ -1257,11 +1348,53 @@ var Schematic = class {
1257
1348
  }
1258
1349
  }
1259
1350
  };
1260
- flushEventQueue = () => {
1261
- while (this.eventQueue.length > 0) {
1262
- const event = this.eventQueue.shift();
1263
- if (event) {
1264
- this.sendEvent(event);
1351
+ startRetryTimer = () => {
1352
+ if (this.retryTimer !== null) {
1353
+ return;
1354
+ }
1355
+ this.retryTimer = setInterval(() => {
1356
+ this.flushEventQueue().catch((error) => {
1357
+ this.debug("Error in retry timer flush:", error);
1358
+ });
1359
+ if (this.eventQueue.length === 0) {
1360
+ this.stopRetryTimer();
1361
+ }
1362
+ }, 5e3);
1363
+ this.debug("Started retry timer");
1364
+ };
1365
+ stopRetryTimer = () => {
1366
+ if (this.retryTimer !== null) {
1367
+ clearInterval(this.retryTimer);
1368
+ this.retryTimer = null;
1369
+ this.debug("Stopped retry timer");
1370
+ }
1371
+ };
1372
+ flushEventQueue = async () => {
1373
+ if (this.eventQueue.length === 0) {
1374
+ return;
1375
+ }
1376
+ const now = Date.now();
1377
+ const readyEvents = [];
1378
+ const notReadyEvents = [];
1379
+ for (const event of this.eventQueue) {
1380
+ if (event.next_retry_at === void 0 || event.next_retry_at <= now) {
1381
+ readyEvents.push(event);
1382
+ } else {
1383
+ notReadyEvents.push(event);
1384
+ }
1385
+ }
1386
+ if (readyEvents.length === 0) {
1387
+ this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1388
+ return;
1389
+ }
1390
+ this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1391
+ this.eventQueue = notReadyEvents;
1392
+ for (const event of readyEvents) {
1393
+ try {
1394
+ await this.sendEvent(event);
1395
+ this.debug(`Queued event sent successfully:`, event.type);
1396
+ } catch (error) {
1397
+ this.debug(`Failed to send queued event:`, error);
1265
1398
  }
1266
1399
  }
1267
1400
  };
@@ -1309,12 +1442,37 @@ var Schematic = class {
1309
1442
  },
1310
1443
  body: payload
1311
1444
  });
1445
+ if (!response.ok) {
1446
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1447
+ }
1312
1448
  this.debug(`event sent:`, {
1313
1449
  status: response.status,
1314
1450
  statusText: response.statusText
1315
1451
  });
1316
1452
  } catch (error) {
1317
- console.error("Error sending Schematic event: ", error);
1453
+ const retryCount = (event.retry_count ?? 0) + 1;
1454
+ if (retryCount <= this.maxEventRetries) {
1455
+ this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1456
+ const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1457
+ const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1458
+ const nextRetryAt = Date.now() + jitterDelay;
1459
+ const retryEvent = {
1460
+ ...event,
1461
+ retry_count: retryCount,
1462
+ next_retry_at: nextRetryAt
1463
+ };
1464
+ if (this.eventQueue.length < this.maxEventQueueSize) {
1465
+ this.eventQueue.push(retryEvent);
1466
+ this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1467
+ } else {
1468
+ this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1469
+ this.eventQueue.shift();
1470
+ this.eventQueue.push(retryEvent);
1471
+ }
1472
+ this.startRetryTimer();
1473
+ } else {
1474
+ this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1475
+ }
1318
1476
  }
1319
1477
  return Promise.resolve();
1320
1478
  };
@@ -1339,6 +1497,7 @@ var Schematic = class {
1339
1497
  clearTimeout(this.wsReconnectTimer);
1340
1498
  this.wsReconnectTimer = null;
1341
1499
  }
1500
+ this.stopRetryTimer();
1342
1501
  if (this.conn) {
1343
1502
  try {
1344
1503
  const socket = await this.conn;
@@ -1386,16 +1545,15 @@ var Schematic = class {
1386
1545
  * Handle browser coming back online
1387
1546
  */
1388
1547
  handleNetworkOnline = () => {
1389
- if (this.context.company === void 0 && this.context.user === void 0) {
1390
- this.debug("No context set, skipping reconnection");
1391
- return;
1392
- }
1548
+ this.debug("Network online, attempting reconnection and flushing queued events");
1393
1549
  this.wsReconnectAttempts = 0;
1394
1550
  if (this.wsReconnectTimer !== null) {
1395
1551
  clearTimeout(this.wsReconnectTimer);
1396
1552
  this.wsReconnectTimer = null;
1397
1553
  }
1398
- this.debug("Network online, reconnecting immediately");
1554
+ this.flushEventQueue().catch((error) => {
1555
+ this.debug("Error flushing event queue on network online:", error);
1556
+ });
1399
1557
  this.attemptReconnect();
1400
1558
  };
1401
1559
  /**
@@ -1425,10 +1583,20 @@ var Schematic = class {
1425
1583
  try {
1426
1584
  this.conn = this.wsConnect();
1427
1585
  const socket = await this.conn;
1586
+ this.debug(`Reconnection context check:`, {
1587
+ hasCompany: this.context.company !== void 0,
1588
+ hasUser: this.context.user !== void 0,
1589
+ context: this.context
1590
+ });
1428
1591
  if (this.context.company !== void 0 || this.context.user !== void 0) {
1429
- this.debug(`Reconnected, re-sending context`);
1430
- await this.wsSendMessage(socket, this.context);
1592
+ this.debug(`Reconnected, force re-sending context`);
1593
+ await this.wsSendContextAfterReconnection(socket, this.context);
1594
+ } else {
1595
+ this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1431
1596
  }
1597
+ this.flushEventQueue().catch((error) => {
1598
+ this.debug("Error flushing event queue after websocket reconnection:", error);
1599
+ });
1432
1600
  this.debug(`Reconnection successful`);
1433
1601
  } catch (error) {
1434
1602
  this.debug(`Reconnection attempt failed:`, error);
@@ -1489,6 +1657,61 @@ var Schematic = class {
1489
1657
  };
1490
1658
  });
1491
1659
  };
1660
+ // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1661
+ // because the server has lost all state and needs the initial context
1662
+ wsSendContextAfterReconnection = (socket, context) => {
1663
+ if (this.isOffline()) {
1664
+ this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1665
+ this.setIsPending(false);
1666
+ return Promise.resolve();
1667
+ }
1668
+ return new Promise((resolve) => {
1669
+ this.debug(`WebSocket force sending context after reconnection:`, context);
1670
+ this.context = context;
1671
+ const sendMessage = () => {
1672
+ let resolved = false;
1673
+ const messageHandler = (event) => {
1674
+ const message = JSON.parse(event.data);
1675
+ this.debug(`WebSocket message received after reconnection:`, message);
1676
+ if (!(contextString(context) in this.checks)) {
1677
+ this.checks[contextString(context)] = {};
1678
+ }
1679
+ (message.flags ?? []).forEach((flag) => {
1680
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1681
+ const contextStr = contextString(context);
1682
+ if (this.checks[contextStr] === void 0) {
1683
+ this.checks[contextStr] = {};
1684
+ }
1685
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1686
+ });
1687
+ this.useWebSocket = true;
1688
+ socket.removeEventListener("message", messageHandler);
1689
+ if (!resolved) {
1690
+ resolved = true;
1691
+ resolve(this.setIsPending(false));
1692
+ }
1693
+ };
1694
+ socket.addEventListener("message", messageHandler);
1695
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1696
+ const messagePayload = {
1697
+ apiKey: this.apiKey,
1698
+ clientVersion,
1699
+ data: context
1700
+ };
1701
+ this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1702
+ socket.send(JSON.stringify(messagePayload));
1703
+ };
1704
+ if (socket.readyState === WebSocket.OPEN) {
1705
+ this.debug(`WebSocket already open, sending forced message after reconnection`);
1706
+ sendMessage();
1707
+ } else {
1708
+ socket.addEventListener("open", () => {
1709
+ this.debug(`WebSocket opened, sending forced message after reconnection`);
1710
+ sendMessage();
1711
+ });
1712
+ }
1713
+ });
1714
+ };
1492
1715
  // Send a message on the websocket indicating interest in a particular evaluation context
1493
1716
  // and wait for the initial set of flag values to be returned
1494
1717
  wsSendMessage = (socket, context) => {
@@ -1677,7 +1900,7 @@ var notifyFlagValueListener = (listener, value) => {
1677
1900
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1678
1901
 
1679
1902
  // src/version.ts
1680
- var version2 = "1.2.7";
1903
+ var version2 = "1.2.9";
1681
1904
 
1682
1905
  // src/context/schematic.tsx
1683
1906
  import { jsx } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schematichq/schematic-react",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "main": "dist/schematic-react.cjs.js",
5
5
  "module": "dist/schematic-react.esm.js",
6
6
  "types": "dist/schematic-react.d.ts",
@@ -31,7 +31,7 @@
31
31
  "prepare": "husky"
32
32
  },
33
33
  "dependencies": {
34
- "@schematichq/schematic-js": "^1.2.7"
34
+ "@schematichq/schematic-js": "^1.2.9"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^9.39.1",
@@ -39,7 +39,7 @@
39
39
  "@testing-library/dom": "^10.4.1",
40
40
  "@testing-library/jest-dom": "^6.9.1",
41
41
  "@testing-library/react": "^16.3.0",
42
- "@types/react": "^19.2.3",
42
+ "@types/react": "^19.2.6",
43
43
  "@vitest/browser": "^4.0.8",
44
44
  "esbuild": "^0.27.0",
45
45
  "eslint": "^9.39.1",
@@ -54,7 +54,7 @@
54
54
  "react": "^19.2.0",
55
55
  "react-dom": "^19.2.0",
56
56
  "typescript": "^5.9.3",
57
- "typescript-eslint": "^8.46.4",
57
+ "typescript-eslint": "^8.47.0",
58
58
  "vitest": "^4.0.8"
59
59
  },
60
60
  "peerDependencies": {