@schematichq/schematic-js 1.2.9 → 1.2.11

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.
@@ -800,7 +800,7 @@ function contextString(context) {
800
800
  }
801
801
 
802
802
  // src/version.ts
803
- var version = "1.2.9";
803
+ var version = "1.2.11";
804
804
 
805
805
  // src/index.ts
806
806
  var anonymousIdKey = "schematicId";
@@ -832,6 +832,8 @@ var Schematic = class {
832
832
  wsReconnectAttempts = 0;
833
833
  wsReconnectTimer = null;
834
834
  wsIntentionalDisconnect = false;
835
+ currentWebSocket = null;
836
+ isConnecting = false;
835
837
  maxEventQueueSize = 100;
836
838
  // Prevent memory issues with very long network outages
837
839
  maxEventRetries = 5;
@@ -1109,6 +1111,49 @@ var Schematic = class {
1109
1111
  console.log(`[Schematic] ${message}`, ...args);
1110
1112
  }
1111
1113
  }
1114
+ /**
1115
+ * Create a persistent message handler for websocket flag updates
1116
+ */
1117
+ createPersistentMessageHandler(context) {
1118
+ return (event) => {
1119
+ const message = JSON.parse(event.data);
1120
+ this.debug(`WebSocket persistent message received:`, message);
1121
+ if (!(contextString(context) in this.checks)) {
1122
+ this.checks[contextString(context)] = {};
1123
+ }
1124
+ (message.flags ?? []).forEach((flag) => {
1125
+ const flagCheck = CheckFlagReturnFromJSON(flag);
1126
+ const contextStr = contextString(context);
1127
+ if (this.checks[contextStr] === void 0) {
1128
+ this.checks[contextStr] = {};
1129
+ }
1130
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1131
+ this.debug(`WebSocket flag update:`, {
1132
+ flag: flagCheck.flag,
1133
+ value: flagCheck.value,
1134
+ flagCheck
1135
+ });
1136
+ if (typeof flagCheck.featureUsageEvent === "string") {
1137
+ this.updateFeatureUsageEventMap(flagCheck);
1138
+ }
1139
+ if ((this.flagCheckListeners[flag.flag]?.size ?? 0) > 0 || (this.flagValueListeners[flag.flag]?.size ?? 0) > 0) {
1140
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1141
+ }
1142
+ this.debug(`About to notify listeners for flag ${flag.flag}`, {
1143
+ flag: flag.flag,
1144
+ value: flagCheck.value
1145
+ });
1146
+ this.notifyFlagCheckListeners(flag.flag, flagCheck);
1147
+ this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1148
+ this.debug(`Finished notifying listeners for flag ${flag.flag}`, {
1149
+ flag: flag.flag,
1150
+ value: flagCheck.value
1151
+ });
1152
+ });
1153
+ this.flushContextDependentEventQueue();
1154
+ this.setIsPending(false);
1155
+ };
1156
+ }
1112
1157
  /**
1113
1158
  * Helper function to check if client is in offline mode
1114
1159
  */
@@ -1260,6 +1305,19 @@ var Schematic = class {
1260
1305
  try {
1261
1306
  this.setIsPending(true);
1262
1307
  if (!this.conn) {
1308
+ if (this.isConnecting) {
1309
+ this.debug(
1310
+ `Connection already in progress, waiting for it to complete`
1311
+ );
1312
+ while (this.isConnecting && this.conn === null) {
1313
+ await new Promise((resolve) => setTimeout(resolve, 10));
1314
+ }
1315
+ if (this.conn !== null) {
1316
+ const socket2 = await this.conn;
1317
+ await this.wsSendMessage(socket2, context);
1318
+ return;
1319
+ }
1320
+ }
1263
1321
  if (this.wsReconnectTimer !== null) {
1264
1322
  this.debug(
1265
1323
  `Cancelling scheduled reconnection, connecting immediately`
@@ -1267,7 +1325,17 @@ var Schematic = class {
1267
1325
  clearTimeout(this.wsReconnectTimer);
1268
1326
  this.wsReconnectTimer = null;
1269
1327
  }
1270
- this.conn = this.wsConnect();
1328
+ this.isConnecting = true;
1329
+ try {
1330
+ this.conn = this.wsConnect();
1331
+ const socket2 = await this.conn;
1332
+ this.isConnecting = false;
1333
+ await this.wsSendMessage(socket2, context);
1334
+ return;
1335
+ } catch (error) {
1336
+ this.isConnecting = false;
1337
+ throw error;
1338
+ }
1271
1339
  }
1272
1340
  const socket = await this.conn;
1273
1341
  await this.wsSendMessage(socket, context);
@@ -1432,10 +1500,14 @@ var Schematic = class {
1432
1500
  }
1433
1501
  }
1434
1502
  if (readyEvents.length === 0) {
1435
- this.debug(`No events ready for retry yet (${notReadyEvents.length} still in backoff)`);
1503
+ this.debug(
1504
+ `No events ready for retry yet (${notReadyEvents.length} still in backoff)`
1505
+ );
1436
1506
  return;
1437
1507
  }
1438
- this.debug(`Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`);
1508
+ this.debug(
1509
+ `Flushing event queue: ${readyEvents.length} ready, ${notReadyEvents.length} waiting`
1510
+ );
1439
1511
  this.eventQueue = notReadyEvents;
1440
1512
  for (const event of readyEvents) {
1441
1513
  try {
@@ -1500,7 +1572,10 @@ var Schematic = class {
1500
1572
  } catch (error) {
1501
1573
  const retryCount = (event.retry_count ?? 0) + 1;
1502
1574
  if (retryCount <= this.maxEventRetries) {
1503
- this.debug(`Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`, error);
1575
+ this.debug(
1576
+ `Event failed to send (attempt ${retryCount}/${this.maxEventRetries}), queueing for retry:`,
1577
+ error
1578
+ );
1504
1579
  const baseDelay = this.eventRetryInitialDelay * Math.pow(2, retryCount - 1);
1505
1580
  const jitterDelay = Math.min(baseDelay, this.eventRetryMaxDelay);
1506
1581
  const nextRetryAt = Date.now() + jitterDelay;
@@ -1511,15 +1586,22 @@ var Schematic = class {
1511
1586
  };
1512
1587
  if (this.eventQueue.length < this.maxEventQueueSize) {
1513
1588
  this.eventQueue.push(retryEvent);
1514
- this.debug(`Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`);
1589
+ this.debug(
1590
+ `Event queued for retry in ${jitterDelay}ms (${this.eventQueue.length}/${this.maxEventQueueSize})`
1591
+ );
1515
1592
  } else {
1516
- this.debug(`Event queue full (${this.maxEventQueueSize}), dropping oldest event`);
1593
+ this.debug(
1594
+ `Event queue full (${this.maxEventQueueSize}), dropping oldest event`
1595
+ );
1517
1596
  this.eventQueue.shift();
1518
1597
  this.eventQueue.push(retryEvent);
1519
1598
  }
1520
1599
  this.startRetryTimer();
1521
1600
  } else {
1522
- this.debug(`Event failed permanently after ${this.maxEventRetries} attempts, dropping:`, error);
1601
+ this.debug(
1602
+ `Event failed permanently after ${this.maxEventRetries} attempts, dropping:`,
1603
+ error
1604
+ );
1523
1605
  }
1524
1606
  }
1525
1607
  return Promise.resolve();
@@ -1549,12 +1631,95 @@ var Schematic = class {
1549
1631
  if (this.conn) {
1550
1632
  try {
1551
1633
  const socket = await this.conn;
1634
+ if (this.currentWebSocket === socket) {
1635
+ this.debug(`Cleaning up current websocket tracking`);
1636
+ this.currentWebSocket = null;
1637
+ }
1552
1638
  socket.close();
1553
1639
  } catch (error) {
1554
1640
  console.error("Error during cleanup:", error);
1555
1641
  } finally {
1556
1642
  this.conn = null;
1643
+ this.currentWebSocket = null;
1644
+ this.isConnecting = false;
1645
+ }
1646
+ }
1647
+ };
1648
+ /**
1649
+ * Force an immediate WebSocket reconnection.
1650
+ * This is useful when the application returns from a background state (e.g., mobile app
1651
+ * coming back to foreground) and wants to immediately re-establish the connection
1652
+ * rather than waiting for the exponential backoff timer.
1653
+ *
1654
+ * This method will:
1655
+ * - Cancel any pending reconnection timer
1656
+ * - Reset the reconnection attempt counter
1657
+ * - Immediately attempt to reconnect
1658
+ * - Re-send the current context to get fresh flag values
1659
+ *
1660
+ * If a connection is already active and healthy, this will close it and create a new one.
1661
+ * If you just want to ensure a connection exists, check the connection state first.
1662
+ *
1663
+ * @example
1664
+ * ```typescript
1665
+ * // React Native example: reconnect when app comes to foreground
1666
+ * useEffect(() => {
1667
+ * const subscription = AppState.addEventListener("change", (state) => {
1668
+ * if (state === "active") {
1669
+ * client.forceReconnect();
1670
+ * }
1671
+ * });
1672
+ * return () => subscription.remove();
1673
+ * }, [client]);
1674
+ * ```
1675
+ */
1676
+ forceReconnect = async () => {
1677
+ if (this.isOffline()) {
1678
+ this.debug("forceReconnect: skipped (offline mode)");
1679
+ return Promise.resolve();
1680
+ }
1681
+ this.debug("forceReconnect: forcing immediate reconnection");
1682
+ this.wsIntentionalDisconnect = false;
1683
+ if (this.wsReconnectTimer !== null) {
1684
+ this.debug("forceReconnect: cancelling pending reconnection timer");
1685
+ clearTimeout(this.wsReconnectTimer);
1686
+ this.wsReconnectTimer = null;
1687
+ }
1688
+ this.wsReconnectAttempts = 0;
1689
+ if (this.conn !== null) {
1690
+ this.debug("forceReconnect: closing existing connection");
1691
+ try {
1692
+ const existingSocket = await this.conn;
1693
+ if (this.currentWebSocket === existingSocket) {
1694
+ this.currentWebSocket = null;
1695
+ }
1696
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1697
+ existingSocket.close();
1698
+ }
1699
+ } catch (error) {
1700
+ this.debug("forceReconnect: error closing existing connection:", error);
1701
+ }
1702
+ this.conn = null;
1703
+ this.isConnecting = false;
1704
+ }
1705
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1706
+ this.debug("forceReconnect: reconnecting with existing context");
1707
+ try {
1708
+ this.isConnecting = true;
1709
+ this.conn = this.wsConnect();
1710
+ const socket = await this.conn;
1711
+ this.isConnecting = false;
1712
+ await this.wsSendMessage(socket, this.context, true);
1713
+ this.debug("forceReconnect: reconnection successful");
1714
+ } catch (error) {
1715
+ this.isConnecting = false;
1716
+ this.debug("forceReconnect: reconnection failed:", error);
1717
+ this.attemptReconnect();
1557
1718
  }
1719
+ } else {
1720
+ this.debug(
1721
+ "forceReconnect: no context available, websocket will connect when context is set"
1722
+ );
1558
1723
  }
1559
1724
  };
1560
1725
  /**
@@ -1578,7 +1743,9 @@ var Schematic = class {
1578
1743
  if (this.conn !== null) {
1579
1744
  try {
1580
1745
  const socket = await this.conn;
1581
- socket.close();
1746
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
1747
+ socket.close();
1748
+ }
1582
1749
  } catch (error) {
1583
1750
  this.debug("Error closing connection on offline:", error);
1584
1751
  }
@@ -1593,7 +1760,9 @@ var Schematic = class {
1593
1760
  * Handle browser coming back online
1594
1761
  */
1595
1762
  handleNetworkOnline = () => {
1596
- this.debug("Network online, attempting reconnection and flushing queued events");
1763
+ this.debug(
1764
+ "Network online, attempting reconnection and flushing queued events"
1765
+ );
1597
1766
  this.wsReconnectAttempts = 0;
1598
1767
  if (this.wsReconnectTimer !== null) {
1599
1768
  clearTimeout(this.wsReconnectTimer);
@@ -1616,7 +1785,10 @@ var Schematic = class {
1616
1785
  return;
1617
1786
  }
1618
1787
  if (this.wsReconnectTimer !== null) {
1619
- clearTimeout(this.wsReconnectTimer);
1788
+ this.debug(
1789
+ `Reconnection attempt already scheduled, ignoring duplicate request`
1790
+ );
1791
+ return;
1620
1792
  }
1621
1793
  const delay = this.calculateReconnectDelay();
1622
1794
  this.debug(
@@ -1629,23 +1801,57 @@ var Schematic = class {
1629
1801
  `Attempting to reconnect (attempt ${this.wsReconnectAttempts}/${this.webSocketMaxReconnectAttempts})`
1630
1802
  );
1631
1803
  try {
1632
- this.conn = this.wsConnect();
1633
- const socket = await this.conn;
1634
- this.debug(`Reconnection context check:`, {
1635
- hasCompany: this.context.company !== void 0,
1636
- hasUser: this.context.user !== void 0,
1637
- context: this.context
1638
- });
1639
- if (this.context.company !== void 0 || this.context.user !== void 0) {
1640
- this.debug(`Reconnected, force re-sending context`);
1641
- await this.wsSendContextAfterReconnection(socket, this.context);
1642
- } else {
1643
- this.debug(`No context to re-send after reconnection - websocket ready for new context`);
1804
+ if (this.conn !== null) {
1805
+ this.debug(`Cleaning up existing connection before reconnection`);
1806
+ try {
1807
+ const existingSocket = await this.conn;
1808
+ if (this.currentWebSocket === existingSocket) {
1809
+ this.debug(`Existing websocket is current, will be replaced`);
1810
+ this.currentWebSocket = null;
1811
+ }
1812
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1813
+ existingSocket.close();
1814
+ }
1815
+ } catch (error) {
1816
+ this.debug(`Error cleaning up existing connection:`, error);
1817
+ }
1818
+ this.conn = null;
1819
+ this.currentWebSocket = null;
1820
+ this.isConnecting = false;
1821
+ }
1822
+ this.isConnecting = true;
1823
+ try {
1824
+ this.conn = this.wsConnect();
1825
+ const socket = await this.conn;
1826
+ this.isConnecting = false;
1827
+ this.debug(`Reconnection context check:`, {
1828
+ hasCompany: this.context.company !== void 0,
1829
+ hasUser: this.context.user !== void 0,
1830
+ context: this.context
1831
+ });
1832
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1833
+ this.debug(`Reconnected, force re-sending context`);
1834
+ await this.wsSendMessage(socket, this.context, true);
1835
+ } else {
1836
+ this.debug(
1837
+ `No context to re-send after reconnection - websocket ready for new context`
1838
+ );
1839
+ this.debug(
1840
+ `Setting up tracking for reconnected websocket (no context to send)`
1841
+ );
1842
+ this.currentWebSocket = socket;
1843
+ }
1844
+ this.flushEventQueue().catch((error) => {
1845
+ this.debug(
1846
+ "Error flushing event queue after websocket reconnection:",
1847
+ error
1848
+ );
1849
+ });
1850
+ this.debug(`Reconnection successful`);
1851
+ } catch (error) {
1852
+ this.isConnecting = false;
1853
+ throw error;
1644
1854
  }
1645
- this.flushEventQueue().catch((error) => {
1646
- this.debug("Error flushing event queue after websocket reconnection:", error);
1647
- });
1648
- this.debug(`Reconnection successful`);
1649
1855
  } catch (error) {
1650
1856
  this.debug(`Reconnection attempt failed:`, error);
1651
1857
  }
@@ -1663,6 +1869,8 @@ var Schematic = class {
1663
1869
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1664
1870
  this.debug(`connecting to WebSocket:`, wsUrl);
1665
1871
  const webSocket = new WebSocket(wsUrl);
1872
+ const connectionId = Math.random().toString(36).substring(7);
1873
+ this.debug(`Creating WebSocket connection ${connectionId} to ${wsUrl}`);
1666
1874
  let timeoutId = null;
1667
1875
  let isResolved = false;
1668
1876
  timeoutId = setTimeout(() => {
@@ -1681,7 +1889,7 @@ var Schematic = class {
1681
1889
  }
1682
1890
  this.wsReconnectAttempts = 0;
1683
1891
  this.wsIntentionalDisconnect = false;
1684
- this.debug(`WebSocket connection opened`);
1892
+ this.debug(`WebSocket connection ${connectionId} opened successfully`);
1685
1893
  resolve(webSocket);
1686
1894
  };
1687
1895
  webSocket.onerror = (error) => {
@@ -1689,7 +1897,7 @@ var Schematic = class {
1689
1897
  if (timeoutId !== null) {
1690
1898
  clearTimeout(timeoutId);
1691
1899
  }
1692
- this.debug(`WebSocket connection error:`, error);
1900
+ this.debug(`WebSocket connection ${connectionId} error:`, error);
1693
1901
  reject(error);
1694
1902
  };
1695
1903
  webSocket.onclose = () => {
@@ -1697,121 +1905,48 @@ var Schematic = class {
1697
1905
  if (timeoutId !== null) {
1698
1906
  clearTimeout(timeoutId);
1699
1907
  }
1700
- this.debug(`WebSocket connection closed`);
1908
+ this.debug(`WebSocket connection ${connectionId} closed`);
1701
1909
  this.conn = null;
1910
+ if (this.currentWebSocket === webSocket) {
1911
+ this.currentWebSocket = null;
1912
+ this.isConnecting = false;
1913
+ }
1702
1914
  if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1703
1915
  this.attemptReconnect();
1704
1916
  }
1705
1917
  };
1706
1918
  });
1707
1919
  };
1708
- // Send a message on the websocket after reconnection, forcing the send even if context appears unchanged
1709
- // because the server has lost all state and needs the initial context
1710
- wsSendContextAfterReconnection = (socket, context) => {
1711
- if (this.isOffline()) {
1712
- this.debug("wsSendContextAfterReconnection: skipped (offline mode)");
1713
- this.setIsPending(false);
1714
- return Promise.resolve();
1715
- }
1716
- return new Promise((resolve) => {
1717
- this.debug(`WebSocket force sending context after reconnection:`, context);
1718
- this.context = context;
1719
- const sendMessage = () => {
1720
- let resolved = false;
1721
- const messageHandler = (event) => {
1722
- const message = JSON.parse(event.data);
1723
- this.debug(`WebSocket message received after reconnection:`, message);
1724
- if (!(contextString(context) in this.checks)) {
1725
- this.checks[contextString(context)] = {};
1726
- }
1727
- (message.flags ?? []).forEach((flag) => {
1728
- const flagCheck = CheckFlagReturnFromJSON(flag);
1729
- const contextStr = contextString(context);
1730
- if (this.checks[contextStr] === void 0) {
1731
- this.checks[contextStr] = {};
1732
- }
1733
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1734
- });
1735
- this.useWebSocket = true;
1736
- socket.removeEventListener("message", messageHandler);
1737
- if (!resolved) {
1738
- resolved = true;
1739
- resolve(this.setIsPending(false));
1740
- }
1741
- };
1742
- socket.addEventListener("message", messageHandler);
1743
- const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1744
- const messagePayload = {
1745
- apiKey: this.apiKey,
1746
- clientVersion,
1747
- data: context
1748
- };
1749
- this.debug(`WebSocket sending forced message after reconnection:`, messagePayload);
1750
- socket.send(JSON.stringify(messagePayload));
1751
- };
1752
- if (socket.readyState === WebSocket.OPEN) {
1753
- this.debug(`WebSocket already open, sending forced message after reconnection`);
1754
- sendMessage();
1755
- } else {
1756
- socket.addEventListener("open", () => {
1757
- this.debug(`WebSocket opened, sending forced message after reconnection`);
1758
- sendMessage();
1759
- });
1760
- }
1761
- });
1762
- };
1763
1920
  // Send a message on the websocket indicating interest in a particular evaluation context
1764
1921
  // and wait for the initial set of flag values to be returned
1765
- wsSendMessage = (socket, context) => {
1922
+ wsSendMessage = (socket, context, forceContextSend = false) => {
1766
1923
  if (this.isOffline()) {
1767
1924
  this.debug("wsSendMessage: skipped (offline mode)");
1768
1925
  this.setIsPending(false);
1769
1926
  return Promise.resolve();
1770
1927
  }
1771
1928
  return new Promise((resolve, reject) => {
1772
- if (contextString(context) == contextString(this.context)) {
1929
+ if (!forceContextSend && contextString(context) == contextString(this.context)) {
1773
1930
  this.debug(`WebSocket context unchanged, skipping update`);
1774
1931
  return resolve(this.setIsPending(false));
1775
1932
  }
1776
- this.debug(`WebSocket context updated:`, context);
1933
+ this.debug(
1934
+ forceContextSend ? `WebSocket force sending context (reconnection):` : `WebSocket context updated:`,
1935
+ context
1936
+ );
1777
1937
  this.context = context;
1778
1938
  const sendMessage = () => {
1779
1939
  let resolved = false;
1940
+ const persistentMessageHandler = this.createPersistentMessageHandler(context);
1780
1941
  const messageHandler = (event) => {
1781
- const message = JSON.parse(event.data);
1782
- this.debug(`WebSocket message received:`, message);
1783
- if (!(contextString(context) in this.checks)) {
1784
- this.checks[contextString(context)] = {};
1785
- }
1786
- (message.flags ?? []).forEach((flag) => {
1787
- const flagCheck = CheckFlagReturnFromJSON(flag);
1788
- const contextStr = contextString(context);
1789
- if (this.checks[contextStr] === void 0) {
1790
- this.checks[contextStr] = {};
1791
- }
1792
- this.checks[contextStr][flagCheck.flag] = flagCheck;
1793
- this.debug(`WebSocket flag update:`, {
1794
- flag: flagCheck.flag,
1795
- value: flagCheck.value,
1796
- flagCheck
1797
- });
1798
- if (typeof flagCheck.featureUsageEvent === "string") {
1799
- this.updateFeatureUsageEventMap(flagCheck);
1800
- }
1801
- if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1802
- this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1803
- }
1804
- this.notifyFlagCheckListeners(flag.flag, flagCheck);
1805
- this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1806
- });
1807
- this.flushContextDependentEventQueue();
1808
- this.setIsPending(false);
1942
+ persistentMessageHandler(event);
1809
1943
  if (!resolved) {
1810
1944
  resolved = true;
1811
1945
  resolve();
1812
1946
  }
1813
1947
  };
1814
1948
  socket.addEventListener("message", messageHandler);
1949
+ this.currentWebSocket = socket;
1815
1950
  const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1816
1951
  const messagePayload = {
1817
1952
  apiKey: this.apiKey,
@@ -1919,7 +2054,17 @@ var Schematic = class {
1919
2054
  { value }
1920
2055
  );
1921
2056
  }
1922
- listeners.forEach((listener) => notifyFlagValueListener(listener, value));
2057
+ listeners.forEach((listener, index) => {
2058
+ this.debug(`Calling listener ${index} for flag ${flagKey}`, {
2059
+ flagKey,
2060
+ value
2061
+ });
2062
+ notifyFlagValueListener(listener, value);
2063
+ this.debug(`Listener ${index} for flag ${flagKey} completed`, {
2064
+ flagKey,
2065
+ value
2066
+ });
2067
+ });
1923
2068
  };
1924
2069
  };
1925
2070
  var notifyPendingListener = (listener, value) => {
@@ -378,6 +378,8 @@ export declare class Schematic {
378
378
  private wsReconnectAttempts;
379
379
  private wsReconnectTimer;
380
380
  private wsIntentionalDisconnect;
381
+ private currentWebSocket;
382
+ private isConnecting;
381
383
  private maxEventQueueSize;
382
384
  private maxEventRetries;
383
385
  private eventRetryInitialDelay;
@@ -413,7 +415,11 @@ export declare class Schematic {
413
415
  * Helper function to log debug messages
414
416
  * Only logs if debug mode is enabled
415
417
  */
416
- private debug;
418
+ debug(message: string, ...args: unknown[]): void;
419
+ /**
420
+ * Create a persistent message handler for websocket flag updates
421
+ */
422
+ private createPersistentMessageHandler;
417
423
  /**
418
424
  * Helper function to check if client is in offline mode
419
425
  */
@@ -481,6 +487,35 @@ export declare class Schematic {
481
487
  * In offline mode, this is a no-op.
482
488
  */
483
489
  cleanup: () => Promise<void>;
490
+ /**
491
+ * Force an immediate WebSocket reconnection.
492
+ * This is useful when the application returns from a background state (e.g., mobile app
493
+ * coming back to foreground) and wants to immediately re-establish the connection
494
+ * rather than waiting for the exponential backoff timer.
495
+ *
496
+ * This method will:
497
+ * - Cancel any pending reconnection timer
498
+ * - Reset the reconnection attempt counter
499
+ * - Immediately attempt to reconnect
500
+ * - Re-send the current context to get fresh flag values
501
+ *
502
+ * If a connection is already active and healthy, this will close it and create a new one.
503
+ * If you just want to ensure a connection exists, check the connection state first.
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * // React Native example: reconnect when app comes to foreground
508
+ * useEffect(() => {
509
+ * const subscription = AppState.addEventListener("change", (state) => {
510
+ * if (state === "active") {
511
+ * client.forceReconnect();
512
+ * }
513
+ * });
514
+ * return () => subscription.remove();
515
+ * }, [client]);
516
+ * ```
517
+ */
518
+ forceReconnect: () => Promise<void>;
484
519
  /**
485
520
  * Calculate the delay for the next reconnection attempt using exponential backoff with jitter.
486
521
  * This helps prevent dogpiling when the server recovers from an outage.
@@ -500,7 +535,6 @@ export declare class Schematic {
500
535
  */
501
536
  private attemptReconnect;
502
537
  private wsConnect;
503
- private wsSendContextAfterReconnection;
504
538
  private wsSendMessage;
505
539
  /**
506
540
  * State management