react-realtime-hooks 1.0.2 → 1.0.4

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.js CHANGED
@@ -402,6 +402,7 @@ var useHeartbeat = (options) => {
402
402
  const startOnMount = options.startOnMount ?? true;
403
403
  const intervalRef = useRef(createManagedInterval());
404
404
  const timeoutRef = useRef(createManagedTimeout());
405
+ const generationRef = useRef(0);
405
406
  const [state, setState] = useState(
406
407
  () => createInitialState2(enabled && startOnMount)
407
408
  );
@@ -428,8 +429,7 @@ var useHeartbeat = (options) => {
428
429
  handleTimeout();
429
430
  }, options.timeoutMs);
430
431
  });
431
- const runBeat = useEffectEvent(() => {
432
- const performedAt = Date.now();
432
+ const handleBeatSuccess = useEffectEvent((performedAt) => {
433
433
  commitState((current) => ({
434
434
  ...current,
435
435
  hasTimedOut: false,
@@ -437,7 +437,35 @@ var useHeartbeat = (options) => {
437
437
  }));
438
438
  scheduleTimeout();
439
439
  options.onBeat?.();
440
- void options.beat?.();
440
+ });
441
+ const handleBeatError = useEffectEvent((error) => {
442
+ timeoutRef.current.cancel();
443
+ options.onError?.(error);
444
+ });
445
+ const runBeat = useEffectEvent(() => {
446
+ const generation = generationRef.current;
447
+ const completeBeat = (result) => {
448
+ if (generation !== generationRef.current || result === false) {
449
+ return;
450
+ }
451
+ handleBeatSuccess(Date.now());
452
+ };
453
+ const failBeat = (error) => {
454
+ if (generation !== generationRef.current) {
455
+ return;
456
+ }
457
+ handleBeatError(error);
458
+ };
459
+ try {
460
+ const result = options.beat?.();
461
+ if (result !== null && typeof result === "object" && "then" in result) {
462
+ void Promise.resolve(result).then(completeBeat, failBeat);
463
+ return;
464
+ }
465
+ completeBeat(result);
466
+ } catch (error) {
467
+ failBeat(error);
468
+ }
441
469
  });
442
470
  const start = () => {
443
471
  if (!enabled) {
@@ -455,13 +483,10 @@ var useHeartbeat = (options) => {
455
483
  }));
456
484
  };
457
485
  const stop = () => {
486
+ generationRef.current += 1;
458
487
  intervalRef.current.cancel();
459
488
  timeoutRef.current.cancel();
460
- commitState((current) => ({
461
- ...current,
462
- hasTimedOut: false,
463
- isRunning: false
464
- }));
489
+ commitState(createInitialState2(false));
465
490
  };
466
491
  const beat = () => {
467
492
  if (!enabled) {
@@ -500,6 +525,7 @@ var useHeartbeat = (options) => {
500
525
  return void 0;
501
526
  }, [enabled, options.intervalMs, startOnMount]);
502
527
  useEffect(() => () => {
528
+ generationRef.current += 1;
503
529
  intervalRef.current.cancel();
504
530
  timeoutRef.current.cancel();
505
531
  }, []);
@@ -605,6 +631,7 @@ var toProtocolsDependency = (protocols) => {
605
631
  return Array.isArray(protocols) ? protocols.join("|") : protocols;
606
632
  };
607
633
  var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
634
+ var isSocketActive = (socket) => socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING;
608
635
  var useWebSocket = (options) => {
609
636
  const connect = options.connect ?? true;
610
637
  const supported = isWebSocketSupported();
@@ -616,6 +643,7 @@ var useWebSocket = (options) => {
616
643
  const manualOpenRef = useRef(false);
617
644
  const skipCloseReconnectRef = useRef(false);
618
645
  const suppressReconnectRef = useRef(false);
646
+ const pendingCloseActionRef = useRef(null);
619
647
  const terminalErrorRef = useRef(null);
620
648
  const [openNonce, setOpenNonce] = useState(0);
621
649
  const [state, setState] = useState(
@@ -634,6 +662,7 @@ var useWebSocket = (options) => {
634
662
  const heartbeatConfig = toHeartbeatConfig(
635
663
  options.heartbeat
636
664
  );
665
+ const defaultHeartbeatAction = options.reconnect === false ? "close" : "reconnect";
637
666
  const heartbeatHookOptions = heartbeatConfig === null ? {
638
667
  enabled: false,
639
668
  intervalMs: 1e3,
@@ -669,6 +698,27 @@ var useWebSocket = (options) => {
669
698
  if (heartbeatConfig !== null && heartbeatConfig.onTimeout !== void 0) {
670
699
  heartbeatHookOptions.onTimeout = heartbeatConfig.onTimeout;
671
700
  }
701
+ if (heartbeatConfig !== null) {
702
+ const onTimeout = heartbeatHookOptions.onTimeout;
703
+ heartbeatHookOptions.onTimeout = () => {
704
+ applyHeartbeatAction(
705
+ heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
706
+ new Event("heartbeat-timeout"),
707
+ "heartbeat-timeout"
708
+ );
709
+ onTimeout?.();
710
+ };
711
+ const onError = heartbeatConfig.onError;
712
+ heartbeatHookOptions.onError = (error) => {
713
+ const event = error instanceof Event ? error : new Event("heartbeat-error");
714
+ applyHeartbeatAction(
715
+ heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
716
+ event,
717
+ "error"
718
+ );
719
+ onError?.(error);
720
+ };
721
+ }
672
722
  const heartbeat = useHeartbeat(
673
723
  heartbeatHookOptions
674
724
  );
@@ -684,10 +734,47 @@ var useWebSocket = (options) => {
684
734
  }
685
735
  socketRef.current = null;
686
736
  socketKeyRef.current = null;
687
- if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
737
+ if (isSocketActive(socket2)) {
688
738
  socket2.close(code, reason);
689
739
  }
690
740
  });
741
+ const applyHeartbeatAction = useEffectEvent(
742
+ (action, error, reconnectTrigger) => {
743
+ heartbeat.stop();
744
+ options.onError?.(error);
745
+ if (action === "none") {
746
+ commitState((current) => ({
747
+ ...current,
748
+ lastChangedAt: Date.now(),
749
+ lastError: error
750
+ }));
751
+ return;
752
+ }
753
+ const shouldReconnect = action === "reconnect" && reconnectEnabled && (options.shouldReconnect?.(error) ?? true);
754
+ manualOpenRef.current = false;
755
+ terminalErrorRef.current = shouldReconnect ? null : error;
756
+ const socket2 = socketRef.current;
757
+ if (socket2 === null || !isSocketActive(socket2)) {
758
+ commitState((current) => ({
759
+ ...current,
760
+ lastChangedAt: Date.now(),
761
+ lastError: error,
762
+ status: shouldReconnect ? "reconnecting" : "error"
763
+ }));
764
+ if (shouldReconnect) {
765
+ reconnect.schedule(reconnectTrigger);
766
+ }
767
+ return;
768
+ }
769
+ pendingCloseActionRef.current = {
770
+ error,
771
+ reconnectTrigger: shouldReconnect ? reconnectTrigger : null
772
+ };
773
+ skipCloseReconnectRef.current = true;
774
+ suppressReconnectRef.current = true;
775
+ closeSocket();
776
+ }
777
+ );
691
778
  const parseMessage = useEffectEvent((event) => {
692
779
  const parser = options.parseMessage ?? defaultParseMessage;
693
780
  return parser(event);
@@ -732,6 +819,7 @@ var useWebSocket = (options) => {
732
819
  suppressReconnectRef.current = true;
733
820
  reconnect.cancel();
734
821
  heartbeat.stop();
822
+ options.onError?.(parseError);
735
823
  commitState((current) => ({
736
824
  ...current,
737
825
  lastChangedAt: Date.now(),
@@ -745,6 +833,7 @@ var useWebSocket = (options) => {
745
833
  heartbeat.stop();
746
834
  commitState((current) => ({
747
835
  ...current,
836
+ lastChangedAt: Date.now(),
748
837
  lastError: event,
749
838
  status: "error"
750
839
  }));
@@ -755,9 +844,26 @@ var useWebSocket = (options) => {
755
844
  socketKeyRef.current = null;
756
845
  heartbeat.stop();
757
846
  updateBufferedAmount();
847
+ const pendingCloseAction = pendingCloseActionRef.current;
848
+ pendingCloseActionRef.current = null;
758
849
  const terminalError = terminalErrorRef.current;
759
850
  const skipCloseReconnect = skipCloseReconnectRef.current;
760
851
  skipCloseReconnectRef.current = false;
852
+ if (pendingCloseAction !== null) {
853
+ suppressReconnectRef.current = false;
854
+ commitState((current) => ({
855
+ ...current,
856
+ lastChangedAt: Date.now(),
857
+ lastCloseEvent: event,
858
+ lastError: pendingCloseAction.error ?? current.lastError,
859
+ status: pendingCloseAction.reconnectTrigger === null ? "error" : "reconnecting"
860
+ }));
861
+ options.onClose?.(event);
862
+ if (pendingCloseAction.reconnectTrigger !== null) {
863
+ reconnect.schedule(pendingCloseAction.reconnectTrigger);
864
+ }
865
+ return;
866
+ }
761
867
  if (terminalError !== null) {
762
868
  suppressReconnectRef.current = false;
763
869
  commitState((current) => ({
@@ -916,7 +1022,7 @@ var useWebSocket = (options) => {
916
1022
  if (socket2 === null) {
917
1023
  return;
918
1024
  }
919
- if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
1025
+ if (isSocketActive(socket2)) {
920
1026
  socket2.close();
921
1027
  }
922
1028
  }, []);
@@ -1074,6 +1180,7 @@ var useEventSource = (options) => {
1074
1180
  suppressReconnectRef.current = true;
1075
1181
  reconnect.cancel();
1076
1182
  closeEventSource();
1183
+ options.onError?.(parseError);
1077
1184
  commitState((current) => ({
1078
1185
  ...current,
1079
1186
  lastChangedAt: Date.now(),