react-realtime-hooks 1.0.2 → 1.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 +6 -1
- package/dist/index.cjs +112 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +112 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -350,7 +350,7 @@ export function NetworkIndicator() {
|
|
|
350
350
|
| `parseMessage` | `(event) => TIncoming` | raw `event.data` | Incoming parser |
|
|
351
351
|
| `serializeMessage` | `(message) => ...` | JSON/string passthrough | Outgoing serializer |
|
|
352
352
|
| `reconnect` | `false \| UseReconnectOptions` | enabled | Reconnect configuration |
|
|
353
|
-
| `heartbeat` | `false \|
|
|
353
|
+
| `heartbeat` | `false \| UseWebSocketHeartbeatOptions` | disabled unless configured | Heartbeat configuration |
|
|
354
354
|
| `shouldReconnect` | `(event) => boolean` | `true` | Reconnect gate on close |
|
|
355
355
|
| `onOpen` | `(event, socket) => void` | `undefined` | Open callback |
|
|
356
356
|
| `onMessage` | `(message, event) => void` | `undefined` | Message callback |
|
|
@@ -375,6 +375,10 @@ export function NetworkIndicator() {
|
|
|
375
375
|
| `reconnect` | `() => void` | Manual reconnect |
|
|
376
376
|
| `send` | `(message) => boolean` | Sends an outgoing payload |
|
|
377
377
|
|
|
378
|
+
When you configure `useWebSocket` heartbeat, you can also set `timeoutAction` and
|
|
379
|
+
`errorAction` to `"none"`, `"close"`, or `"reconnect"`. The default is
|
|
380
|
+
`"reconnect"` when reconnect is enabled and `"close"` otherwise.
|
|
381
|
+
|
|
378
382
|
</details>
|
|
379
383
|
|
|
380
384
|
<details>
|
|
@@ -464,6 +468,7 @@ export function NetworkIndicator() {
|
|
|
464
468
|
| `startOnMount` | `boolean` | `true` | Starts immediately |
|
|
465
469
|
| `onBeat` | `() => void` | `undefined` | Called on every beat |
|
|
466
470
|
| `onTimeout` | `() => void` | `undefined` | Called on timeout |
|
|
471
|
+
| `onError` | `(error) => void` | `undefined` | Called when `beat()` throws or rejects |
|
|
467
472
|
|
|
468
473
|
### Result
|
|
469
474
|
|
package/dist/index.cjs
CHANGED
|
@@ -404,6 +404,7 @@ var useHeartbeat = (options) => {
|
|
|
404
404
|
const startOnMount = options.startOnMount ?? true;
|
|
405
405
|
const intervalRef = react.useRef(createManagedInterval());
|
|
406
406
|
const timeoutRef = react.useRef(createManagedTimeout());
|
|
407
|
+
const generationRef = react.useRef(0);
|
|
407
408
|
const [state, setState] = react.useState(
|
|
408
409
|
() => createInitialState2(enabled && startOnMount)
|
|
409
410
|
);
|
|
@@ -430,8 +431,7 @@ var useHeartbeat = (options) => {
|
|
|
430
431
|
handleTimeout();
|
|
431
432
|
}, options.timeoutMs);
|
|
432
433
|
});
|
|
433
|
-
const
|
|
434
|
-
const performedAt = Date.now();
|
|
434
|
+
const handleBeatSuccess = react.useEffectEvent((performedAt) => {
|
|
435
435
|
commitState((current) => ({
|
|
436
436
|
...current,
|
|
437
437
|
hasTimedOut: false,
|
|
@@ -439,7 +439,35 @@ var useHeartbeat = (options) => {
|
|
|
439
439
|
}));
|
|
440
440
|
scheduleTimeout();
|
|
441
441
|
options.onBeat?.();
|
|
442
|
-
|
|
442
|
+
});
|
|
443
|
+
const handleBeatError = react.useEffectEvent((error) => {
|
|
444
|
+
timeoutRef.current.cancel();
|
|
445
|
+
options.onError?.(error);
|
|
446
|
+
});
|
|
447
|
+
const runBeat = react.useEffectEvent(() => {
|
|
448
|
+
const generation = generationRef.current;
|
|
449
|
+
const completeBeat = (result) => {
|
|
450
|
+
if (generation !== generationRef.current || result === false) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
handleBeatSuccess(Date.now());
|
|
454
|
+
};
|
|
455
|
+
const failBeat = (error) => {
|
|
456
|
+
if (generation !== generationRef.current) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
handleBeatError(error);
|
|
460
|
+
};
|
|
461
|
+
try {
|
|
462
|
+
const result = options.beat?.();
|
|
463
|
+
if (result !== null && typeof result === "object" && "then" in result) {
|
|
464
|
+
void Promise.resolve(result).then(completeBeat, failBeat);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
completeBeat(result);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
failBeat(error);
|
|
470
|
+
}
|
|
443
471
|
});
|
|
444
472
|
const start = () => {
|
|
445
473
|
if (!enabled) {
|
|
@@ -457,6 +485,7 @@ var useHeartbeat = (options) => {
|
|
|
457
485
|
}));
|
|
458
486
|
};
|
|
459
487
|
const stop = () => {
|
|
488
|
+
generationRef.current += 1;
|
|
460
489
|
intervalRef.current.cancel();
|
|
461
490
|
timeoutRef.current.cancel();
|
|
462
491
|
commitState((current) => ({
|
|
@@ -502,6 +531,7 @@ var useHeartbeat = (options) => {
|
|
|
502
531
|
return void 0;
|
|
503
532
|
}, [enabled, options.intervalMs, startOnMount]);
|
|
504
533
|
react.useEffect(() => () => {
|
|
534
|
+
generationRef.current += 1;
|
|
505
535
|
intervalRef.current.cancel();
|
|
506
536
|
timeoutRef.current.cancel();
|
|
507
537
|
}, []);
|
|
@@ -607,6 +637,7 @@ var toProtocolsDependency = (protocols) => {
|
|
|
607
637
|
return Array.isArray(protocols) ? protocols.join("|") : protocols;
|
|
608
638
|
};
|
|
609
639
|
var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
|
|
640
|
+
var isSocketActive = (socket) => socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING;
|
|
610
641
|
var useWebSocket = (options) => {
|
|
611
642
|
const connect = options.connect ?? true;
|
|
612
643
|
const supported = isWebSocketSupported();
|
|
@@ -618,6 +649,7 @@ var useWebSocket = (options) => {
|
|
|
618
649
|
const manualOpenRef = react.useRef(false);
|
|
619
650
|
const skipCloseReconnectRef = react.useRef(false);
|
|
620
651
|
const suppressReconnectRef = react.useRef(false);
|
|
652
|
+
const pendingCloseActionRef = react.useRef(null);
|
|
621
653
|
const terminalErrorRef = react.useRef(null);
|
|
622
654
|
const [openNonce, setOpenNonce] = react.useState(0);
|
|
623
655
|
const [state, setState] = react.useState(
|
|
@@ -636,6 +668,7 @@ var useWebSocket = (options) => {
|
|
|
636
668
|
const heartbeatConfig = toHeartbeatConfig(
|
|
637
669
|
options.heartbeat
|
|
638
670
|
);
|
|
671
|
+
const defaultHeartbeatAction = options.reconnect === false ? "close" : "reconnect";
|
|
639
672
|
const heartbeatHookOptions = heartbeatConfig === null ? {
|
|
640
673
|
enabled: false,
|
|
641
674
|
intervalMs: 1e3,
|
|
@@ -671,6 +704,27 @@ var useWebSocket = (options) => {
|
|
|
671
704
|
if (heartbeatConfig !== null && heartbeatConfig.onTimeout !== void 0) {
|
|
672
705
|
heartbeatHookOptions.onTimeout = heartbeatConfig.onTimeout;
|
|
673
706
|
}
|
|
707
|
+
if (heartbeatConfig !== null) {
|
|
708
|
+
const onTimeout = heartbeatHookOptions.onTimeout;
|
|
709
|
+
heartbeatHookOptions.onTimeout = () => {
|
|
710
|
+
applyHeartbeatAction(
|
|
711
|
+
heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
|
|
712
|
+
new Event("heartbeat-timeout"),
|
|
713
|
+
"heartbeat-timeout"
|
|
714
|
+
);
|
|
715
|
+
onTimeout?.();
|
|
716
|
+
};
|
|
717
|
+
const onError = heartbeatConfig.onError;
|
|
718
|
+
heartbeatHookOptions.onError = (error) => {
|
|
719
|
+
const event = error instanceof Event ? error : new Event("heartbeat-error");
|
|
720
|
+
applyHeartbeatAction(
|
|
721
|
+
heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
|
|
722
|
+
event,
|
|
723
|
+
"error"
|
|
724
|
+
);
|
|
725
|
+
onError?.(error);
|
|
726
|
+
};
|
|
727
|
+
}
|
|
674
728
|
const heartbeat = useHeartbeat(
|
|
675
729
|
heartbeatHookOptions
|
|
676
730
|
);
|
|
@@ -686,10 +740,46 @@ var useWebSocket = (options) => {
|
|
|
686
740
|
}
|
|
687
741
|
socketRef.current = null;
|
|
688
742
|
socketKeyRef.current = null;
|
|
689
|
-
if (socket2
|
|
743
|
+
if (isSocketActive(socket2)) {
|
|
690
744
|
socket2.close(code, reason);
|
|
691
745
|
}
|
|
692
746
|
});
|
|
747
|
+
const applyHeartbeatAction = react.useEffectEvent(
|
|
748
|
+
(action, error, reconnectTrigger) => {
|
|
749
|
+
heartbeat.stop();
|
|
750
|
+
if (action === "none") {
|
|
751
|
+
commitState((current) => ({
|
|
752
|
+
...current,
|
|
753
|
+
lastChangedAt: Date.now(),
|
|
754
|
+
lastError: error
|
|
755
|
+
}));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const shouldReconnect = action === "reconnect" && reconnectEnabled && (options.shouldReconnect?.(error) ?? true);
|
|
759
|
+
manualOpenRef.current = false;
|
|
760
|
+
terminalErrorRef.current = shouldReconnect ? null : error;
|
|
761
|
+
const socket2 = socketRef.current;
|
|
762
|
+
if (socket2 === null || !isSocketActive(socket2)) {
|
|
763
|
+
commitState((current) => ({
|
|
764
|
+
...current,
|
|
765
|
+
lastChangedAt: Date.now(),
|
|
766
|
+
lastError: error,
|
|
767
|
+
status: shouldReconnect ? "reconnecting" : "error"
|
|
768
|
+
}));
|
|
769
|
+
if (shouldReconnect) {
|
|
770
|
+
reconnect.schedule(reconnectTrigger);
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
pendingCloseActionRef.current = {
|
|
775
|
+
error,
|
|
776
|
+
reconnectTrigger: shouldReconnect ? reconnectTrigger : null
|
|
777
|
+
};
|
|
778
|
+
skipCloseReconnectRef.current = true;
|
|
779
|
+
suppressReconnectRef.current = true;
|
|
780
|
+
closeSocket();
|
|
781
|
+
}
|
|
782
|
+
);
|
|
693
783
|
const parseMessage = react.useEffectEvent((event) => {
|
|
694
784
|
const parser = options.parseMessage ?? defaultParseMessage;
|
|
695
785
|
return parser(event);
|
|
@@ -757,9 +847,26 @@ var useWebSocket = (options) => {
|
|
|
757
847
|
socketKeyRef.current = null;
|
|
758
848
|
heartbeat.stop();
|
|
759
849
|
updateBufferedAmount();
|
|
850
|
+
const pendingCloseAction = pendingCloseActionRef.current;
|
|
851
|
+
pendingCloseActionRef.current = null;
|
|
760
852
|
const terminalError = terminalErrorRef.current;
|
|
761
853
|
const skipCloseReconnect = skipCloseReconnectRef.current;
|
|
762
854
|
skipCloseReconnectRef.current = false;
|
|
855
|
+
if (pendingCloseAction !== null) {
|
|
856
|
+
suppressReconnectRef.current = false;
|
|
857
|
+
commitState((current) => ({
|
|
858
|
+
...current,
|
|
859
|
+
lastChangedAt: Date.now(),
|
|
860
|
+
lastCloseEvent: event,
|
|
861
|
+
lastError: pendingCloseAction.error ?? current.lastError,
|
|
862
|
+
status: pendingCloseAction.reconnectTrigger === null ? "error" : "reconnecting"
|
|
863
|
+
}));
|
|
864
|
+
options.onClose?.(event);
|
|
865
|
+
if (pendingCloseAction.reconnectTrigger !== null) {
|
|
866
|
+
reconnect.schedule(pendingCloseAction.reconnectTrigger);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
763
870
|
if (terminalError !== null) {
|
|
764
871
|
suppressReconnectRef.current = false;
|
|
765
872
|
commitState((current) => ({
|
|
@@ -918,7 +1025,7 @@ var useWebSocket = (options) => {
|
|
|
918
1025
|
if (socket2 === null) {
|
|
919
1026
|
return;
|
|
920
1027
|
}
|
|
921
|
-
if (socket2
|
|
1028
|
+
if (isSocketActive(socket2)) {
|
|
922
1029
|
socket2.close();
|
|
923
1030
|
}
|
|
924
1031
|
}, []);
|