react-realtime-hooks 2.0.1 → 2.0.3

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
@@ -8,6 +8,7 @@
8
8
  [![license](https://img.shields.io/npm/l/react-realtime-hooks)](https://github.com/volkov85/react-realtime-hooks/blob/main/LICENSE)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-typed-3178c6)](https://www.typescriptlang.org/)
10
10
  [![react](https://img.shields.io/badge/react-18%20%7C%2019-149eca)](https://www.npmjs.com/package/react)
11
+ [![React Doctor](https://www.react.doctor/share/badge?p=react-realtime-hooks&s=94&e=2&w=28&f=21)](https://www.react.doctor/share?p=react-realtime-hooks&s=94&e=2&w=28&f=21)
11
12
 
12
13
  Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness including page visibility and connection gating.
13
14
 
package/dist/index.cjs CHANGED
@@ -939,6 +939,7 @@ var useWebSocket = (options) => {
939
939
  [protocolsDependency]
940
940
  );
941
941
  const socketRef = react.useRef(null);
942
+ const detachSocketListenersRef = react.useRef(null);
942
943
  const socketKeyRef = react.useRef(null);
943
944
  const activeSocketEpochRef = react.useRef(null);
944
945
  const closingSocketEpochRef = react.useRef(null);
@@ -1051,6 +1052,10 @@ var useWebSocket = (options) => {
1051
1052
  socketKeyRef.current = null;
1052
1053
  activeSocketEpochRef.current = null;
1053
1054
  closingSocketEpochRef.current = config.trackClose ? socketEpoch : null;
1055
+ if (!config.trackClose) {
1056
+ detachSocketListenersRef.current?.();
1057
+ detachSocketListenersRef.current = null;
1058
+ }
1054
1059
  if (isSocketActive(socket2)) {
1055
1060
  socket2.close(config.code, config.reason);
1056
1061
  }
@@ -1162,6 +1167,8 @@ var useWebSocket = (options) => {
1162
1167
  if (!shouldHandleSocketClose(socketEpoch)) {
1163
1168
  return;
1164
1169
  }
1170
+ detachSocketListenersRef.current?.();
1171
+ detachSocketListenersRef.current = null;
1165
1172
  if (activeSocketEpochRef.current === socketEpoch) {
1166
1173
  socketRef.current = null;
1167
1174
  socketKeyRef.current = null;
@@ -1232,10 +1239,12 @@ var useWebSocket = (options) => {
1232
1239
  controller.noteUserCloseRequested();
1233
1240
  reconnect.cancel();
1234
1241
  heartbeat.stop();
1242
+ const socket2 = socketRef.current;
1243
+ const hasInFlightSocket = socket2 !== null && (socket2.readyState === WebSocket.CONNECTING || socket2.readyState === WebSocket.OPEN);
1235
1244
  commitState((current) => ({
1236
1245
  ...current,
1237
1246
  lastChangedAt: Date.now(),
1238
- status: "closing"
1247
+ status: hasInFlightSocket ? "closing" : "closed"
1239
1248
  }));
1240
1249
  closeSocket({
1241
1250
  code,
@@ -1299,12 +1308,21 @@ var useWebSocket = (options) => {
1299
1308
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1300
1309
  }));
1301
1310
  let cancelled = false;
1302
- let detachListeners = null;
1303
1311
  queueMicrotask(() => {
1304
1312
  if (cancelled) {
1305
1313
  return;
1306
1314
  }
1315
+ if (controller.hasManualCloseRequested()) {
1316
+ commitState((current) => ({
1317
+ ...current,
1318
+ lastChangedAt: Date.now(),
1319
+ status: "closed"
1320
+ }));
1321
+ return;
1322
+ }
1307
1323
  const socket2 = new WebSocket(resolvedUrl, protocols);
1324
+ detachSocketListenersRef.current?.();
1325
+ detachSocketListenersRef.current = null;
1308
1326
  const socketEpoch = nextSocketEpochRef.current + 1;
1309
1327
  socketRef.current = socket2;
1310
1328
  socketKeyRef.current = nextSocketKey;
@@ -1338,7 +1356,7 @@ var useWebSocket = (options) => {
1338
1356
  socket2.addEventListener("message", handleSocketMessage);
1339
1357
  socket2.addEventListener("error", handleSocketError);
1340
1358
  socket2.addEventListener("close", handleSocketClose);
1341
- detachListeners = () => {
1359
+ detachSocketListenersRef.current = () => {
1342
1360
  socket2.removeEventListener("open", handleSocketOpen);
1343
1361
  socket2.removeEventListener("message", handleSocketMessage);
1344
1362
  socket2.removeEventListener("error", handleSocketError);
@@ -1347,7 +1365,6 @@ var useWebSocket = (options) => {
1347
1365
  });
1348
1366
  return () => {
1349
1367
  cancelled = true;
1350
- detachListeners?.();
1351
1368
  };
1352
1369
  }, [
1353
1370
  closeSocket,
@@ -1368,6 +1385,8 @@ var useWebSocket = (options) => {
1368
1385
  supported
1369
1386
  ]);
1370
1387
  react.useEffect(() => () => {
1388
+ detachSocketListenersRef.current?.();
1389
+ detachSocketListenersRef.current = null;
1371
1390
  controller.noteEffectInitiatedClose();
1372
1391
  socketKeyRef.current = null;
1373
1392
  activeSocketEpochRef.current = null;
@@ -1512,6 +1531,7 @@ var useEventSource = (options) => {
1512
1531
  [eventsDependency]
1513
1532
  );
1514
1533
  const eventSourceRef = react.useRef(null);
1534
+ const detachEventSourceListenersRef = react.useRef(null);
1515
1535
  const eventSourceKeyRef = react.useRef(null);
1516
1536
  const activeEventSourceEpochRef = react.useRef(null);
1517
1537
  const nextEventSourceEpochRef = react.useRef(0);
@@ -1549,6 +1569,8 @@ var useEventSource = (options) => {
1549
1569
  eventSourceRef.current = null;
1550
1570
  eventSourceKeyRef.current = null;
1551
1571
  activeEventSourceEpochRef.current = null;
1572
+ detachEventSourceListenersRef.current?.();
1573
+ detachEventSourceListenersRef.current = null;
1552
1574
  source.close();
1553
1575
  });
1554
1576
  const isActiveEventSourceEvent = useStableCallback((sourceEpoch) => {
@@ -1719,11 +1741,18 @@ var useEventSource = (options) => {
1719
1741
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1720
1742
  }));
1721
1743
  let cancelled = false;
1722
- let detachListeners = null;
1723
1744
  queueMicrotask(() => {
1724
1745
  if (cancelled) {
1725
1746
  return;
1726
1747
  }
1748
+ if (manualCloseRef.current) {
1749
+ commitState((current) => ({
1750
+ ...current,
1751
+ lastChangedAt: Date.now(),
1752
+ status: "closed"
1753
+ }));
1754
+ return;
1755
+ }
1727
1756
  const source = new EventSource(resolvedUrl, {
1728
1757
  withCredentials: options.withCredentials ?? false
1729
1758
  });
@@ -1761,7 +1790,7 @@ var useEventSource = (options) => {
1761
1790
  source.addEventListener(eventName, handler);
1762
1791
  }
1763
1792
  source.addEventListener("error", handleSourceError);
1764
- detachListeners = () => {
1793
+ detachEventSourceListenersRef.current = () => {
1765
1794
  source.removeEventListener("open", handleSourceOpen);
1766
1795
  source.removeEventListener("message", handleSourceMessage);
1767
1796
  for (const [eventName, handler] of namedEventHandlers) {
@@ -1772,7 +1801,6 @@ var useEventSource = (options) => {
1772
1801
  });
1773
1802
  return () => {
1774
1803
  cancelled = true;
1775
- detachListeners?.();
1776
1804
  };
1777
1805
  }, [
1778
1806
  closeEventSource,
@@ -1791,6 +1819,8 @@ var useEventSource = (options) => {
1791
1819
  supported
1792
1820
  ]);
1793
1821
  react.useEffect(() => () => {
1822
+ detachEventSourceListenersRef.current?.();
1823
+ detachEventSourceListenersRef.current = null;
1794
1824
  suppressReconnectRef.current = true;
1795
1825
  eventSourceKeyRef.current = null;
1796
1826
  activeEventSourceEpochRef.current = null;