react-realtime-hooks 1.3.3 → 1.4.0

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
@@ -257,6 +257,21 @@ export function ChatSocket() {
257
257
  }
258
258
  ```
259
259
 
260
+ The native `WebSocket` does not emit any event when buffered bytes drain to the network, so by default `bufferedAmount` only refreshes on `send()`, on incoming messages, and on the `open` transition. Pass `bufferedAmountPolling` to surface the live value for backpressure indicators:
261
+
262
+ ```tsx
263
+ const socket = useWebSocket<IncomingMessage, OutgoingMessage>({
264
+ url: "ws://localhost:8080",
265
+ // "raf" reads `bufferedAmount` on every animation frame while open;
266
+ // use `true` for a 100ms interval, or `{ intervalMs: 50 }` for custom.
267
+ bufferedAmountPolling: "raf",
268
+ });
269
+
270
+ // `socket.bufferedAmount` now ticks down as the OS flushes the buffer,
271
+ // even between `send` calls. Identical readings do not trigger React
272
+ // re-renders.
273
+ ```
274
+
260
275
  ### `useEventSource`
261
276
 
262
277
  ```tsx
@@ -400,6 +415,7 @@ export function GatedNotifications() {
400
415
  | `protocols` | `string \| string[]` | `undefined` | WebSocket subprotocols |
401
416
  | `connect` | `boolean` | `true` | Auto-connect on mount |
402
417
  | `binaryType` | `BinaryType` | `"blob"` | Socket binary mode |
418
+ | `bufferedAmountPolling` | `false \| true \| "raf" \| { intervalMs: number }` | `false` | Live polling of `WebSocket.bufferedAmount` for backpressure UIs |
403
419
  | `parseMessage` | `(event) => TIncoming` | raw `event.data` | Incoming parser |
404
420
  | `serializeMessage` | `(message) => ...` | JSON/string passthrough | Outgoing serializer |
405
421
  | `reconnect` | `false \| UseReconnectOptions` | enabled | Reconnect configuration |
@@ -420,7 +436,7 @@ export function GatedNotifications() {
420
436
  | `lastMessageEvent` | `MessageEvent \| null` | Last raw message event |
421
437
  | `lastCloseEvent` | `CloseEvent \| null` | Last close event |
422
438
  | `lastError` | `Event \| null` | Last transport error, or `RealtimeErrorEvent` for parse/heartbeat failures |
423
- | `bufferedAmount` | `number` | Current socket buffer size |
439
+ | `bufferedAmount` | `number` | Current socket buffer size (refresh cadence is controlled by `bufferedAmountPolling`) |
424
440
  | `reconnectState` | reconnect snapshot or `null` | Current reconnect data |
425
441
  | `heartbeatState` | heartbeat snapshot or `null` | Current heartbeat data |
426
442
  | `open` | `() => void` | Manual connect |
package/dist/index.cjs CHANGED
@@ -1222,47 +1222,62 @@ var useWebSocket = (options) => {
1222
1222
  if (socketRef.current !== null) {
1223
1223
  return;
1224
1224
  }
1225
- const socket2 = new WebSocket(resolvedUrl, protocols);
1226
- const socketEpoch = nextSocketEpochRef.current + 1;
1227
- socketRef.current = socket2;
1228
- socketKeyRef.current = nextSocketKey;
1229
- activeSocketEpochRef.current = socketEpoch;
1230
- closingSocketEpochRef.current = null;
1231
- nextSocketEpochRef.current = socketEpoch;
1232
- socket2.binaryType = options.binaryType ?? "blob";
1233
1225
  commitState((current) => ({
1234
1226
  ...current,
1235
- bufferedAmount: socket2.bufferedAmount,
1227
+ bufferedAmount: 0,
1236
1228
  lastChangedAt: Date.now(),
1237
1229
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1238
1230
  }));
1239
- const handleSocketOpen = (event) => {
1240
- if (!isActiveSocketEvent(socketEpoch)) {
1241
- return;
1242
- }
1243
- handleOpen(event, socket2);
1244
- };
1245
- const handleSocketMessage = (event) => {
1246
- if (!isActiveSocketEvent(socketEpoch)) {
1231
+ let cancelled = false;
1232
+ let detachListeners = null;
1233
+ queueMicrotask(() => {
1234
+ if (cancelled) {
1247
1235
  return;
1248
1236
  }
1249
- handleMessage(event);
1250
- };
1251
- const handleSocketError = (event) => {
1252
- handleError(event, socketEpoch);
1253
- };
1254
- const handleSocketClose = (event) => {
1255
- handleClose(event, socketEpoch);
1256
- };
1257
- socket2.addEventListener("open", handleSocketOpen);
1258
- socket2.addEventListener("message", handleSocketMessage);
1259
- socket2.addEventListener("error", handleSocketError);
1260
- socket2.addEventListener("close", handleSocketClose);
1237
+ const socket2 = new WebSocket(resolvedUrl, protocols);
1238
+ const socketEpoch = nextSocketEpochRef.current + 1;
1239
+ socketRef.current = socket2;
1240
+ socketKeyRef.current = nextSocketKey;
1241
+ activeSocketEpochRef.current = socketEpoch;
1242
+ closingSocketEpochRef.current = null;
1243
+ nextSocketEpochRef.current = socketEpoch;
1244
+ socket2.binaryType = options.binaryType ?? "blob";
1245
+ commitState((current) => ({
1246
+ ...current,
1247
+ bufferedAmount: socket2.bufferedAmount
1248
+ }));
1249
+ const handleSocketOpen = (event) => {
1250
+ if (!isActiveSocketEvent(socketEpoch)) {
1251
+ return;
1252
+ }
1253
+ handleOpen(event, socket2);
1254
+ };
1255
+ const handleSocketMessage = (event) => {
1256
+ if (!isActiveSocketEvent(socketEpoch)) {
1257
+ return;
1258
+ }
1259
+ handleMessage(event);
1260
+ };
1261
+ const handleSocketError = (event) => {
1262
+ handleError(event, socketEpoch);
1263
+ };
1264
+ const handleSocketClose = (event) => {
1265
+ handleClose(event, socketEpoch);
1266
+ };
1267
+ socket2.addEventListener("open", handleSocketOpen);
1268
+ socket2.addEventListener("message", handleSocketMessage);
1269
+ socket2.addEventListener("error", handleSocketError);
1270
+ socket2.addEventListener("close", handleSocketClose);
1271
+ detachListeners = () => {
1272
+ socket2.removeEventListener("open", handleSocketOpen);
1273
+ socket2.removeEventListener("message", handleSocketMessage);
1274
+ socket2.removeEventListener("error", handleSocketError);
1275
+ socket2.removeEventListener("close", handleSocketClose);
1276
+ };
1277
+ });
1261
1278
  return () => {
1262
- socket2.removeEventListener("open", handleSocketOpen);
1263
- socket2.removeEventListener("message", handleSocketMessage);
1264
- socket2.removeEventListener("error", handleSocketError);
1265
- socket2.removeEventListener("close", handleSocketClose);
1279
+ cancelled = true;
1280
+ detachListeners?.();
1266
1281
  };
1267
1282
  }, [
1268
1283
  closeSocket,
@@ -1302,6 +1317,58 @@ var useWebSocket = (options) => {
1302
1317
  stopHeartbeat();
1303
1318
  }
1304
1319
  }, [state.status, stopHeartbeat]);
1320
+ const rawBufferedAmountPolling = options.bufferedAmountPolling;
1321
+ const bufferedAmountPollingMode = react.useMemo(() => {
1322
+ if (rawBufferedAmountPolling === void 0 || rawBufferedAmountPolling === false) {
1323
+ return null;
1324
+ }
1325
+ if (rawBufferedAmountPolling === true) {
1326
+ return 100;
1327
+ }
1328
+ if (rawBufferedAmountPolling === "raf") {
1329
+ return "raf";
1330
+ }
1331
+ const intervalMs = rawBufferedAmountPolling.intervalMs;
1332
+ return Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : null;
1333
+ }, [rawBufferedAmountPolling]);
1334
+ const pollBufferedAmount = useStableCallback(() => {
1335
+ const socket2 = socketRef.current;
1336
+ if (socket2 === null) {
1337
+ return;
1338
+ }
1339
+ const next = socket2.bufferedAmount;
1340
+ if (stateRef.current.bufferedAmount === next) {
1341
+ return;
1342
+ }
1343
+ commitState((current) => ({
1344
+ ...current,
1345
+ bufferedAmount: next
1346
+ }));
1347
+ });
1348
+ react.useEffect(() => {
1349
+ if (bufferedAmountPollingMode === null) {
1350
+ return;
1351
+ }
1352
+ if (state.status !== "open") {
1353
+ return;
1354
+ }
1355
+ if (bufferedAmountPollingMode === "raf") {
1356
+ if (typeof requestAnimationFrame !== "function") {
1357
+ return;
1358
+ }
1359
+ let frame = requestAnimationFrame(function loop() {
1360
+ pollBufferedAmount();
1361
+ frame = requestAnimationFrame(loop);
1362
+ });
1363
+ return () => {
1364
+ cancelAnimationFrame(frame);
1365
+ };
1366
+ }
1367
+ const intervalId = setInterval(pollBufferedAmount, bufferedAmountPollingMode);
1368
+ return () => {
1369
+ clearInterval(intervalId);
1370
+ };
1371
+ }, [bufferedAmountPollingMode, state.status, pollBufferedAmount]);
1305
1372
  const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1306
1373
  const snapshot = createConnectionStateSnapshot(status, {
1307
1374
  isSupported: supported,
@@ -1573,55 +1640,66 @@ var useEventSource = (options) => {
1573
1640
  if (eventSourceRef.current !== null) {
1574
1641
  return;
1575
1642
  }
1576
- const source = new EventSource(resolvedUrl, {
1577
- withCredentials: options.withCredentials ?? false
1578
- });
1579
- const sourceEpoch = nextEventSourceEpochRef.current + 1;
1580
- eventSourceRef.current = source;
1581
- eventSourceKeyRef.current = nextEventSourceKey;
1582
- activeEventSourceEpochRef.current = sourceEpoch;
1583
- nextEventSourceEpochRef.current = sourceEpoch;
1584
1643
  commitState((current) => ({
1585
1644
  ...current,
1586
1645
  lastChangedAt: Date.now(),
1587
1646
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1588
1647
  }));
1589
- const handleSourceOpen = (event) => {
1590
- if (!isActiveEventSourceEvent(sourceEpoch)) {
1648
+ let cancelled = false;
1649
+ let detachListeners = null;
1650
+ queueMicrotask(() => {
1651
+ if (cancelled) {
1591
1652
  return;
1592
1653
  }
1593
- handleOpen(event, source);
1594
- };
1595
- const handleSourceMessage = (event) => {
1596
- if (!isActiveEventSourceEvent(sourceEpoch)) {
1597
- return;
1598
- }
1599
- commitParsedMessage("message", event, false);
1600
- };
1601
- const namedEventHandlers = /* @__PURE__ */ new Map();
1602
- const handleSourceError = (event) => {
1603
- handleError(event, source, sourceEpoch);
1604
- };
1605
- source.addEventListener("open", handleSourceOpen);
1606
- source.addEventListener("message", handleSourceMessage);
1607
- for (const eventName of namedEvents) {
1608
- const handler = (event) => {
1654
+ const source = new EventSource(resolvedUrl, {
1655
+ withCredentials: options.withCredentials ?? false
1656
+ });
1657
+ const sourceEpoch = nextEventSourceEpochRef.current + 1;
1658
+ eventSourceRef.current = source;
1659
+ eventSourceKeyRef.current = nextEventSourceKey;
1660
+ activeEventSourceEpochRef.current = sourceEpoch;
1661
+ nextEventSourceEpochRef.current = sourceEpoch;
1662
+ const handleSourceOpen = (event) => {
1609
1663
  if (!isActiveEventSourceEvent(sourceEpoch)) {
1610
1664
  return;
1611
1665
  }
1612
- commitParsedMessage(eventName, event, true);
1666
+ handleOpen(event, source);
1613
1667
  };
1614
- namedEventHandlers.set(eventName, handler);
1615
- source.addEventListener(eventName, handler);
1616
- }
1617
- source.addEventListener("error", handleSourceError);
1618
- return () => {
1619
- source.removeEventListener("open", handleSourceOpen);
1620
- source.removeEventListener("message", handleSourceMessage);
1621
- for (const [eventName, handler] of namedEventHandlers) {
1622
- source.removeEventListener(eventName, handler);
1668
+ const handleSourceMessage = (event) => {
1669
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1670
+ return;
1671
+ }
1672
+ commitParsedMessage("message", event, false);
1673
+ };
1674
+ const namedEventHandlers = /* @__PURE__ */ new Map();
1675
+ const handleSourceError = (event) => {
1676
+ handleError(event, source, sourceEpoch);
1677
+ };
1678
+ source.addEventListener("open", handleSourceOpen);
1679
+ source.addEventListener("message", handleSourceMessage);
1680
+ for (const eventName of namedEvents) {
1681
+ const handler = (event) => {
1682
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1683
+ return;
1684
+ }
1685
+ commitParsedMessage(eventName, event, true);
1686
+ };
1687
+ namedEventHandlers.set(eventName, handler);
1688
+ source.addEventListener(eventName, handler);
1623
1689
  }
1624
- source.removeEventListener("error", handleSourceError);
1690
+ source.addEventListener("error", handleSourceError);
1691
+ detachListeners = () => {
1692
+ source.removeEventListener("open", handleSourceOpen);
1693
+ source.removeEventListener("message", handleSourceMessage);
1694
+ for (const [eventName, handler] of namedEventHandlers) {
1695
+ source.removeEventListener(eventName, handler);
1696
+ }
1697
+ source.removeEventListener("error", handleSourceError);
1698
+ };
1699
+ });
1700
+ return () => {
1701
+ cancelled = true;
1702
+ detachListeners?.();
1625
1703
  };
1626
1704
  }, [
1627
1705
  closeEventSource,