@schematichq/schematic-react 1.2.10 → 1.2.13

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.
package/README.md CHANGED
@@ -136,6 +136,43 @@ const MyComponent = () => {
136
136
 
137
137
  *Note: `useSchematicIsPending` is checking if entitlement data has been loaded, typically via `identify`. It should, therefore, be used to wrap flag and entitlement checks, but never the initial call to `identify`.*
138
138
 
139
+ ## React Native
140
+
141
+ ### Handling app background/foreground
142
+
143
+ When a React Native app is backgrounded for an extended period, the WebSocket connection may be closed by the OS. When the app returns to the foreground, the connection will automatically reconnect, but this happens on an exponential backoff timer which may cause a delay before fresh flag values are available.
144
+
145
+ For cases where you need immediate flag updates when returning to the foreground (e.g., after an in-app purchase), you can use one of these methods to re-establish the connection:
146
+
147
+ - `forceReconnect()`: Always closes and re-establishes the WebSocket connection, guaranteeing fresh values
148
+ - `reconnectIfNeeded()`: Only reconnects if the current connection is unhealthy (more efficient for frequent foreground events)
149
+
150
+ ```tsx
151
+ import { useSchematic } from "@schematichq/schematic-react";
152
+ import { useEffect } from "react";
153
+ import { AppState } from "react-native";
154
+
155
+ const SchematicAppStateHandler = () => {
156
+ const { client } = useSchematic();
157
+
158
+ useEffect(() => {
159
+ const subscription = AppState.addEventListener("change", (state) => {
160
+ if (state === "active") {
161
+ // Use forceReconnect() for guaranteed fresh values
162
+ client.forceReconnect();
163
+ // Or use reconnectIfNeeded() to skip if connection is healthy
164
+ // client.reconnectIfNeeded();
165
+ }
166
+ });
167
+ return () => subscription.remove();
168
+ }, [client]);
169
+
170
+ return null;
171
+ };
172
+ ```
173
+
174
+ Render this inside your `SchematicProvider`.
175
+
139
176
  ## Troubleshooting
140
177
 
141
178
  For debugging and development, Schematic supports two special modes:
@@ -799,7 +799,7 @@ function contextString(context) {
799
799
  }, {});
800
800
  return JSON.stringify(sortedContext);
801
801
  }
802
- var version = "1.2.10";
802
+ var version = "1.2.13";
803
803
  var anonymousIdKey = "schematicId";
804
804
  var Schematic = class {
805
805
  additionalHeaders = {};
@@ -824,6 +824,9 @@ var Schematic = class {
824
824
  webSocketConnectionTimeout = 1e4;
825
825
  webSocketReconnect = true;
826
826
  webSocketMaxReconnectAttempts = 7;
827
+ // Max attempts after connection disrupted
828
+ webSocketMaxConnectionAttempts = 3;
829
+ // Max attempts for initial connection
827
830
  webSocketInitialRetryDelay = 1e3;
828
831
  webSocketMaxRetryDelay = 3e4;
829
832
  wsReconnectAttempts = 0;
@@ -1047,7 +1050,7 @@ var Schematic = class {
1047
1050
  this.submitFlagCheckEvent(key, result, context);
1048
1051
  return result.value;
1049
1052
  }).catch((error) => {
1050
- console.error("There was a problem with the fetch operation:", error);
1053
+ console.warn("There was a problem with the fetch operation:", error);
1051
1054
  const errorResult = this.resolveFallbackCheckFlagReturn(
1052
1055
  key,
1053
1056
  fallback,
@@ -1070,10 +1073,7 @@ var Schematic = class {
1070
1073
  try {
1071
1074
  await this.setContext(context);
1072
1075
  } catch (error) {
1073
- console.error(
1074
- "WebSocket connection failed, falling back to REST:",
1075
- error
1076
- );
1076
+ console.warn("WebSocket connection failed, falling back to REST:", error);
1077
1077
  return this.fallbackToRest(key, context, fallback);
1078
1078
  }
1079
1079
  const contextVals = this.checks[contextStr] ?? {};
@@ -1213,7 +1213,7 @@ var Schematic = class {
1213
1213
  this.submitFlagCheckEvent(key, result, context);
1214
1214
  return result.value;
1215
1215
  } catch (error) {
1216
- console.error("REST API call failed, using fallback value:", error);
1216
+ console.warn("REST API call failed, using fallback value:", error);
1217
1217
  const errorResult = this.resolveFallbackCheckFlagReturn(
1218
1218
  key,
1219
1219
  fallback,
@@ -1262,7 +1262,7 @@ var Schematic = class {
1262
1262
  {}
1263
1263
  );
1264
1264
  }).catch((error) => {
1265
- console.error("There was a problem with the fetch operation:", error);
1265
+ console.warn("There was a problem with the fetch operation:", error);
1266
1266
  return {};
1267
1267
  });
1268
1268
  };
@@ -1279,7 +1279,7 @@ var Schematic = class {
1279
1279
  user: body.keys
1280
1280
  });
1281
1281
  } catch (error) {
1282
- console.error("Error setting context:", error);
1282
+ console.warn("Error setting context:", error);
1283
1283
  }
1284
1284
  return this.handleEvent("identify", body);
1285
1285
  };
@@ -1337,7 +1337,7 @@ var Schematic = class {
1337
1337
  const socket = await this.conn;
1338
1338
  await this.wsSendMessage(socket, context);
1339
1339
  } catch (error) {
1340
- console.error("Failed to establish WebSocket connection:", error);
1340
+ console.warn("Failed to establish WebSocket connection:", error);
1341
1341
  throw error;
1342
1342
  }
1343
1343
  };
@@ -1610,6 +1610,130 @@ var Schematic = class {
1610
1610
  /**
1611
1611
  * Websocket management
1612
1612
  */
1613
+ /**
1614
+ * Force an immediate WebSocket reconnection.
1615
+ * This is useful when the application returns from a background state (e.g., mobile app
1616
+ * coming back to foreground) and wants to immediately re-establish the connection
1617
+ * rather than waiting for the exponential backoff timer.
1618
+ *
1619
+ * This method will:
1620
+ * - Cancel any pending reconnection timer
1621
+ * - Reset the reconnection attempt counter
1622
+ * - Close any existing connection
1623
+ * - Immediately attempt to reconnect
1624
+ * - Re-send the current context to get fresh flag values
1625
+ *
1626
+ * Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * // React Native example: reconnect when app comes to foreground
1631
+ * useEffect(() => {
1632
+ * const subscription = AppState.addEventListener("change", (state) => {
1633
+ * if (state === "active") {
1634
+ * client.forceReconnect();
1635
+ * }
1636
+ * });
1637
+ * return () => subscription.remove();
1638
+ * }, [client]);
1639
+ * ```
1640
+ */
1641
+ forceReconnect = async () => {
1642
+ return this.reconnect({ force: true });
1643
+ };
1644
+ /**
1645
+ * Reconnect the WebSocket connection only if the current connection is unhealthy.
1646
+ * This is useful when the application returns from a background state and wants to
1647
+ * ensure a healthy connection exists, but doesn't need to force a reconnection if
1648
+ * the connection is still active.
1649
+ *
1650
+ * This method will:
1651
+ * - Check if an existing connection is healthy (readyState === OPEN)
1652
+ * - If healthy, return immediately without reconnecting
1653
+ * - If unhealthy, perform the same reconnection logic as forceReconnect()
1654
+ *
1655
+ * Use this when you want efficient reconnection that avoids unnecessary disconnects.
1656
+ *
1657
+ * @example
1658
+ * ```typescript
1659
+ * // React Native example: reconnect only if needed when app comes to foreground
1660
+ * useEffect(() => {
1661
+ * const subscription = AppState.addEventListener("change", (state) => {
1662
+ * if (state === "active") {
1663
+ * client.reconnectIfNeeded();
1664
+ * }
1665
+ * });
1666
+ * return () => subscription.remove();
1667
+ * }, [client]);
1668
+ * ```
1669
+ */
1670
+ reconnectIfNeeded = async () => {
1671
+ return this.reconnect({ force: false });
1672
+ };
1673
+ /**
1674
+ * Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
1675
+ */
1676
+ reconnect = async (options) => {
1677
+ const { force } = options;
1678
+ const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
1679
+ if (this.isOffline()) {
1680
+ this.debug(`${methodName}: skipped (offline mode)`);
1681
+ return Promise.resolve();
1682
+ }
1683
+ if (!force && this.conn !== null) {
1684
+ try {
1685
+ const existingSocket = await this.conn;
1686
+ if (existingSocket.readyState === WebSocket.OPEN) {
1687
+ this.debug(`${methodName}: connection is healthy, skipping`);
1688
+ return Promise.resolve();
1689
+ }
1690
+ } catch {
1691
+ }
1692
+ }
1693
+ this.debug(
1694
+ `${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
1695
+ );
1696
+ this.wsIntentionalDisconnect = false;
1697
+ if (this.wsReconnectTimer !== null) {
1698
+ this.debug(`${methodName}: cancelling pending reconnection timer`);
1699
+ clearTimeout(this.wsReconnectTimer);
1700
+ this.wsReconnectTimer = null;
1701
+ }
1702
+ this.wsReconnectAttempts = 0;
1703
+ if (this.conn !== null) {
1704
+ this.debug(`${methodName}: closing existing connection`);
1705
+ try {
1706
+ const existingSocket = await this.conn;
1707
+ if (this.currentWebSocket === existingSocket) {
1708
+ this.currentWebSocket = null;
1709
+ }
1710
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1711
+ existingSocket.close();
1712
+ }
1713
+ } catch (error) {
1714
+ this.debug(`${methodName}: error closing existing connection:`, error);
1715
+ }
1716
+ this.conn = null;
1717
+ this.isConnecting = false;
1718
+ }
1719
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1720
+ this.debug(`${methodName}: reconnecting with existing context`);
1721
+ try {
1722
+ this.isConnecting = true;
1723
+ this.conn = this.wsConnect();
1724
+ const socket = await this.conn;
1725
+ this.isConnecting = false;
1726
+ await this.wsSendMessage(socket, this.context, true);
1727
+ this.debug(`${methodName}: reconnection successful`);
1728
+ } catch (error) {
1729
+ this.isConnecting = false;
1730
+ this.debug(`${methodName}: reconnection failed:`, error);
1731
+ }
1732
+ } else {
1733
+ this.debug(`${methodName}: no context set, skipping reconnection`);
1734
+ }
1735
+ return Promise.resolve();
1736
+ };
1613
1737
  /**
1614
1738
  * If using websocket mode, close the connection when done.
1615
1739
  * In offline mode, this is a no-op.
@@ -1634,7 +1758,7 @@ var Schematic = class {
1634
1758
  }
1635
1759
  socket.close();
1636
1760
  } catch (error) {
1637
- console.error("Error during cleanup:", error);
1761
+ console.warn("Error during cleanup:", error);
1638
1762
  } finally {
1639
1763
  this.conn = null;
1640
1764
  this.currentWebSocket = null;
@@ -1777,14 +1901,49 @@ var Schematic = class {
1777
1901
  }
1778
1902
  }, delay);
1779
1903
  };
1780
- // Open a websocket connection
1781
- wsConnect = () => {
1904
+ // Open a websocket connection with retry logic for timeouts
1905
+ wsConnect = async () => {
1782
1906
  if (this.isOffline()) {
1783
1907
  this.debug("wsConnect: skipped (offline mode)");
1784
- return Promise.reject(
1785
- new Error("WebSocket connection skipped in offline mode")
1786
- );
1908
+ throw new Error("WebSocket connection skipped in offline mode");
1787
1909
  }
1910
+ let lastError = null;
1911
+ const maxAttempts = this.webSocketMaxConnectionAttempts;
1912
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1913
+ try {
1914
+ const socket = await this.wsConnectOnce();
1915
+ this.wsReconnectAttempts = 0;
1916
+ return socket;
1917
+ } catch (error) {
1918
+ lastError = error instanceof Error ? error : new Error(String(error));
1919
+ const isTimeout = lastError.message === "WebSocket connection timeout";
1920
+ if (!isTimeout) {
1921
+ this.debug(
1922
+ `WebSocket connection failed with non-timeout error, not retrying:`,
1923
+ lastError.message
1924
+ );
1925
+ throw lastError;
1926
+ }
1927
+ if (attempt < maxAttempts - 1) {
1928
+ const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
1929
+ const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
1930
+ const jitter = cappedDelay * 0.2 * Math.random();
1931
+ const delay = cappedDelay + jitter;
1932
+ this.debug(
1933
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
1934
+ );
1935
+ await new Promise((resolve) => setTimeout(resolve, delay));
1936
+ } else {
1937
+ this.debug(
1938
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
1939
+ );
1940
+ }
1941
+ }
1942
+ }
1943
+ throw lastError ?? new Error("WebSocket connection failed");
1944
+ };
1945
+ // Single attempt to open a websocket connection (no retry logic)
1946
+ wsConnectOnce = () => {
1788
1947
  return new Promise((resolve, reject) => {
1789
1948
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1790
1949
  this.debug(`connecting to WebSocket:`, wsUrl);
@@ -1795,6 +1954,7 @@ var Schematic = class {
1795
1954
  let isResolved = false;
1796
1955
  timeoutId = setTimeout(() => {
1797
1956
  if (!isResolved) {
1957
+ isResolved = true;
1798
1958
  this.debug(
1799
1959
  `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1800
1960
  );
@@ -1803,16 +1963,17 @@ var Schematic = class {
1803
1963
  }
1804
1964
  }, this.webSocketConnectionTimeout);
1805
1965
  webSocket.onopen = () => {
1966
+ if (isResolved) return;
1806
1967
  isResolved = true;
1807
1968
  if (timeoutId !== null) {
1808
1969
  clearTimeout(timeoutId);
1809
1970
  }
1810
- this.wsReconnectAttempts = 0;
1811
1971
  this.wsIntentionalDisconnect = false;
1812
1972
  this.debug(`WebSocket connection ${connectionId} opened successfully`);
1813
1973
  resolve(webSocket);
1814
1974
  };
1815
1975
  webSocket.onerror = (error) => {
1976
+ if (isResolved) return;
1816
1977
  isResolved = true;
1817
1978
  if (timeoutId !== null) {
1818
1979
  clearTimeout(timeoutId);
@@ -1821,7 +1982,6 @@ var Schematic = class {
1821
1982
  reject(error);
1822
1983
  };
1823
1984
  webSocket.onclose = () => {
1824
- isResolved = true;
1825
1985
  if (timeoutId !== null) {
1826
1986
  clearTimeout(timeoutId);
1827
1987
  }
@@ -1831,7 +1991,7 @@ var Schematic = class {
1831
1991
  this.currentWebSocket = null;
1832
1992
  this.isConnecting = false;
1833
1993
  }
1834
- if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1994
+ if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
1835
1995
  this.attemptReconnect();
1836
1996
  }
1837
1997
  };
@@ -2013,7 +2173,7 @@ var notifyFlagValueListener = (listener, value) => {
2013
2173
  var import_react = __toESM(require("react"));
2014
2174
 
2015
2175
  // src/version.ts
2016
- var version2 = "1.2.10";
2176
+ var version2 = "1.2.13";
2017
2177
 
2018
2178
  // src/context/schematic.tsx
2019
2179
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -2028,7 +2188,7 @@ var SchematicProvider = ({
2028
2188
  }) => {
2029
2189
  const initialOptsRef = (0, import_react.useRef)({
2030
2190
  publishableKey,
2031
- useWebSocket: clientOpts.useWebSocket ?? true,
2191
+ useWebSocket: true,
2032
2192
  additionalHeaders: {
2033
2193
  "X-Schematic-Client-Version": `schematic-react@${version2}`
2034
2194
  },
@@ -15,7 +15,7 @@ import { StoragePersister } from '@schematichq/schematic-js';
15
15
  import { Traits } from '@schematichq/schematic-js';
16
16
  import { UsagePeriod } from '@schematichq/schematic-js';
17
17
 
18
- declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey"> & {
18
+ declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey" | "useWebSocket"> & {
19
19
  children: React_2.ReactNode;
20
20
  };
21
21
 
@@ -754,7 +754,7 @@ function contextString(context) {
754
754
  }, {});
755
755
  return JSON.stringify(sortedContext);
756
756
  }
757
- var version = "1.2.10";
757
+ var version = "1.2.13";
758
758
  var anonymousIdKey = "schematicId";
759
759
  var Schematic = class {
760
760
  additionalHeaders = {};
@@ -779,6 +779,9 @@ var Schematic = class {
779
779
  webSocketConnectionTimeout = 1e4;
780
780
  webSocketReconnect = true;
781
781
  webSocketMaxReconnectAttempts = 7;
782
+ // Max attempts after connection disrupted
783
+ webSocketMaxConnectionAttempts = 3;
784
+ // Max attempts for initial connection
782
785
  webSocketInitialRetryDelay = 1e3;
783
786
  webSocketMaxRetryDelay = 3e4;
784
787
  wsReconnectAttempts = 0;
@@ -1002,7 +1005,7 @@ var Schematic = class {
1002
1005
  this.submitFlagCheckEvent(key, result, context);
1003
1006
  return result.value;
1004
1007
  }).catch((error) => {
1005
- console.error("There was a problem with the fetch operation:", error);
1008
+ console.warn("There was a problem with the fetch operation:", error);
1006
1009
  const errorResult = this.resolveFallbackCheckFlagReturn(
1007
1010
  key,
1008
1011
  fallback,
@@ -1025,10 +1028,7 @@ var Schematic = class {
1025
1028
  try {
1026
1029
  await this.setContext(context);
1027
1030
  } catch (error) {
1028
- console.error(
1029
- "WebSocket connection failed, falling back to REST:",
1030
- error
1031
- );
1031
+ console.warn("WebSocket connection failed, falling back to REST:", error);
1032
1032
  return this.fallbackToRest(key, context, fallback);
1033
1033
  }
1034
1034
  const contextVals = this.checks[contextStr] ?? {};
@@ -1168,7 +1168,7 @@ var Schematic = class {
1168
1168
  this.submitFlagCheckEvent(key, result, context);
1169
1169
  return result.value;
1170
1170
  } catch (error) {
1171
- console.error("REST API call failed, using fallback value:", error);
1171
+ console.warn("REST API call failed, using fallback value:", error);
1172
1172
  const errorResult = this.resolveFallbackCheckFlagReturn(
1173
1173
  key,
1174
1174
  fallback,
@@ -1217,7 +1217,7 @@ var Schematic = class {
1217
1217
  {}
1218
1218
  );
1219
1219
  }).catch((error) => {
1220
- console.error("There was a problem with the fetch operation:", error);
1220
+ console.warn("There was a problem with the fetch operation:", error);
1221
1221
  return {};
1222
1222
  });
1223
1223
  };
@@ -1234,7 +1234,7 @@ var Schematic = class {
1234
1234
  user: body.keys
1235
1235
  });
1236
1236
  } catch (error) {
1237
- console.error("Error setting context:", error);
1237
+ console.warn("Error setting context:", error);
1238
1238
  }
1239
1239
  return this.handleEvent("identify", body);
1240
1240
  };
@@ -1292,7 +1292,7 @@ var Schematic = class {
1292
1292
  const socket = await this.conn;
1293
1293
  await this.wsSendMessage(socket, context);
1294
1294
  } catch (error) {
1295
- console.error("Failed to establish WebSocket connection:", error);
1295
+ console.warn("Failed to establish WebSocket connection:", error);
1296
1296
  throw error;
1297
1297
  }
1298
1298
  };
@@ -1565,6 +1565,130 @@ var Schematic = class {
1565
1565
  /**
1566
1566
  * Websocket management
1567
1567
  */
1568
+ /**
1569
+ * Force an immediate WebSocket reconnection.
1570
+ * This is useful when the application returns from a background state (e.g., mobile app
1571
+ * coming back to foreground) and wants to immediately re-establish the connection
1572
+ * rather than waiting for the exponential backoff timer.
1573
+ *
1574
+ * This method will:
1575
+ * - Cancel any pending reconnection timer
1576
+ * - Reset the reconnection attempt counter
1577
+ * - Close any existing connection
1578
+ * - Immediately attempt to reconnect
1579
+ * - Re-send the current context to get fresh flag values
1580
+ *
1581
+ * Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
1582
+ *
1583
+ * @example
1584
+ * ```typescript
1585
+ * // React Native example: reconnect when app comes to foreground
1586
+ * useEffect(() => {
1587
+ * const subscription = AppState.addEventListener("change", (state) => {
1588
+ * if (state === "active") {
1589
+ * client.forceReconnect();
1590
+ * }
1591
+ * });
1592
+ * return () => subscription.remove();
1593
+ * }, [client]);
1594
+ * ```
1595
+ */
1596
+ forceReconnect = async () => {
1597
+ return this.reconnect({ force: true });
1598
+ };
1599
+ /**
1600
+ * Reconnect the WebSocket connection only if the current connection is unhealthy.
1601
+ * This is useful when the application returns from a background state and wants to
1602
+ * ensure a healthy connection exists, but doesn't need to force a reconnection if
1603
+ * the connection is still active.
1604
+ *
1605
+ * This method will:
1606
+ * - Check if an existing connection is healthy (readyState === OPEN)
1607
+ * - If healthy, return immediately without reconnecting
1608
+ * - If unhealthy, perform the same reconnection logic as forceReconnect()
1609
+ *
1610
+ * Use this when you want efficient reconnection that avoids unnecessary disconnects.
1611
+ *
1612
+ * @example
1613
+ * ```typescript
1614
+ * // React Native example: reconnect only if needed when app comes to foreground
1615
+ * useEffect(() => {
1616
+ * const subscription = AppState.addEventListener("change", (state) => {
1617
+ * if (state === "active") {
1618
+ * client.reconnectIfNeeded();
1619
+ * }
1620
+ * });
1621
+ * return () => subscription.remove();
1622
+ * }, [client]);
1623
+ * ```
1624
+ */
1625
+ reconnectIfNeeded = async () => {
1626
+ return this.reconnect({ force: false });
1627
+ };
1628
+ /**
1629
+ * Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
1630
+ */
1631
+ reconnect = async (options) => {
1632
+ const { force } = options;
1633
+ const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
1634
+ if (this.isOffline()) {
1635
+ this.debug(`${methodName}: skipped (offline mode)`);
1636
+ return Promise.resolve();
1637
+ }
1638
+ if (!force && this.conn !== null) {
1639
+ try {
1640
+ const existingSocket = await this.conn;
1641
+ if (existingSocket.readyState === WebSocket.OPEN) {
1642
+ this.debug(`${methodName}: connection is healthy, skipping`);
1643
+ return Promise.resolve();
1644
+ }
1645
+ } catch {
1646
+ }
1647
+ }
1648
+ this.debug(
1649
+ `${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
1650
+ );
1651
+ this.wsIntentionalDisconnect = false;
1652
+ if (this.wsReconnectTimer !== null) {
1653
+ this.debug(`${methodName}: cancelling pending reconnection timer`);
1654
+ clearTimeout(this.wsReconnectTimer);
1655
+ this.wsReconnectTimer = null;
1656
+ }
1657
+ this.wsReconnectAttempts = 0;
1658
+ if (this.conn !== null) {
1659
+ this.debug(`${methodName}: closing existing connection`);
1660
+ try {
1661
+ const existingSocket = await this.conn;
1662
+ if (this.currentWebSocket === existingSocket) {
1663
+ this.currentWebSocket = null;
1664
+ }
1665
+ if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
1666
+ existingSocket.close();
1667
+ }
1668
+ } catch (error) {
1669
+ this.debug(`${methodName}: error closing existing connection:`, error);
1670
+ }
1671
+ this.conn = null;
1672
+ this.isConnecting = false;
1673
+ }
1674
+ if (this.context.company !== void 0 || this.context.user !== void 0) {
1675
+ this.debug(`${methodName}: reconnecting with existing context`);
1676
+ try {
1677
+ this.isConnecting = true;
1678
+ this.conn = this.wsConnect();
1679
+ const socket = await this.conn;
1680
+ this.isConnecting = false;
1681
+ await this.wsSendMessage(socket, this.context, true);
1682
+ this.debug(`${methodName}: reconnection successful`);
1683
+ } catch (error) {
1684
+ this.isConnecting = false;
1685
+ this.debug(`${methodName}: reconnection failed:`, error);
1686
+ }
1687
+ } else {
1688
+ this.debug(`${methodName}: no context set, skipping reconnection`);
1689
+ }
1690
+ return Promise.resolve();
1691
+ };
1568
1692
  /**
1569
1693
  * If using websocket mode, close the connection when done.
1570
1694
  * In offline mode, this is a no-op.
@@ -1589,7 +1713,7 @@ var Schematic = class {
1589
1713
  }
1590
1714
  socket.close();
1591
1715
  } catch (error) {
1592
- console.error("Error during cleanup:", error);
1716
+ console.warn("Error during cleanup:", error);
1593
1717
  } finally {
1594
1718
  this.conn = null;
1595
1719
  this.currentWebSocket = null;
@@ -1732,14 +1856,49 @@ var Schematic = class {
1732
1856
  }
1733
1857
  }, delay);
1734
1858
  };
1735
- // Open a websocket connection
1736
- wsConnect = () => {
1859
+ // Open a websocket connection with retry logic for timeouts
1860
+ wsConnect = async () => {
1737
1861
  if (this.isOffline()) {
1738
1862
  this.debug("wsConnect: skipped (offline mode)");
1739
- return Promise.reject(
1740
- new Error("WebSocket connection skipped in offline mode")
1741
- );
1863
+ throw new Error("WebSocket connection skipped in offline mode");
1742
1864
  }
1865
+ let lastError = null;
1866
+ const maxAttempts = this.webSocketMaxConnectionAttempts;
1867
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1868
+ try {
1869
+ const socket = await this.wsConnectOnce();
1870
+ this.wsReconnectAttempts = 0;
1871
+ return socket;
1872
+ } catch (error) {
1873
+ lastError = error instanceof Error ? error : new Error(String(error));
1874
+ const isTimeout = lastError.message === "WebSocket connection timeout";
1875
+ if (!isTimeout) {
1876
+ this.debug(
1877
+ `WebSocket connection failed with non-timeout error, not retrying:`,
1878
+ lastError.message
1879
+ );
1880
+ throw lastError;
1881
+ }
1882
+ if (attempt < maxAttempts - 1) {
1883
+ const baseDelay = this.webSocketInitialRetryDelay * Math.pow(2, attempt);
1884
+ const cappedDelay = Math.min(baseDelay, this.webSocketMaxRetryDelay);
1885
+ const jitter = cappedDelay * 0.2 * Math.random();
1886
+ const delay = cappedDelay + jitter;
1887
+ this.debug(
1888
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay.toFixed(0)}ms`
1889
+ );
1890
+ await new Promise((resolve) => setTimeout(resolve, delay));
1891
+ } else {
1892
+ this.debug(
1893
+ `WebSocket connection timeout (attempt ${attempt + 1}/${maxAttempts}), no more retries`
1894
+ );
1895
+ }
1896
+ }
1897
+ }
1898
+ throw lastError ?? new Error("WebSocket connection failed");
1899
+ };
1900
+ // Single attempt to open a websocket connection (no retry logic)
1901
+ wsConnectOnce = () => {
1743
1902
  return new Promise((resolve, reject) => {
1744
1903
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1745
1904
  this.debug(`connecting to WebSocket:`, wsUrl);
@@ -1750,6 +1909,7 @@ var Schematic = class {
1750
1909
  let isResolved = false;
1751
1910
  timeoutId = setTimeout(() => {
1752
1911
  if (!isResolved) {
1912
+ isResolved = true;
1753
1913
  this.debug(
1754
1914
  `WebSocket connection timeout after ${this.webSocketConnectionTimeout}ms`
1755
1915
  );
@@ -1758,16 +1918,17 @@ var Schematic = class {
1758
1918
  }
1759
1919
  }, this.webSocketConnectionTimeout);
1760
1920
  webSocket.onopen = () => {
1921
+ if (isResolved) return;
1761
1922
  isResolved = true;
1762
1923
  if (timeoutId !== null) {
1763
1924
  clearTimeout(timeoutId);
1764
1925
  }
1765
- this.wsReconnectAttempts = 0;
1766
1926
  this.wsIntentionalDisconnect = false;
1767
1927
  this.debug(`WebSocket connection ${connectionId} opened successfully`);
1768
1928
  resolve(webSocket);
1769
1929
  };
1770
1930
  webSocket.onerror = (error) => {
1931
+ if (isResolved) return;
1771
1932
  isResolved = true;
1772
1933
  if (timeoutId !== null) {
1773
1934
  clearTimeout(timeoutId);
@@ -1776,7 +1937,6 @@ var Schematic = class {
1776
1937
  reject(error);
1777
1938
  };
1778
1939
  webSocket.onclose = () => {
1779
- isResolved = true;
1780
1940
  if (timeoutId !== null) {
1781
1941
  clearTimeout(timeoutId);
1782
1942
  }
@@ -1786,7 +1946,7 @@ var Schematic = class {
1786
1946
  this.currentWebSocket = null;
1787
1947
  this.isConnecting = false;
1788
1948
  }
1789
- if (!this.wsIntentionalDisconnect && this.webSocketReconnect) {
1949
+ if (!isResolved && !this.wsIntentionalDisconnect && this.webSocketReconnect) {
1790
1950
  this.attemptReconnect();
1791
1951
  }
1792
1952
  };
@@ -1968,7 +2128,7 @@ var notifyFlagValueListener = (listener, value) => {
1968
2128
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1969
2129
 
1970
2130
  // src/version.ts
1971
- var version2 = "1.2.10";
2131
+ var version2 = "1.2.13";
1972
2132
 
1973
2133
  // src/context/schematic.tsx
1974
2134
  import { jsx } from "react/jsx-runtime";
@@ -1983,7 +2143,7 @@ var SchematicProvider = ({
1983
2143
  }) => {
1984
2144
  const initialOptsRef = useRef({
1985
2145
  publishableKey,
1986
- useWebSocket: clientOpts.useWebSocket ?? true,
2146
+ useWebSocket: true,
1987
2147
  additionalHeaders: {
1988
2148
  "X-Schematic-Client-Version": `schematic-react@${version2}`
1989
2149
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schematichq/schematic-react",
3
- "version": "1.2.10",
3
+ "version": "1.2.13",
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.10"
34
+ "@schematichq/schematic-js": "^1.2.13"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^9.39.1",