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 +17 -1
- package/dist/index.cjs +148 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +148 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
1227
|
+
bufferedAmount: 0,
|
|
1236
1228
|
lastChangedAt: Date.now(),
|
|
1237
1229
|
status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
|
|
1238
1230
|
}));
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
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
|
-
|
|
1590
|
-
|
|
1648
|
+
let cancelled = false;
|
|
1649
|
+
let detachListeners = null;
|
|
1650
|
+
queueMicrotask(() => {
|
|
1651
|
+
if (cancelled) {
|
|
1591
1652
|
return;
|
|
1592
1653
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
1666
|
+
handleOpen(event, source);
|
|
1613
1667
|
};
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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.
|
|
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,
|