@schematichq/schematic-react 1.2.9 → 1.2.10

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.9";
802
+ var version = "1.2.10";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -829,6 +829,8 @@ var Schematic = class {
829
829
  wsReconnectAttempts = 0;
830
830
  wsReconnectTimer = null;
831
831
  wsIntentionalDisconnect = false;
832
+ currentWebSocket = null;
833
+ isConnecting = false;
832
834
  maxEventQueueSize = 100;
833
835
  // Prevent memory issues with very long network outages
834
836
  maxEventRetries = 5;
@@ -1106,6 +1108,49 @@ var Schematic = class {
1106
1108
  console.log(`[Schematic] ${message}`, ...args);
1107
1109
  }
1108
1110
  }
1111
+ /**
1112
+ * Create a persistent message handler for websocket flag updates
1113
+ */
1114
+ createPersistentMessageHandler(context) {
1115
+ return (event) => {
1116
+ const message = JSON.parse(event.data);
1117
+ this.debug(`WebSocket persistent message received:`, message);
1118
+ if (!(contextString(context) in this.checks)) {
1119
+ this.checks[contextString(context)] = {};
1120
+ }
1121
+ (message.flags ?? []).forEach((flag) => {
1122
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1123
+ const contextStr = contextString(context);
1124
+ if (this.checks[contextStr] === void 0) {
1125
+ this.checks[contextStr] = {};
1126
+ }
1127
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1128
+ this.debug(`WebSocket flag update:`, {
1129
+ flag: flagCheck.flag,
1130
+ value: flagCheck.value,
1131
+ flagCheck
1132
+ });
1133
+ if (typeof flagCheck.featureUsageEvent === "string") {
1134
+ this.updateFeatureUsageEventMap(flagCheck);
1135
+ }
1136
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1137
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1138
+ }
1139
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1140
+ flag: flag.flag,
1141
+ value: flagCheck.value
1142
+ });
1143
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1144
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1145
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1146
+ flag: flag.flag,
1147
+ value: flagCheck.value
1148
+ });
1149
+ });
1150
+ this.flushContextDependentEventQueue();
1151
+ this.setIsPending(false);
1152
+ };
1153
+ }
1109
1154
  /**
1110
1155
  * Helper function to check if client is in offline mode
1111
1156
  */
@@ -1257,6 +1302,19 @@ var Schematic = class {
1257
1302
  try {
1258
1303
  this.setIsPending(true);
1259
1304
  if (!this.conn) {
1305
+ if (this.isConnecting) {
1306
+ this.debug(
1307
+ `Connection already in progress, waiting for it to complete`
1308
+ );
1309
+ while (this.isConnecting && this.conn === null) {
1310
+ await new Promise((resolve) => setTimeout(resolve, 10));
1311
+ }
1312
+ if (this.conn !== null) {
1313
+ const socket2 = await this.conn;
1314
+ await this.wsSendMessage(socket2, context);
1315
+ return;
1316
+ }
1317
+ }
1260
1318
  if (this.wsReconnectTimer !== null) {
1261
1319
  this.debug(
1262
1320
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1264,7 +1322,17 @@ var Schematic = class {
1264
1322
  clearTimeout(this.wsReconnectTimer);
1265
1323
  this.wsReconnectTimer = null;
1266
1324
  }
1267
- this.conn = this.wsConnect();
1325
+ this.isConnecting = true;
1326
+ try {
1327
+ this.conn = this.wsConnect();
1328
+ const socket2 = await this.conn;
1329
+ this.isConnecting = false;
1330
+ await this.wsSendMessage(socket2, context);
1331
+ return;
1332
+ } catch (error) {
1333
+ this.isConnecting = false;
1334
+ throw error;
1335
+ }
1268
1336
  }
1269
1337
  const socket = await this.conn;
1270
1338
  await this.wsSendMessage(socket, context);
@@ -1429,10 +1497,14 @@ var Schematic = class {
1429
1497
  }
1430
1498
  }
1431
1499
  if (readyEvents.length === 0) {
1432
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1500
+ this.debug(
1501
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1502
+ );
1433
1503
  return;
1434
1504
  }
1435
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1505
+ this.debug(
1506
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1507
+ );
1436
1508
  this.eventQueue = notReadyEvents;
1437
1509
  for (const event of readyEvents) {
1438
1510
  try {
@@ -1497,7 +1569,10 @@ var Schematic = class {
1497
1569
  } catch (error) {
1498
1570
  const retryCount = (event.retry_count ?? 0) + 1;
1499
1571
  if (retryCount <= this.maxEventRetries) {
1500
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1572
+ this.debug(
1573
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1574
+ error
1575
+ );
1501
1576
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1502
1577
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1503
1578
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1508,15 +1583,22 @@ var Schematic = class {
1508
1583
  };
1509
1584
  if (this.eventQueue.length < this.maxEventQueueSize) {
1510
1585
  this.eventQueue.push(retryEvent);
1511
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1586
+ this.debug(
1587
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1588
+ );
1512
1589
  } else {
1513
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1590
+ this.debug(
1591
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1592
+ );
1514
1593
  this.eventQueue.shift();
1515
1594
  this.eventQueue.push(retryEvent);
1516
1595
  }
1517
1596
  this.startRetryTimer();
1518
1597
  } else {
1519
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1598
+ this.debug(
1599
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1600
+ error
1601
+ );
1520
1602
  }
1521
1603
  }
1522
1604
  return Promise.resolve();
@@ -1546,11 +1628,17 @@ var Schematic = class {
1546
1628
  if (this.conn) {
1547
1629
  try {
1548
1630
  const socket = await this.conn;
1631
+ if (this.currentWebSocket === socket) {
1632
+ this.debug(`Cleaning up current websocket tracking`);
1633
+ this.currentWebSocket = null;
1634
+ }
1549
1635
  socket.close();
1550
1636
  } catch (error) {
1551
1637
  console.error("Error during cleanup:", error);
1552
1638
  } finally {
1553
1639
  this.conn = null;
1640
+ this.currentWebSocket = null;
1641
+ this.isConnecting = false;
1554
1642
  }
1555
1643
  }
1556
1644
  };
@@ -1575,7 +1663,9 @@ var Schematic = class {
1575
1663
  if (this.conn !== null) {
1576
1664
  try {
1577
1665
  const socket = await this.conn;
1578
- socket.close();
1666
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1667
+ socket.close();
1668
+ }
1579
1669
  } catch (error) {
1580
1670
  this.debug("Error closing connection on offline:", error);
1581
1671
  }
@@ -1590,7 +1680,9 @@ var Schematic = class {
1590
1680
  * Handle browser coming back online
1591
1681
  */
1592
1682
  handleNetworkOnline = () => {
1593
- this.debug("Network online, attempting reconnection and flushing queued events");
1683
+ this.debug(
1684
+ "Network online, attempting reconnection and flushing queued events"
1685
+ );
1594
1686
  this.wsReconnectAttempts = 0;
1595
1687
  if (this.wsReconnectTimer !== null) {
1596
1688
  clearTimeout(this.wsReconnectTimer);
@@ -1613,7 +1705,10 @@ var Schematic = class {
1613
1705
  return;
1614
1706
  }
1615
1707
  if (this.wsReconnectTimer !== null) {
1616
- clearTimeout(this.wsReconnectTimer);
1708
+ this.debug(
1709
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1710
+ );
1711
+ return;
1617
1712
  }
1618
1713
  const delay = this.calculateReconnectDelay();
1619
1714
  this.debug(
@@ -1626,23 +1721,57 @@ var Schematic = class {
1626
1721
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1627
1722
  );
1628
1723
  try {
1629
- this.conn = this.wsConnect();
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
- });
1636
- if (this.context.company !== void 0 || this.context.user !== void 0) {
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`);
1724
+ if (this.conn !== null) {
1725
+ this.debug(`Cleaning up existing connection before reconnection`);
1726
+ try {
1727
+ const existingSocket = await this.conn;
1728
+ if (this.currentWebSocket === existingSocket) {
1729
+ this.debug(`Existing websocket is current, will be replaced`);
1730
+ this.currentWebSocket = null;
1731
+ }
1732
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1733
+ existingSocket.close();
1734
+ }
1735
+ } catch (error) {
1736
+ this.debug(`Error cleaning up existing connection:`, error);
1737
+ }
1738
+ this.conn = null;
1739
+ this.currentWebSocket = null;
1740
+ this.isConnecting = false;
1741
+ }
1742
+ this.isConnecting = true;
1743
+ try {
1744
+ this.conn = this.wsConnect();
1745
+ const socket = await this.conn;
1746
+ this.isConnecting = false;
1747
+ this.debug(`Reconnection context check:`, {
1748
+ hasCompany: this.context.company !== void 0,
1749
+ hasUser: this.context.user !== void 0,
1750
+ context: this.context
1751
+ });
1752
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1753
+ this.debug(`Reconnected, force re-sending context`);
1754
+ await this.wsSendMessage(socket, this.context, true);
1755
+ } else {
1756
+ this.debug(
1757
+ `No context to re-send after reconnection - websocket ready for new context`
1758
+ );
1759
+ this.debug(
1760
+ `Setting up tracking for reconnected websocket (no context to send)`
1761
+ );
1762
+ this.currentWebSocket = socket;
1763
+ }
1764
+ this.flushEventQueue().catch((error) => {
1765
+ this.debug(
1766
+ "Error flushing event queue after websocket reconnection:",
1767
+ error
1768
+ );
1769
+ });
1770
+ this.debug(`Reconnection successful`);
1771
+ } catch (error) {
1772
+ this.isConnecting = false;
1773
+ throw error;
1641
1774
  }
1642
- this.flushEventQueue().catch((error) => {
1643
- this.debug("Error flushing event queue after websocket reconnection:", error);
1644
- });
1645
- this.debug(`Reconnection successful`);
1646
1775
  } catch (error) {
1647
1776
  this.debug(`Reconnection attempt failed:`, error);
1648
1777
  }
@@ -1660,6 +1789,8 @@ var Schematic = class {
1660
1789
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1661
1790
  this.debug(`connecting to WebSocket:`, wsUrl);
1662
1791
  const webSocket = new WebSocket(wsUrl);
1792
+ const connectionId = Math.random().toString(36).substring(7);
1793
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1663
1794
  let timeoutId = null;
1664
1795
  let isResolved = false;
1665
1796
  timeoutId = setTimeout(() => {
@@ -1678,7 +1809,7 @@ var Schematic = class {
1678
1809
  }
1679
1810
  this.wsReconnectAttempts = 0;
1680
1811
  this.wsIntentionalDisconnect = false;
1681
- this.debug(`WebSocket connection opened`);
1812
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1682
1813
  resolve(webSocket);
1683
1814
  };
1684
1815
  webSocket.onerror = (error) => {
@@ -1686,7 +1817,7 @@ var Schematic = class {
1686
1817
  if (timeoutId !== null) {
1687
1818
  clearTimeout(timeoutId);
1688
1819
  }
1689
- this.debug(`WebSocket connection error:`, error);
1820
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1690
1821
  reject(error);
1691
1822
  };
1692
1823
  webSocket.onclose = () => {
@@ -1694,121 +1825,48 @@ var Schematic = class {
1694
1825
  if (timeoutId !== null) {
1695
1826
  clearTimeout(timeoutId);
1696
1827
  }
1697
- this.debug(`WebSocket connection closed`);
1828
+ this.debug(`WebSocket connection ${connectionId} closed`);
1698
1829
  this.conn = null;
1830
+ if (this.currentWebSocket === webSocket) {
1831
+ this.currentWebSocket = null;
1832
+ this.isConnecting = false;
1833
+ }
1699
1834
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1700
1835
  this.attemptReconnect();
1701
1836
  }
1702
1837
  };
1703
1838
  });
1704
1839
  };
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
- };
1760
1840
  // Send a message on the websocket indicating interest in a particular evaluation context
1761
1841
  // and wait for the initial set of flag values to be returned
1762
- wsSendMessage = (socket, context) => {
1842
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1763
1843
  if (this.isOffline()) {
1764
1844
  this.debug("wsSendMessage: skipped (offline mode)");
1765
1845
  this.setIsPending(false);
1766
1846
  return Promise.resolve();
1767
1847
  }
1768
1848
  return new Promise((resolve, reject) => {
1769
- if (contextString(context) == contextString(this.context)) {
1849
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1770
1850
  this.debug(`WebSocket context unchanged, skipping update`);
1771
1851
  return resolve(this.setIsPending(false));
1772
1852
  }
1773
- this.debug(`WebSocket context updated:`, context);
1853
+ this.debug(
1854
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1855
+ context
1856
+ );
1774
1857
  this.context = context;
1775
1858
  const sendMessage = () => {
1776
1859
  let resolved = false;
1860
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1777
1861
  const messageHandler = (event) => {
1778
- const message = JSON.parse(event.data);
1779
- this.debug(`WebSocket message received:`, message);
1780
- if (!(contextString(context) in this.checks)) {
1781
- this.checks[contextString(context)] = {};
1782
- }
1783
- (message.flags ?? []).forEach((flag) => {
1784
- const flagCheck = CheckFlagReturnFromJSON(flag);
1785
- const contextStr = contextString(context);
1786
- if (this.checks[contextStr] === void 0) {
1787
- this.checks[contextStr] = {};
1788
- }
1789
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1790
- this.debug(`WebSocket flag update:`, {
1791
- flag: flagCheck.flag,
1792
- value: flagCheck.value,
1793
- flagCheck
1794
- });
1795
- if (typeof flagCheck.featureUsageEvent === "string") {
1796
- this.updateFeatureUsageEventMap(flagCheck);
1797
- }
1798
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1799
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1800
- }
1801
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1802
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1803
- });
1804
- this.flushContextDependentEventQueue();
1805
- this.setIsPending(false);
1862
+ persistentMessageHandler(event);
1806
1863
  if (!resolved) {
1807
1864
  resolved = true;
1808
1865
  resolve();
1809
1866
  }
1810
1867
  };
1811
1868
  socket.addEventListener("message", messageHandler);
1869
+ this.currentWebSocket = socket;
1812
1870
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1813
1871
  const messagePayload = {
1814
1872
  apiKey: this.apiKey,
@@ -1916,7 +1974,17 @@ var Schematic = class {
1916
1974
  { value }
1917
1975
  );
1918
1976
  }
1919
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1977
+ listeners.forEach((listener, index) => {
1978
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1979
+ flagKey,
1980
+ value
1981
+ });
1982
+ notifyFlagValueListener(listener, value);
1983
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1984
+ flagKey,
1985
+ value
1986
+ });
1987
+ });
1920
1988
  };
1921
1989
  };
1922
1990
  var notifyPendingListener = (listener, value) => {
@@ -1945,7 +2013,7 @@ var notifyFlagValueListener = (listener, value) => {
1945
2013
  var import_react = __toESM(require("react"));
1946
2014
 
1947
2015
  // src/version.ts
1948
- var version2 = "1.2.9";
2016
+ var version2 = "1.2.10";
1949
2017
 
1950
2018
  // src/context/schematic.tsx
1951
2019
  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.9";
757
+ var version = "1.2.10";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -784,6 +784,8 @@ var Schematic = class {
784
784
  wsReconnectAttempts = 0;
785
785
  wsReconnectTimer = null;
786
786
  wsIntentionalDisconnect = false;
787
+ currentWebSocket = null;
788
+ isConnecting = false;
787
789
  maxEventQueueSize = 100;
788
790
  // Prevent memory issues with very long network outages
789
791
  maxEventRetries = 5;
@@ -1061,6 +1063,49 @@ var Schematic = class {
1061
1063
  console.log(`[Schematic] ${message}`, ...args);
1062
1064
  }
1063
1065
  }
1066
+ /**
1067
+ * Create a persistent message handler for websocket flag updates
1068
+ */
1069
+ createPersistentMessageHandler(context) {
1070
+ return (event) => {
1071
+ const message = JSON.parse(event.data);
1072
+ this.debug(`WebSocket persistent message received:`, message);
1073
+ if (!(contextString(context) in this.checks)) {
1074
+ this.checks[contextString(context)] = {};
1075
+ }
1076
+ (message.flags ?? []).forEach((flag) => {
1077
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1078
+ const contextStr = contextString(context);
1079
+ if (this.checks[contextStr] === void 0) {
1080
+ this.checks[contextStr] = {};
1081
+ }
1082
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1083
+ this.debug(`WebSocket flag update:`, {
1084
+ flag: flagCheck.flag,
1085
+ value: flagCheck.value,
1086
+ flagCheck
1087
+ });
1088
+ if (typeof flagCheck.featureUsageEvent === "string") {
1089
+ this.updateFeatureUsageEventMap(flagCheck);
1090
+ }
1091
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1092
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1093
+ }
1094
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1095
+ flag: flag.flag,
1096
+ value: flagCheck.value
1097
+ });
1098
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1099
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1100
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1101
+ flag: flag.flag,
1102
+ value: flagCheck.value
1103
+ });
1104
+ });
1105
+ this.flushContextDependentEventQueue();
1106
+ this.setIsPending(false);
1107
+ };
1108
+ }
1064
1109
  /**
1065
1110
  * Helper function to check if client is in offline mode
1066
1111
  */
@@ -1212,6 +1257,19 @@ var Schematic = class {
1212
1257
  try {
1213
1258
  this.setIsPending(true);
1214
1259
  if (!this.conn) {
1260
+ if (this.isConnecting) {
1261
+ this.debug(
1262
+ `Connection already in progress, waiting for it to complete`
1263
+ );
1264
+ while (this.isConnecting && this.conn === null) {
1265
+ await new Promise((resolve) => setTimeout(resolve, 10));
1266
+ }
1267
+ if (this.conn !== null) {
1268
+ const socket2 = await this.conn;
1269
+ await this.wsSendMessage(socket2, context);
1270
+ return;
1271
+ }
1272
+ }
1215
1273
  if (this.wsReconnectTimer !== null) {
1216
1274
  this.debug(
1217
1275
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1219,7 +1277,17 @@ var Schematic = class {
1219
1277
  clearTimeout(this.wsReconnectTimer);
1220
1278
  this.wsReconnectTimer = null;
1221
1279
  }
1222
- this.conn = this.wsConnect();
1280
+ this.isConnecting = true;
1281
+ try {
1282
+ this.conn = this.wsConnect();
1283
+ const socket2 = await this.conn;
1284
+ this.isConnecting = false;
1285
+ await this.wsSendMessage(socket2, context);
1286
+ return;
1287
+ } catch (error) {
1288
+ this.isConnecting = false;
1289
+ throw error;
1290
+ }
1223
1291
  }
1224
1292
  const socket = await this.conn;
1225
1293
  await this.wsSendMessage(socket, context);
@@ -1384,10 +1452,14 @@ var Schematic = class {
1384
1452
  }
1385
1453
  }
1386
1454
  if (readyEvents.length === 0) {
1387
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1455
+ this.debug(
1456
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1457
+ );
1388
1458
  return;
1389
1459
  }
1390
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1460
+ this.debug(
1461
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1462
+ );
1391
1463
  this.eventQueue = notReadyEvents;
1392
1464
  for (const event of readyEvents) {
1393
1465
  try {
@@ -1452,7 +1524,10 @@ var Schematic = class {
1452
1524
  } catch (error) {
1453
1525
  const retryCount = (event.retry_count ?? 0) + 1;
1454
1526
  if (retryCount <= this.maxEventRetries) {
1455
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1527
+ this.debug(
1528
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1529
+ error
1530
+ );
1456
1531
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1457
1532
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1458
1533
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1463,15 +1538,22 @@ var Schematic = class {
1463
1538
  };
1464
1539
  if (this.eventQueue.length < this.maxEventQueueSize) {
1465
1540
  this.eventQueue.push(retryEvent);
1466
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1541
+ this.debug(
1542
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1543
+ );
1467
1544
  } else {
1468
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1545
+ this.debug(
1546
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1547
+ );
1469
1548
  this.eventQueue.shift();
1470
1549
  this.eventQueue.push(retryEvent);
1471
1550
  }
1472
1551
  this.startRetryTimer();
1473
1552
  } else {
1474
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1553
+ this.debug(
1554
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1555
+ error
1556
+ );
1475
1557
  }
1476
1558
  }
1477
1559
  return Promise.resolve();
@@ -1501,11 +1583,17 @@ var Schematic = class {
1501
1583
  if (this.conn) {
1502
1584
  try {
1503
1585
  const socket = await this.conn;
1586
+ if (this.currentWebSocket === socket) {
1587
+ this.debug(`Cleaning up current websocket tracking`);
1588
+ this.currentWebSocket = null;
1589
+ }
1504
1590
  socket.close();
1505
1591
  } catch (error) {
1506
1592
  console.error("Error during cleanup:", error);
1507
1593
  } finally {
1508
1594
  this.conn = null;
1595
+ this.currentWebSocket = null;
1596
+ this.isConnecting = false;
1509
1597
  }
1510
1598
  }
1511
1599
  };
@@ -1530,7 +1618,9 @@ var Schematic = class {
1530
1618
  if (this.conn !== null) {
1531
1619
  try {
1532
1620
  const socket = await this.conn;
1533
- socket.close();
1621
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1622
+ socket.close();
1623
+ }
1534
1624
  } catch (error) {
1535
1625
  this.debug("Error closing connection on offline:", error);
1536
1626
  }
@@ -1545,7 +1635,9 @@ var Schematic = class {
1545
1635
  * Handle browser coming back online
1546
1636
  */
1547
1637
  handleNetworkOnline = () => {
1548
- this.debug("Network online, attempting reconnection and flushing queued events");
1638
+ this.debug(
1639
+ "Network online, attempting reconnection and flushing queued events"
1640
+ );
1549
1641
  this.wsReconnectAttempts = 0;
1550
1642
  if (this.wsReconnectTimer !== null) {
1551
1643
  clearTimeout(this.wsReconnectTimer);
@@ -1568,7 +1660,10 @@ var Schematic = class {
1568
1660
  return;
1569
1661
  }
1570
1662
  if (this.wsReconnectTimer !== null) {
1571
- clearTimeout(this.wsReconnectTimer);
1663
+ this.debug(
1664
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1665
+ );
1666
+ return;
1572
1667
  }
1573
1668
  const delay = this.calculateReconnectDelay();
1574
1669
  this.debug(
@@ -1581,23 +1676,57 @@ var Schematic = class {
1581
1676
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1582
1677
  );
1583
1678
  try {
1584
- this.conn = this.wsConnect();
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
- });
1591
- if (this.context.company !== void 0 || this.context.user !== void 0) {
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`);
1679
+ if (this.conn !== null) {
1680
+ this.debug(`Cleaning up existing connection before reconnection`);
1681
+ try {
1682
+ const existingSocket = await this.conn;
1683
+ if (this.currentWebSocket === existingSocket) {
1684
+ this.debug(`Existing websocket is current, will be replaced`);
1685
+ this.currentWebSocket = null;
1686
+ }
1687
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1688
+ existingSocket.close();
1689
+ }
1690
+ } catch (error) {
1691
+ this.debug(`Error cleaning up existing connection:`, error);
1692
+ }
1693
+ this.conn = null;
1694
+ this.currentWebSocket = null;
1695
+ this.isConnecting = false;
1696
+ }
1697
+ this.isConnecting = true;
1698
+ try {
1699
+ this.conn = this.wsConnect();
1700
+ const socket = await this.conn;
1701
+ this.isConnecting = false;
1702
+ this.debug(`Reconnection context check:`, {
1703
+ hasCompany: this.context.company !== void 0,
1704
+ hasUser: this.context.user !== void 0,
1705
+ context: this.context
1706
+ });
1707
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1708
+ this.debug(`Reconnected, force re-sending context`);
1709
+ await this.wsSendMessage(socket, this.context, true);
1710
+ } else {
1711
+ this.debug(
1712
+ `No context to re-send after reconnection - websocket ready for new context`
1713
+ );
1714
+ this.debug(
1715
+ `Setting up tracking for reconnected websocket (no context to send)`
1716
+ );
1717
+ this.currentWebSocket = socket;
1718
+ }
1719
+ this.flushEventQueue().catch((error) => {
1720
+ this.debug(
1721
+ "Error flushing event queue after websocket reconnection:",
1722
+ error
1723
+ );
1724
+ });
1725
+ this.debug(`Reconnection successful`);
1726
+ } catch (error) {
1727
+ this.isConnecting = false;
1728
+ throw error;
1596
1729
  }
1597
- this.flushEventQueue().catch((error) => {
1598
- this.debug("Error flushing event queue after websocket reconnection:", error);
1599
- });
1600
- this.debug(`Reconnection successful`);
1601
1730
  } catch (error) {
1602
1731
  this.debug(`Reconnection attempt failed:`, error);
1603
1732
  }
@@ -1615,6 +1744,8 @@ var Schematic = class {
1615
1744
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1616
1745
  this.debug(`connecting to WebSocket:`, wsUrl);
1617
1746
  const webSocket = new WebSocket(wsUrl);
1747
+ const connectionId = Math.random().toString(36).substring(7);
1748
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1618
1749
  let timeoutId = null;
1619
1750
  let isResolved = false;
1620
1751
  timeoutId = setTimeout(() => {
@@ -1633,7 +1764,7 @@ var Schematic = class {
1633
1764
  }
1634
1765
  this.wsReconnectAttempts = 0;
1635
1766
  this.wsIntentionalDisconnect = false;
1636
- this.debug(`WebSocket connection opened`);
1767
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1637
1768
  resolve(webSocket);
1638
1769
  };
1639
1770
  webSocket.onerror = (error) => {
@@ -1641,7 +1772,7 @@ var Schematic = class {
1641
1772
  if (timeoutId !== null) {
1642
1773
  clearTimeout(timeoutId);
1643
1774
  }
1644
- this.debug(`WebSocket connection error:`, error);
1775
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1645
1776
  reject(error);
1646
1777
  };
1647
1778
  webSocket.onclose = () => {
@@ -1649,121 +1780,48 @@ var Schematic = class {
1649
1780
  if (timeoutId !== null) {
1650
1781
  clearTimeout(timeoutId);
1651
1782
  }
1652
- this.debug(`WebSocket connection closed`);
1783
+ this.debug(`WebSocket connection ${connectionId} closed`);
1653
1784
  this.conn = null;
1785
+ if (this.currentWebSocket === webSocket) {
1786
+ this.currentWebSocket = null;
1787
+ this.isConnecting = false;
1788
+ }
1654
1789
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1655
1790
  this.attemptReconnect();
1656
1791
  }
1657
1792
  };
1658
1793
  });
1659
1794
  };
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
- };
1715
1795
  // Send a message on the websocket indicating interest in a particular evaluation context
1716
1796
  // and wait for the initial set of flag values to be returned
1717
- wsSendMessage = (socket, context) => {
1797
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1718
1798
  if (this.isOffline()) {
1719
1799
  this.debug("wsSendMessage: skipped (offline mode)");
1720
1800
  this.setIsPending(false);
1721
1801
  return Promise.resolve();
1722
1802
  }
1723
1803
  return new Promise((resolve, reject) => {
1724
- if (contextString(context) == contextString(this.context)) {
1804
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1725
1805
  this.debug(`WebSocket context unchanged, skipping update`);
1726
1806
  return resolve(this.setIsPending(false));
1727
1807
  }
1728
- this.debug(`WebSocket context updated:`, context);
1808
+ this.debug(
1809
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1810
+ context
1811
+ );
1729
1812
  this.context = context;
1730
1813
  const sendMessage = () => {
1731
1814
  let resolved = false;
1815
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1732
1816
  const messageHandler = (event) => {
1733
- const message = JSON.parse(event.data);
1734
- this.debug(`WebSocket message received:`, message);
1735
- if (!(contextString(context) in this.checks)) {
1736
- this.checks[contextString(context)] = {};
1737
- }
1738
- (message.flags ?? []).forEach((flag) => {
1739
- const flagCheck = CheckFlagReturnFromJSON(flag);
1740
- const contextStr = contextString(context);
1741
- if (this.checks[contextStr] === void 0) {
1742
- this.checks[contextStr] = {};
1743
- }
1744
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1745
- this.debug(`WebSocket flag update:`, {
1746
- flag: flagCheck.flag,
1747
- value: flagCheck.value,
1748
- flagCheck
1749
- });
1750
- if (typeof flagCheck.featureUsageEvent === "string") {
1751
- this.updateFeatureUsageEventMap(flagCheck);
1752
- }
1753
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1754
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1755
- }
1756
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1757
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1758
- });
1759
- this.flushContextDependentEventQueue();
1760
- this.setIsPending(false);
1817
+ persistentMessageHandler(event);
1761
1818
  if (!resolved) {
1762
1819
  resolved = true;
1763
1820
  resolve();
1764
1821
  }
1765
1822
  };
1766
1823
  socket.addEventListener("message", messageHandler);
1824
+ this.currentWebSocket = socket;
1767
1825
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1768
1826
  const messagePayload = {
1769
1827
  apiKey: this.apiKey,
@@ -1871,7 +1929,17 @@ var Schematic = class {
1871
1929
  { value }
1872
1930
  );
1873
1931
  }
1874
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1932
+ listeners.forEach((listener, index) => {
1933
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
1934
+ flagKey,
1935
+ value
1936
+ });
1937
+ notifyFlagValueListener(listener, value);
1938
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
1939
+ flagKey,
1940
+ value
1941
+ });
1942
+ });
1875
1943
  };
1876
1944
  };
1877
1945
  var notifyPendingListener = (listener, value) => {
@@ -1900,7 +1968,7 @@ var notifyFlagValueListener = (listener, value) => {
1900
1968
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1901
1969
 
1902
1970
  // src/version.ts
1903
- var version2 = "1.2.9";
1971
+ var version2 = "1.2.10";
1904
1972
 
1905
1973
  // src/context/schematic.tsx
1906
1974
  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.9",
3
+ "version": "1.2.10",
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.9"
34
+ "@schematichq/schematic-js": "^1.2.10"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^9.39.1",