react-realtime-hooks 1.0.1 → 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/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,6 +483,7 @@ 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
489
  commitState((current) => ({
@@ -500,6 +529,7 @@ var useHeartbeat = (options) => {
500
529
  return void 0;
501
530
  }, [enabled, options.intervalMs, startOnMount]);
502
531
  useEffect(() => () => {
532
+ generationRef.current += 1;
503
533
  intervalRef.current.cancel();
504
534
  timeoutRef.current.cancel();
505
535
  }, []);
@@ -605,6 +635,7 @@ var toProtocolsDependency = (protocols) => {
605
635
  return Array.isArray(protocols) ? protocols.join("|") : protocols;
606
636
  };
607
637
  var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
638
+ var isSocketActive = (socket) => socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING;
608
639
  var useWebSocket = (options) => {
609
640
  const connect = options.connect ?? true;
610
641
  const supported = isWebSocketSupported();
@@ -616,6 +647,8 @@ var useWebSocket = (options) => {
616
647
  const manualOpenRef = useRef(false);
617
648
  const skipCloseReconnectRef = useRef(false);
618
649
  const suppressReconnectRef = useRef(false);
650
+ const pendingCloseActionRef = useRef(null);
651
+ const terminalErrorRef = useRef(null);
619
652
  const [openNonce, setOpenNonce] = useState(0);
620
653
  const [state, setState] = useState(
621
654
  () => createInitialState3(connect ? "connecting" : "idle")
@@ -633,6 +666,7 @@ var useWebSocket = (options) => {
633
666
  const heartbeatConfig = toHeartbeatConfig(
634
667
  options.heartbeat
635
668
  );
669
+ const defaultHeartbeatAction = options.reconnect === false ? "close" : "reconnect";
636
670
  const heartbeatHookOptions = heartbeatConfig === null ? {
637
671
  enabled: false,
638
672
  intervalMs: 1e3,
@@ -668,6 +702,27 @@ var useWebSocket = (options) => {
668
702
  if (heartbeatConfig !== null && heartbeatConfig.onTimeout !== void 0) {
669
703
  heartbeatHookOptions.onTimeout = heartbeatConfig.onTimeout;
670
704
  }
705
+ if (heartbeatConfig !== null) {
706
+ const onTimeout = heartbeatHookOptions.onTimeout;
707
+ heartbeatHookOptions.onTimeout = () => {
708
+ applyHeartbeatAction(
709
+ heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
710
+ new Event("heartbeat-timeout"),
711
+ "heartbeat-timeout"
712
+ );
713
+ onTimeout?.();
714
+ };
715
+ const onError = heartbeatConfig.onError;
716
+ heartbeatHookOptions.onError = (error) => {
717
+ const event = error instanceof Event ? error : new Event("heartbeat-error");
718
+ applyHeartbeatAction(
719
+ heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
720
+ event,
721
+ "error"
722
+ );
723
+ onError?.(error);
724
+ };
725
+ }
671
726
  const heartbeat = useHeartbeat(
672
727
  heartbeatHookOptions
673
728
  );
@@ -683,10 +738,46 @@ var useWebSocket = (options) => {
683
738
  }
684
739
  socketRef.current = null;
685
740
  socketKeyRef.current = null;
686
- if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
741
+ if (isSocketActive(socket2)) {
687
742
  socket2.close(code, reason);
688
743
  }
689
744
  });
745
+ const applyHeartbeatAction = useEffectEvent(
746
+ (action, error, reconnectTrigger) => {
747
+ heartbeat.stop();
748
+ if (action === "none") {
749
+ commitState((current) => ({
750
+ ...current,
751
+ lastChangedAt: Date.now(),
752
+ lastError: error
753
+ }));
754
+ return;
755
+ }
756
+ const shouldReconnect = action === "reconnect" && reconnectEnabled && (options.shouldReconnect?.(error) ?? true);
757
+ manualOpenRef.current = false;
758
+ terminalErrorRef.current = shouldReconnect ? null : error;
759
+ const socket2 = socketRef.current;
760
+ if (socket2 === null || !isSocketActive(socket2)) {
761
+ commitState((current) => ({
762
+ ...current,
763
+ lastChangedAt: Date.now(),
764
+ lastError: error,
765
+ status: shouldReconnect ? "reconnecting" : "error"
766
+ }));
767
+ if (shouldReconnect) {
768
+ reconnect.schedule(reconnectTrigger);
769
+ }
770
+ return;
771
+ }
772
+ pendingCloseActionRef.current = {
773
+ error,
774
+ reconnectTrigger: shouldReconnect ? reconnectTrigger : null
775
+ };
776
+ skipCloseReconnectRef.current = true;
777
+ suppressReconnectRef.current = true;
778
+ closeSocket();
779
+ }
780
+ );
690
781
  const parseMessage = useEffectEvent((event) => {
691
782
  const parser = options.parseMessage ?? defaultParseMessage;
692
783
  return parser(event);
@@ -699,7 +790,9 @@ var useWebSocket = (options) => {
699
790
  });
700
791
  const handleOpen = useEffectEvent((event, socket2) => {
701
792
  manualCloseRef.current = false;
793
+ manualOpenRef.current = false;
702
794
  suppressReconnectRef.current = false;
795
+ terminalErrorRef.current = null;
703
796
  reconnect.markConnected();
704
797
  heartbeat.start();
705
798
  commitState((current) => ({
@@ -723,11 +816,19 @@ var useWebSocket = (options) => {
723
816
  options.onMessage?.(message, event);
724
817
  } catch {
725
818
  const parseError = new Event("error");
819
+ terminalErrorRef.current = parseError;
820
+ manualOpenRef.current = false;
821
+ skipCloseReconnectRef.current = true;
822
+ suppressReconnectRef.current = true;
823
+ reconnect.cancel();
824
+ heartbeat.stop();
726
825
  commitState((current) => ({
727
826
  ...current,
827
+ lastChangedAt: Date.now(),
728
828
  lastError: parseError,
729
829
  status: "error"
730
830
  }));
831
+ closeSocket(1003, "parse-error");
731
832
  }
732
833
  });
733
834
  const handleError = useEffectEvent((event) => {
@@ -744,8 +845,38 @@ var useWebSocket = (options) => {
744
845
  socketKeyRef.current = null;
745
846
  heartbeat.stop();
746
847
  updateBufferedAmount();
848
+ const pendingCloseAction = pendingCloseActionRef.current;
849
+ pendingCloseActionRef.current = null;
850
+ const terminalError = terminalErrorRef.current;
747
851
  const skipCloseReconnect = skipCloseReconnectRef.current;
748
852
  skipCloseReconnectRef.current = false;
853
+ if (pendingCloseAction !== null) {
854
+ suppressReconnectRef.current = false;
855
+ commitState((current) => ({
856
+ ...current,
857
+ lastChangedAt: Date.now(),
858
+ lastCloseEvent: event,
859
+ lastError: pendingCloseAction.error ?? current.lastError,
860
+ status: pendingCloseAction.reconnectTrigger === null ? "error" : "reconnecting"
861
+ }));
862
+ options.onClose?.(event);
863
+ if (pendingCloseAction.reconnectTrigger !== null) {
864
+ reconnect.schedule(pendingCloseAction.reconnectTrigger);
865
+ }
866
+ return;
867
+ }
868
+ if (terminalError !== null) {
869
+ suppressReconnectRef.current = false;
870
+ commitState((current) => ({
871
+ ...current,
872
+ lastChangedAt: Date.now(),
873
+ lastCloseEvent: event,
874
+ lastError: terminalError,
875
+ status: "error"
876
+ }));
877
+ options.onClose?.(event);
878
+ return;
879
+ }
749
880
  const shouldReconnect = !suppressReconnectRef.current && !skipCloseReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
750
881
  commitState((current) => ({
751
882
  ...current,
@@ -764,6 +895,7 @@ var useWebSocket = (options) => {
764
895
  manualCloseRef.current = false;
765
896
  manualOpenRef.current = true;
766
897
  suppressReconnectRef.current = false;
898
+ terminalErrorRef.current = null;
767
899
  reconnect.cancel();
768
900
  setOpenNonce((current) => current + 1);
769
901
  };
@@ -772,6 +904,7 @@ var useWebSocket = (options) => {
772
904
  manualOpenRef.current = true;
773
905
  skipCloseReconnectRef.current = true;
774
906
  suppressReconnectRef.current = true;
907
+ terminalErrorRef.current = null;
775
908
  heartbeat.stop();
776
909
  closeSocket();
777
910
  suppressReconnectRef.current = false;
@@ -781,6 +914,7 @@ var useWebSocket = (options) => {
781
914
  manualCloseRef.current = true;
782
915
  manualOpenRef.current = false;
783
916
  suppressReconnectRef.current = true;
917
+ terminalErrorRef.current = null;
784
918
  reconnect.cancel();
785
919
  heartbeat.stop();
786
920
  commitState((current) => ({
@@ -818,7 +952,7 @@ var useWebSocket = (options) => {
818
952
  }));
819
953
  return;
820
954
  }
821
- const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
955
+ const shouldConnect = terminalErrorRef.current === null && (connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running");
822
956
  const nextSocketKey = `${resolvedUrl}::${protocolsDependency}::${options.binaryType ?? "blob"}`;
823
957
  if (!shouldConnect) {
824
958
  if (socketRef.current !== null) {
@@ -828,7 +962,7 @@ var useWebSocket = (options) => {
828
962
  socketKeyRef.current = null;
829
963
  commitState((current) => ({
830
964
  ...current,
831
- status: manualCloseRef.current ? "closed" : "idle"
965
+ status: terminalErrorRef.current !== null ? "error" : manualCloseRef.current ? "closed" : "idle"
832
966
  }));
833
967
  return;
834
968
  }
@@ -883,12 +1017,13 @@ var useWebSocket = (options) => {
883
1017
  useEffect(() => () => {
884
1018
  suppressReconnectRef.current = true;
885
1019
  socketKeyRef.current = null;
1020
+ terminalErrorRef.current = null;
886
1021
  const socket2 = socketRef.current;
887
1022
  socketRef.current = null;
888
1023
  if (socket2 === null) {
889
1024
  return;
890
1025
  }
891
- if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
1026
+ if (isSocketActive(socket2)) {
892
1027
  socket2.close();
893
1028
  }
894
1029
  }, []);
@@ -979,6 +1114,7 @@ var useEventSource = (options) => {
979
1114
  const manualOpenRef = useRef(false);
980
1115
  const skipErrorReconnectRef = useRef(false);
981
1116
  const suppressReconnectRef = useRef(false);
1117
+ const terminalErrorRef = useRef(null);
982
1118
  const [openNonce, setOpenNonce] = useState(0);
983
1119
  const [state, setState] = useState(
984
1120
  () => createInitialState4(connect ? "connecting" : "idle")
@@ -1012,7 +1148,9 @@ var useEventSource = (options) => {
1012
1148
  });
1013
1149
  const handleOpen = useEffectEvent((event, source) => {
1014
1150
  manualCloseRef.current = false;
1151
+ manualOpenRef.current = false;
1015
1152
  suppressReconnectRef.current = false;
1153
+ terminalErrorRef.current = null;
1016
1154
  reconnect.markConnected();
1017
1155
  commitState((current) => ({
1018
1156
  ...current,
@@ -1038,8 +1176,14 @@ var useEventSource = (options) => {
1038
1176
  options.onMessage?.(message, event);
1039
1177
  } catch {
1040
1178
  const parseError = new Event("error");
1179
+ terminalErrorRef.current = parseError;
1180
+ manualOpenRef.current = false;
1181
+ suppressReconnectRef.current = true;
1182
+ reconnect.cancel();
1183
+ closeEventSource();
1041
1184
  commitState((current) => ({
1042
1185
  ...current,
1186
+ lastChangedAt: Date.now(),
1043
1187
  lastError: parseError,
1044
1188
  status: "error"
1045
1189
  }));
@@ -1047,6 +1191,17 @@ var useEventSource = (options) => {
1047
1191
  }
1048
1192
  );
1049
1193
  const handleError = useEffectEvent((event, source) => {
1194
+ const terminalError = terminalErrorRef.current;
1195
+ if (terminalError !== null) {
1196
+ suppressReconnectRef.current = false;
1197
+ commitState((current) => ({
1198
+ ...current,
1199
+ lastChangedAt: Date.now(),
1200
+ lastError: terminalError,
1201
+ status: "error"
1202
+ }));
1203
+ return;
1204
+ }
1050
1205
  const skipErrorReconnect = skipErrorReconnectRef.current;
1051
1206
  skipErrorReconnectRef.current = false;
1052
1207
  const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
@@ -1073,6 +1228,7 @@ var useEventSource = (options) => {
1073
1228
  manualCloseRef.current = false;
1074
1229
  manualOpenRef.current = true;
1075
1230
  suppressReconnectRef.current = false;
1231
+ terminalErrorRef.current = null;
1076
1232
  reconnect.cancel();
1077
1233
  setOpenNonce((current) => current + 1);
1078
1234
  };
@@ -1081,6 +1237,7 @@ var useEventSource = (options) => {
1081
1237
  manualOpenRef.current = true;
1082
1238
  skipErrorReconnectRef.current = true;
1083
1239
  suppressReconnectRef.current = true;
1240
+ terminalErrorRef.current = null;
1084
1241
  closeEventSource();
1085
1242
  suppressReconnectRef.current = false;
1086
1243
  reconnect.schedule("manual");
@@ -1089,6 +1246,7 @@ var useEventSource = (options) => {
1089
1246
  manualCloseRef.current = true;
1090
1247
  manualOpenRef.current = false;
1091
1248
  suppressReconnectRef.current = true;
1249
+ terminalErrorRef.current = null;
1092
1250
  reconnect.cancel();
1093
1251
  closeEventSource();
1094
1252
  commitState((current) => ({
@@ -1115,7 +1273,7 @@ var useEventSource = (options) => {
1115
1273
  }));
1116
1274
  return;
1117
1275
  }
1118
- const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
1276
+ const shouldConnect = terminalErrorRef.current === null && (connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running");
1119
1277
  const nextEventSourceKey = [
1120
1278
  resolvedUrl,
1121
1279
  options.withCredentials ? "credentials" : "anonymous",
@@ -1129,7 +1287,7 @@ var useEventSource = (options) => {
1129
1287
  eventSourceKeyRef.current = null;
1130
1288
  commitState((current) => ({
1131
1289
  ...current,
1132
- status: manualCloseRef.current ? "closed" : "idle"
1290
+ status: terminalErrorRef.current !== null ? "error" : manualCloseRef.current ? "closed" : "idle"
1133
1291
  }));
1134
1292
  return;
1135
1293
  }
@@ -1191,6 +1349,7 @@ var useEventSource = (options) => {
1191
1349
  useEffect(() => () => {
1192
1350
  suppressReconnectRef.current = true;
1193
1351
  eventSourceKeyRef.current = null;
1352
+ terminalErrorRef.current = null;
1194
1353
  const source = eventSourceRef.current;
1195
1354
  eventSourceRef.current = null;
1196
1355
  if (source !== null) {