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/dist/index.d.cts CHANGED
@@ -168,11 +168,39 @@ interface UseWebSocketHeartbeatOptions<TOutgoing = unknown, TIncoming = TOutgoin
168
168
  timeoutAction?: WebSocketHeartbeatAction;
169
169
  errorAction?: WebSocketHeartbeatAction;
170
170
  }
171
+ /**
172
+ * Controls how the hook surfaces the live `WebSocket.bufferedAmount`
173
+ * value through `state.bufferedAmount`.
174
+ *
175
+ * The native `WebSocket` does not emit any event when buffered bytes
176
+ * drain to the network, so by default `state.bufferedAmount` is only
177
+ * refreshed when the consumer calls `send(...)`, when a message
178
+ * arrives, or when the socket transitions to `open`. For UIs that need
179
+ * to display real-time backpressure (e.g. an outbound queue meter,
180
+ * a "flushing..." indicator, or a flow-control gauge), enable polling.
181
+ *
182
+ * - `false` (default): no polling. `state.bufferedAmount` updates only
183
+ * on the natural lifecycle events listed above.
184
+ * - `true`: poll once every 100ms while the socket is `open`.
185
+ * - `"raf"`: poll on every animation frame while the socket is `open`.
186
+ * Recommended for visual gauges that are tied to a render loop.
187
+ * - `{ intervalMs: N }`: poll every `N` milliseconds. `N` must be a
188
+ * positive integer; values <= 0 disable polling.
189
+ *
190
+ * Polling is automatically suspended when the socket is not in the
191
+ * `open` state and resumed when it returns to `open`. The hook diffs
192
+ * the polled value against the last committed one, so identical
193
+ * readings do not trigger React re-renders.
194
+ */
195
+ type BufferedAmountPolling = false | true | "raf" | {
196
+ intervalMs: number;
197
+ };
171
198
  interface UseWebSocketOptions<TIncoming = unknown, TOutgoing = TIncoming> {
172
199
  url: UrlProvider;
173
200
  protocols?: string | string[];
174
201
  connect?: boolean;
175
202
  binaryType?: BinaryType;
203
+ bufferedAmountPolling?: BufferedAmountPolling;
176
204
  parseMessage?: MessageParser<TIncoming>;
177
205
  serializeMessage?: MessageSerializer<TOutgoing>;
178
206
  reconnect?: false | UseReconnectOptions;
@@ -274,4 +302,4 @@ declare class RealtimeErrorEvent extends Event {
274
302
  constructor(type: string, init: RealtimeErrorEventInit);
275
303
  }
276
304
 
277
- export { type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, RealtimeErrorEvent, type RealtimeErrorEventInit, type RealtimeErrorKind, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
305
+ export { type BufferedAmountPolling, type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, RealtimeErrorEvent, type RealtimeErrorEventInit, type RealtimeErrorKind, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
package/dist/index.d.ts CHANGED
@@ -168,11 +168,39 @@ interface UseWebSocketHeartbeatOptions<TOutgoing = unknown, TIncoming = TOutgoin
168
168
  timeoutAction?: WebSocketHeartbeatAction;
169
169
  errorAction?: WebSocketHeartbeatAction;
170
170
  }
171
+ /**
172
+ * Controls how the hook surfaces the live `WebSocket.bufferedAmount`
173
+ * value through `state.bufferedAmount`.
174
+ *
175
+ * The native `WebSocket` does not emit any event when buffered bytes
176
+ * drain to the network, so by default `state.bufferedAmount` is only
177
+ * refreshed when the consumer calls `send(...)`, when a message
178
+ * arrives, or when the socket transitions to `open`. For UIs that need
179
+ * to display real-time backpressure (e.g. an outbound queue meter,
180
+ * a "flushing..." indicator, or a flow-control gauge), enable polling.
181
+ *
182
+ * - `false` (default): no polling. `state.bufferedAmount` updates only
183
+ * on the natural lifecycle events listed above.
184
+ * - `true`: poll once every 100ms while the socket is `open`.
185
+ * - `"raf"`: poll on every animation frame while the socket is `open`.
186
+ * Recommended for visual gauges that are tied to a render loop.
187
+ * - `{ intervalMs: N }`: poll every `N` milliseconds. `N` must be a
188
+ * positive integer; values <= 0 disable polling.
189
+ *
190
+ * Polling is automatically suspended when the socket is not in the
191
+ * `open` state and resumed when it returns to `open`. The hook diffs
192
+ * the polled value against the last committed one, so identical
193
+ * readings do not trigger React re-renders.
194
+ */
195
+ type BufferedAmountPolling = false | true | "raf" | {
196
+ intervalMs: number;
197
+ };
171
198
  interface UseWebSocketOptions<TIncoming = unknown, TOutgoing = TIncoming> {
172
199
  url: UrlProvider;
173
200
  protocols?: string | string[];
174
201
  connect?: boolean;
175
202
  binaryType?: BinaryType;
203
+ bufferedAmountPolling?: BufferedAmountPolling;
176
204
  parseMessage?: MessageParser<TIncoming>;
177
205
  serializeMessage?: MessageSerializer<TOutgoing>;
178
206
  reconnect?: false | UseReconnectOptions;
@@ -274,4 +302,4 @@ declare class RealtimeErrorEvent extends Event {
274
302
  constructor(type: string, init: RealtimeErrorEventInit);
275
303
  }
276
304
 
277
- export { type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, RealtimeErrorEvent, type RealtimeErrorEventInit, type RealtimeErrorKind, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
305
+ export { type BufferedAmountPolling, type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, RealtimeErrorEvent, type RealtimeErrorEventInit, type RealtimeErrorKind, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
package/dist/index.js CHANGED
@@ -1220,47 +1220,62 @@ var useWebSocket = (options) => {
1220
1220
  if (socketRef.current !== null) {
1221
1221
  return;
1222
1222
  }
1223
- const socket2 = new WebSocket(resolvedUrl, protocols);
1224
- const socketEpoch = nextSocketEpochRef.current + 1;
1225
- socketRef.current = socket2;
1226
- socketKeyRef.current = nextSocketKey;
1227
- activeSocketEpochRef.current = socketEpoch;
1228
- closingSocketEpochRef.current = null;
1229
- nextSocketEpochRef.current = socketEpoch;
1230
- socket2.binaryType = options.binaryType ?? "blob";
1231
1223
  commitState((current) => ({
1232
1224
  ...current,
1233
- bufferedAmount: socket2.bufferedAmount,
1225
+ bufferedAmount: 0,
1234
1226
  lastChangedAt: Date.now(),
1235
1227
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1236
1228
  }));
1237
- const handleSocketOpen = (event) => {
1238
- if (!isActiveSocketEvent(socketEpoch)) {
1239
- return;
1240
- }
1241
- handleOpen(event, socket2);
1242
- };
1243
- const handleSocketMessage = (event) => {
1244
- if (!isActiveSocketEvent(socketEpoch)) {
1229
+ let cancelled = false;
1230
+ let detachListeners = null;
1231
+ queueMicrotask(() => {
1232
+ if (cancelled) {
1245
1233
  return;
1246
1234
  }
1247
- handleMessage(event);
1248
- };
1249
- const handleSocketError = (event) => {
1250
- handleError(event, socketEpoch);
1251
- };
1252
- const handleSocketClose = (event) => {
1253
- handleClose(event, socketEpoch);
1254
- };
1255
- socket2.addEventListener("open", handleSocketOpen);
1256
- socket2.addEventListener("message", handleSocketMessage);
1257
- socket2.addEventListener("error", handleSocketError);
1258
- socket2.addEventListener("close", handleSocketClose);
1235
+ const socket2 = new WebSocket(resolvedUrl, protocols);
1236
+ const socketEpoch = nextSocketEpochRef.current + 1;
1237
+ socketRef.current = socket2;
1238
+ socketKeyRef.current = nextSocketKey;
1239
+ activeSocketEpochRef.current = socketEpoch;
1240
+ closingSocketEpochRef.current = null;
1241
+ nextSocketEpochRef.current = socketEpoch;
1242
+ socket2.binaryType = options.binaryType ?? "blob";
1243
+ commitState((current) => ({
1244
+ ...current,
1245
+ bufferedAmount: socket2.bufferedAmount
1246
+ }));
1247
+ const handleSocketOpen = (event) => {
1248
+ if (!isActiveSocketEvent(socketEpoch)) {
1249
+ return;
1250
+ }
1251
+ handleOpen(event, socket2);
1252
+ };
1253
+ const handleSocketMessage = (event) => {
1254
+ if (!isActiveSocketEvent(socketEpoch)) {
1255
+ return;
1256
+ }
1257
+ handleMessage(event);
1258
+ };
1259
+ const handleSocketError = (event) => {
1260
+ handleError(event, socketEpoch);
1261
+ };
1262
+ const handleSocketClose = (event) => {
1263
+ handleClose(event, socketEpoch);
1264
+ };
1265
+ socket2.addEventListener("open", handleSocketOpen);
1266
+ socket2.addEventListener("message", handleSocketMessage);
1267
+ socket2.addEventListener("error", handleSocketError);
1268
+ socket2.addEventListener("close", handleSocketClose);
1269
+ detachListeners = () => {
1270
+ socket2.removeEventListener("open", handleSocketOpen);
1271
+ socket2.removeEventListener("message", handleSocketMessage);
1272
+ socket2.removeEventListener("error", handleSocketError);
1273
+ socket2.removeEventListener("close", handleSocketClose);
1274
+ };
1275
+ });
1259
1276
  return () => {
1260
- socket2.removeEventListener("open", handleSocketOpen);
1261
- socket2.removeEventListener("message", handleSocketMessage);
1262
- socket2.removeEventListener("error", handleSocketError);
1263
- socket2.removeEventListener("close", handleSocketClose);
1277
+ cancelled = true;
1278
+ detachListeners?.();
1264
1279
  };
1265
1280
  }, [
1266
1281
  closeSocket,
@@ -1300,6 +1315,58 @@ var useWebSocket = (options) => {
1300
1315
  stopHeartbeat();
1301
1316
  }
1302
1317
  }, [state.status, stopHeartbeat]);
1318
+ const rawBufferedAmountPolling = options.bufferedAmountPolling;
1319
+ const bufferedAmountPollingMode = useMemo(() => {
1320
+ if (rawBufferedAmountPolling === void 0 || rawBufferedAmountPolling === false) {
1321
+ return null;
1322
+ }
1323
+ if (rawBufferedAmountPolling === true) {
1324
+ return 100;
1325
+ }
1326
+ if (rawBufferedAmountPolling === "raf") {
1327
+ return "raf";
1328
+ }
1329
+ const intervalMs = rawBufferedAmountPolling.intervalMs;
1330
+ return Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : null;
1331
+ }, [rawBufferedAmountPolling]);
1332
+ const pollBufferedAmount = useStableCallback(() => {
1333
+ const socket2 = socketRef.current;
1334
+ if (socket2 === null) {
1335
+ return;
1336
+ }
1337
+ const next = socket2.bufferedAmount;
1338
+ if (stateRef.current.bufferedAmount === next) {
1339
+ return;
1340
+ }
1341
+ commitState((current) => ({
1342
+ ...current,
1343
+ bufferedAmount: next
1344
+ }));
1345
+ });
1346
+ useEffect(() => {
1347
+ if (bufferedAmountPollingMode === null) {
1348
+ return;
1349
+ }
1350
+ if (state.status !== "open") {
1351
+ return;
1352
+ }
1353
+ if (bufferedAmountPollingMode === "raf") {
1354
+ if (typeof requestAnimationFrame !== "function") {
1355
+ return;
1356
+ }
1357
+ let frame = requestAnimationFrame(function loop() {
1358
+ pollBufferedAmount();
1359
+ frame = requestAnimationFrame(loop);
1360
+ });
1361
+ return () => {
1362
+ cancelAnimationFrame(frame);
1363
+ };
1364
+ }
1365
+ const intervalId = setInterval(pollBufferedAmount, bufferedAmountPollingMode);
1366
+ return () => {
1367
+ clearInterval(intervalId);
1368
+ };
1369
+ }, [bufferedAmountPollingMode, state.status, pollBufferedAmount]);
1303
1370
  const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1304
1371
  const snapshot = createConnectionStateSnapshot(status, {
1305
1372
  isSupported: supported,
@@ -1571,55 +1638,66 @@ var useEventSource = (options) => {
1571
1638
  if (eventSourceRef.current !== null) {
1572
1639
  return;
1573
1640
  }
1574
- const source = new EventSource(resolvedUrl, {
1575
- withCredentials: options.withCredentials ?? false
1576
- });
1577
- const sourceEpoch = nextEventSourceEpochRef.current + 1;
1578
- eventSourceRef.current = source;
1579
- eventSourceKeyRef.current = nextEventSourceKey;
1580
- activeEventSourceEpochRef.current = sourceEpoch;
1581
- nextEventSourceEpochRef.current = sourceEpoch;
1582
1641
  commitState((current) => ({
1583
1642
  ...current,
1584
1643
  lastChangedAt: Date.now(),
1585
1644
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1586
1645
  }));
1587
- const handleSourceOpen = (event) => {
1588
- if (!isActiveEventSourceEvent(sourceEpoch)) {
1646
+ let cancelled = false;
1647
+ let detachListeners = null;
1648
+ queueMicrotask(() => {
1649
+ if (cancelled) {
1589
1650
  return;
1590
1651
  }
1591
- handleOpen(event, source);
1592
- };
1593
- const handleSourceMessage = (event) => {
1594
- if (!isActiveEventSourceEvent(sourceEpoch)) {
1595
- return;
1596
- }
1597
- commitParsedMessage("message", event, false);
1598
- };
1599
- const namedEventHandlers = /* @__PURE__ */ new Map();
1600
- const handleSourceError = (event) => {
1601
- handleError(event, source, sourceEpoch);
1602
- };
1603
- source.addEventListener("open", handleSourceOpen);
1604
- source.addEventListener("message", handleSourceMessage);
1605
- for (const eventName of namedEvents) {
1606
- const handler = (event) => {
1652
+ const source = new EventSource(resolvedUrl, {
1653
+ withCredentials: options.withCredentials ?? false
1654
+ });
1655
+ const sourceEpoch = nextEventSourceEpochRef.current + 1;
1656
+ eventSourceRef.current = source;
1657
+ eventSourceKeyRef.current = nextEventSourceKey;
1658
+ activeEventSourceEpochRef.current = sourceEpoch;
1659
+ nextEventSourceEpochRef.current = sourceEpoch;
1660
+ const handleSourceOpen = (event) => {
1607
1661
  if (!isActiveEventSourceEvent(sourceEpoch)) {
1608
1662
  return;
1609
1663
  }
1610
- commitParsedMessage(eventName, event, true);
1664
+ handleOpen(event, source);
1611
1665
  };
1612
- namedEventHandlers.set(eventName, handler);
1613
- source.addEventListener(eventName, handler);
1614
- }
1615
- source.addEventListener("error", handleSourceError);
1616
- return () => {
1617
- source.removeEventListener("open", handleSourceOpen);
1618
- source.removeEventListener("message", handleSourceMessage);
1619
- for (const [eventName, handler] of namedEventHandlers) {
1620
- source.removeEventListener(eventName, handler);
1666
+ const handleSourceMessage = (event) => {
1667
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1668
+ return;
1669
+ }
1670
+ commitParsedMessage("message", event, false);
1671
+ };
1672
+ const namedEventHandlers = /* @__PURE__ */ new Map();
1673
+ const handleSourceError = (event) => {
1674
+ handleError(event, source, sourceEpoch);
1675
+ };
1676
+ source.addEventListener("open", handleSourceOpen);
1677
+ source.addEventListener("message", handleSourceMessage);
1678
+ for (const eventName of namedEvents) {
1679
+ const handler = (event) => {
1680
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1681
+ return;
1682
+ }
1683
+ commitParsedMessage(eventName, event, true);
1684
+ };
1685
+ namedEventHandlers.set(eventName, handler);
1686
+ source.addEventListener(eventName, handler);
1621
1687
  }
1622
- source.removeEventListener("error", handleSourceError);
1688
+ source.addEventListener("error", handleSourceError);
1689
+ detachListeners = () => {
1690
+ source.removeEventListener("open", handleSourceOpen);
1691
+ source.removeEventListener("message", handleSourceMessage);
1692
+ for (const [eventName, handler] of namedEventHandlers) {
1693
+ source.removeEventListener(eventName, handler);
1694
+ }
1695
+ source.removeEventListener("error", handleSourceError);
1696
+ };
1697
+ });
1698
+ return () => {
1699
+ cancelled = true;
1700
+ detachListeners?.();
1623
1701
  };
1624
1702
  }, [
1625
1703
  closeEventSource,