react-realtime-hooks 1.2.0 → 1.2.2

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.cjs CHANGED
@@ -444,6 +444,11 @@ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {
444
444
  trigger
445
445
  };
446
446
  };
447
+ var useStableCallback = (callback) => {
448
+ const callbackRef = react.useRef(callback);
449
+ callbackRef.current = callback;
450
+ return react.useCallback((...args) => callbackRef.current(...args), []);
451
+ };
447
452
 
448
453
  // src/hooks/useReconnect.ts
449
454
  var createInitialState = (enabled) => ({
@@ -467,20 +472,20 @@ var useReconnect = (options = {}) => {
467
472
  setState(resolved);
468
473
  });
469
474
  };
470
- const runAttempt = react.useEffectEvent((attempt) => {
475
+ const runAttempt = useStableCallback((attempt) => {
471
476
  commitState({
472
477
  attempt,
473
478
  nextDelayMs: null,
474
479
  status: "running"
475
480
  });
476
481
  });
477
- const emitSchedule = react.useEffectEvent((attempt) => {
482
+ const emitSchedule = useStableCallback((attempt) => {
478
483
  normalizedOptions.onSchedule?.(attempt);
479
484
  });
480
- const emitCancel = react.useEffectEvent(() => {
485
+ const emitCancel = useStableCallback(() => {
481
486
  normalizedOptions.onCancel?.();
482
487
  });
483
- const emitReset = react.useEffectEvent(() => {
488
+ const emitReset = useStableCallback(() => {
484
489
  normalizedOptions.onReset?.();
485
490
  });
486
491
  react.useEffect(() => {
@@ -503,7 +508,7 @@ var useReconnect = (options = {}) => {
503
508
  react.useEffect(() => () => {
504
509
  timeoutRef.current.cancel();
505
510
  }, []);
506
- const schedule = (trigger = "manual") => {
511
+ const schedule = useStableCallback((trigger = "manual") => {
507
512
  const current = stateRef.current;
508
513
  const nextAttempt = current.attempt + 1;
509
514
  const attempt = createReconnectAttempt(
@@ -531,8 +536,8 @@ var useReconnect = (options = {}) => {
531
536
  status: "scheduled"
532
537
  });
533
538
  emitSchedule(attempt);
534
- };
535
- const cancel = () => {
539
+ });
540
+ const cancel = useStableCallback(() => {
536
541
  const current = stateRef.current;
537
542
  const shouldEmitCancel = timeoutRef.current.isActive() || current.status === "scheduled" || current.status === "running";
538
543
  timeoutRef.current.cancel();
@@ -544,14 +549,14 @@ var useReconnect = (options = {}) => {
544
549
  if (shouldEmitCancel) {
545
550
  emitCancel();
546
551
  }
547
- };
548
- const reset = () => {
552
+ });
553
+ const reset = useStableCallback(() => {
549
554
  timeoutRef.current.cancel();
550
555
  lastDelayRef.current = null;
551
556
  commitState(createInitialState(normalizedOptions.enabled));
552
557
  emitReset();
553
- };
554
- const markConnected = () => {
558
+ });
559
+ const markConnected = useStableCallback(() => {
555
560
  timeoutRef.current.cancel();
556
561
  if (normalizedOptions.resetOnSuccess) {
557
562
  lastDelayRef.current = null;
@@ -564,7 +569,7 @@ var useReconnect = (options = {}) => {
564
569
  nextDelayMs: null,
565
570
  status: normalizedOptions.enabled ? "idle" : "stopped"
566
571
  }));
567
- };
572
+ });
568
573
  return {
569
574
  attempt: state.attempt,
570
575
  cancel,
@@ -600,14 +605,14 @@ var useHeartbeat = (options) => {
600
605
  stateRef.current = resolved;
601
606
  setState(resolved);
602
607
  };
603
- const handleTimeout = react.useEffectEvent(() => {
608
+ const handleTimeout = useStableCallback(() => {
604
609
  commitState((current) => ({
605
610
  ...current,
606
611
  hasTimedOut: true
607
612
  }));
608
613
  options.onTimeout?.();
609
614
  });
610
- const scheduleTimeout = react.useEffectEvent(() => {
615
+ const scheduleTimeout = useStableCallback(() => {
611
616
  if (options.timeoutMs === void 0) {
612
617
  timeoutRef.current.cancel();
613
618
  return;
@@ -616,7 +621,7 @@ var useHeartbeat = (options) => {
616
621
  handleTimeout();
617
622
  }, options.timeoutMs);
618
623
  });
619
- const handleBeatSuccess = react.useEffectEvent((performedAt) => {
624
+ const handleBeatSuccess = useStableCallback((performedAt) => {
620
625
  commitState((current) => ({
621
626
  ...current,
622
627
  hasTimedOut: false,
@@ -625,11 +630,11 @@ var useHeartbeat = (options) => {
625
630
  scheduleTimeout();
626
631
  options.onBeat?.();
627
632
  });
628
- const handleBeatError = react.useEffectEvent((error) => {
633
+ const handleBeatError = useStableCallback((error) => {
629
634
  timeoutRef.current.cancel();
630
635
  options.onError?.(error);
631
636
  });
632
- const runBeat = react.useEffectEvent(() => {
637
+ const runBeat = useStableCallback(() => {
633
638
  const generation = generationRef.current;
634
639
  const completeBeat = (result) => {
635
640
  if (generation !== generationRef.current || result === false) {
@@ -654,7 +659,7 @@ var useHeartbeat = (options) => {
654
659
  failBeat(error);
655
660
  }
656
661
  });
657
- const start = () => {
662
+ const start = useStableCallback(() => {
658
663
  if (!enabled) {
659
664
  return;
660
665
  }
@@ -668,20 +673,20 @@ var useHeartbeat = (options) => {
668
673
  hasTimedOut: false,
669
674
  isRunning: true
670
675
  }));
671
- };
672
- const stop = () => {
676
+ });
677
+ const stop = useStableCallback(() => {
673
678
  generationRef.current += 1;
674
679
  intervalRef.current.cancel();
675
680
  timeoutRef.current.cancel();
676
681
  commitState(createInitialState2(false));
677
- };
678
- const beat = () => {
682
+ });
683
+ const beat = useStableCallback(() => {
679
684
  if (!enabled) {
680
685
  return;
681
686
  }
682
687
  runBeat();
683
- };
684
- const notifyAck = (message) => {
688
+ });
689
+ const notifyAck = useStableCallback((message) => {
685
690
  if (stateRef.current.lastBeatAt === null) {
686
691
  return false;
687
692
  }
@@ -698,7 +703,7 @@ var useHeartbeat = (options) => {
698
703
  latencyMs: current.lastBeatAt === null ? null : acknowledgedAt - current.lastBeatAt
699
704
  }));
700
705
  return true;
701
- };
706
+ });
702
707
  react.useEffect(() => {
703
708
  if (!enabled) {
704
709
  stop();
@@ -710,7 +715,7 @@ var useHeartbeat = (options) => {
710
715
  }
711
716
  stop();
712
717
  return void 0;
713
- }, [enabled, options.intervalMs, startOnMount]);
718
+ }, [enabled, options.intervalMs, start, startOnMount, stop]);
714
719
  react.useEffect(() => () => {
715
720
  generationRef.current += 1;
716
721
  intervalRef.current.cancel();
@@ -774,6 +779,17 @@ var createConnectionStateSnapshot = (status, config = {}) => {
774
779
  }
775
780
  };
776
781
 
782
+ // src/core/errors.ts
783
+ var RealtimeErrorEvent = class extends Event {
784
+ cause;
785
+ kind;
786
+ constructor(type, init) {
787
+ super(type, init);
788
+ this.cause = init.cause;
789
+ this.kind = init.kind;
790
+ }
791
+ };
792
+
777
793
  // src/core/url.ts
778
794
  var normalizeResolvedUrl = (value) => {
779
795
  if (value === null) {
@@ -815,8 +831,9 @@ var toProtocolsDependency = (protocols) => {
815
831
  if (protocols === void 0) {
816
832
  return "";
817
833
  }
818
- return Array.isArray(protocols) ? protocols.join("|") : protocols;
834
+ return JSON.stringify(protocols);
819
835
  };
836
+ var parseProtocolsDependency = (protocolsDependency) => protocolsDependency === "" ? void 0 : JSON.parse(protocolsDependency);
820
837
  var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
821
838
  var isSocketActive = (socket) => socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING;
822
839
  var useWebSocket = (options) => {
@@ -824,8 +841,15 @@ var useWebSocket = (options) => {
824
841
  const supported = isWebSocketSupported();
825
842
  const resolvedUrl = react.useMemo(() => resolveUrlProvider(options.url), [options.url]);
826
843
  const protocolsDependency = toProtocolsDependency(options.protocols);
844
+ const protocols = react.useMemo(
845
+ () => parseProtocolsDependency(protocolsDependency),
846
+ [protocolsDependency]
847
+ );
827
848
  const socketRef = react.useRef(null);
828
849
  const socketKeyRef = react.useRef(null);
850
+ const activeSocketEpochRef = react.useRef(null);
851
+ const closingSocketEpochRef = react.useRef(null);
852
+ const nextSocketEpochRef = react.useRef(0);
829
853
  const manualCloseRef = react.useRef(false);
830
854
  const manualOpenRef = react.useRef(false);
831
855
  const skipCloseReconnectRef = react.useRef(false);
@@ -890,14 +914,19 @@ var useWebSocket = (options) => {
890
914
  heartbeatHookOptions.onTimeout = () => {
891
915
  applyHeartbeatAction(
892
916
  heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
893
- new Event("heartbeat-timeout"),
917
+ new RealtimeErrorEvent("heartbeat-timeout", {
918
+ kind: "heartbeat-timeout"
919
+ }),
894
920
  "heartbeat-timeout"
895
921
  );
896
922
  onTimeout?.();
897
923
  };
898
924
  const onError = heartbeatConfig.onError;
899
925
  heartbeatHookOptions.onError = (error) => {
900
- const event = error instanceof Event ? error : new Event("heartbeat-error");
926
+ const event = new RealtimeErrorEvent("heartbeat-error", {
927
+ cause: error,
928
+ kind: "heartbeat-error"
929
+ });
901
930
  applyHeartbeatAction(
902
931
  heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
903
932
  event,
@@ -909,23 +938,34 @@ var useWebSocket = (options) => {
909
938
  const heartbeat = useHeartbeat(
910
939
  heartbeatHookOptions
911
940
  );
912
- const commitState = (next) => {
941
+ const commitState = useStableCallback((next) => {
913
942
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
914
943
  stateRef.current = resolved;
915
944
  setState(resolved);
916
- };
917
- const closeSocket = react.useEffectEvent((code, reason) => {
918
- const socket2 = socketRef.current;
919
- if (socket2 === null) {
920
- return;
921
- }
922
- socketRef.current = null;
923
- socketKeyRef.current = null;
924
- if (isSocketActive(socket2)) {
925
- socket2.close(code, reason);
926
- }
927
945
  });
928
- const applyHeartbeatAction = react.useEffectEvent(
946
+ const isActiveSocketEvent = useStableCallback((socketEpoch) => {
947
+ return activeSocketEpochRef.current === socketEpoch;
948
+ });
949
+ const shouldHandleSocketClose = useStableCallback((socketEpoch) => {
950
+ return activeSocketEpochRef.current === socketEpoch || closingSocketEpochRef.current === socketEpoch;
951
+ });
952
+ const closeSocket = useStableCallback(
953
+ (config = {}) => {
954
+ const socket2 = socketRef.current;
955
+ const socketEpoch = activeSocketEpochRef.current;
956
+ if (socket2 === null || socketEpoch === null) {
957
+ return;
958
+ }
959
+ socketRef.current = null;
960
+ socketKeyRef.current = null;
961
+ activeSocketEpochRef.current = null;
962
+ closingSocketEpochRef.current = config.trackClose ? socketEpoch : null;
963
+ if (isSocketActive(socket2)) {
964
+ socket2.close(config.code, config.reason);
965
+ }
966
+ }
967
+ );
968
+ const applyHeartbeatAction = useStableCallback(
929
969
  (action, error, reconnectTrigger) => {
930
970
  heartbeat.stop();
931
971
  options.onError?.(error);
@@ -959,20 +999,20 @@ var useWebSocket = (options) => {
959
999
  };
960
1000
  skipCloseReconnectRef.current = true;
961
1001
  suppressReconnectRef.current = true;
962
- closeSocket();
1002
+ closeSocket({ trackClose: true });
963
1003
  }
964
1004
  );
965
- const parseMessage = react.useEffectEvent((event) => {
1005
+ const parseMessage = useStableCallback((event) => {
966
1006
  const parser = options.parseMessage ?? defaultParseMessage;
967
1007
  return parser(event);
968
1008
  });
969
- const updateBufferedAmount = react.useEffectEvent(() => {
1009
+ const updateBufferedAmount = useStableCallback(() => {
970
1010
  commitState((current) => ({
971
1011
  ...current,
972
1012
  bufferedAmount: socketRef.current?.bufferedAmount ?? 0
973
1013
  }));
974
1014
  });
975
- const handleOpen = react.useEffectEvent((event, socket2) => {
1015
+ const handleOpen = useStableCallback((event, socket2) => {
976
1016
  manualCloseRef.current = false;
977
1017
  manualOpenRef.current = false;
978
1018
  suppressReconnectRef.current = false;
@@ -987,7 +1027,7 @@ var useWebSocket = (options) => {
987
1027
  }));
988
1028
  options.onOpen?.(event, socket2);
989
1029
  });
990
- const handleMessage = react.useEffectEvent((event) => {
1030
+ const handleMessage = useStableCallback((event) => {
991
1031
  try {
992
1032
  const message = parseMessage(event);
993
1033
  heartbeat.notifyAck(message);
@@ -998,8 +1038,11 @@ var useWebSocket = (options) => {
998
1038
  lastMessageEvent: event
999
1039
  }));
1000
1040
  options.onMessage?.(message, event);
1001
- } catch {
1002
- const parseError = new Event("error");
1041
+ } catch (error) {
1042
+ const parseError = new RealtimeErrorEvent("error", {
1043
+ cause: error,
1044
+ kind: "parse-error"
1045
+ });
1003
1046
  terminalErrorRef.current = parseError;
1004
1047
  manualOpenRef.current = false;
1005
1048
  skipCloseReconnectRef.current = true;
@@ -1013,10 +1056,17 @@ var useWebSocket = (options) => {
1013
1056
  lastError: parseError,
1014
1057
  status: "error"
1015
1058
  }));
1016
- closeSocket(1003, "parse-error");
1059
+ closeSocket({
1060
+ code: 1003,
1061
+ reason: "parse-error",
1062
+ trackClose: true
1063
+ });
1017
1064
  }
1018
1065
  });
1019
- const handleError = react.useEffectEvent((event) => {
1066
+ const handleError = useStableCallback((event, socketEpoch) => {
1067
+ if (!isActiveSocketEvent(socketEpoch)) {
1068
+ return;
1069
+ }
1020
1070
  heartbeat.stop();
1021
1071
  commitState((current) => ({
1022
1072
  ...current,
@@ -1026,9 +1076,18 @@ var useWebSocket = (options) => {
1026
1076
  }));
1027
1077
  options.onError?.(event);
1028
1078
  });
1029
- const handleClose = react.useEffectEvent((event) => {
1030
- socketRef.current = null;
1031
- socketKeyRef.current = null;
1079
+ const handleClose = useStableCallback((event, socketEpoch) => {
1080
+ if (!shouldHandleSocketClose(socketEpoch)) {
1081
+ return;
1082
+ }
1083
+ if (activeSocketEpochRef.current === socketEpoch) {
1084
+ socketRef.current = null;
1085
+ socketKeyRef.current = null;
1086
+ activeSocketEpochRef.current = null;
1087
+ }
1088
+ if (closingSocketEpochRef.current === socketEpoch) {
1089
+ closingSocketEpochRef.current = null;
1090
+ }
1032
1091
  heartbeat.stop();
1033
1092
  updateBufferedAmount();
1034
1093
  const pendingCloseAction = pendingCloseActionRef.current;
@@ -1077,15 +1136,15 @@ var useWebSocket = (options) => {
1077
1136
  suppressReconnectRef.current = false;
1078
1137
  }
1079
1138
  });
1080
- const open = () => {
1139
+ const open = useStableCallback(() => {
1081
1140
  manualCloseRef.current = false;
1082
1141
  manualOpenRef.current = true;
1083
1142
  suppressReconnectRef.current = false;
1084
1143
  terminalErrorRef.current = null;
1085
1144
  reconnect.cancel();
1086
1145
  setOpenNonce((current) => current + 1);
1087
- };
1088
- const reconnectNow = () => {
1146
+ });
1147
+ const reconnectNow = useStableCallback(() => {
1089
1148
  manualCloseRef.current = false;
1090
1149
  manualOpenRef.current = true;
1091
1150
  skipCloseReconnectRef.current = true;
@@ -1095,8 +1154,8 @@ var useWebSocket = (options) => {
1095
1154
  closeSocket();
1096
1155
  suppressReconnectRef.current = false;
1097
1156
  reconnect.schedule("manual");
1098
- };
1099
- const close = (code, reason) => {
1157
+ });
1158
+ const close = useStableCallback((code, reason) => {
1100
1159
  manualCloseRef.current = true;
1101
1160
  manualOpenRef.current = false;
1102
1161
  suppressReconnectRef.current = true;
@@ -1108,9 +1167,13 @@ var useWebSocket = (options) => {
1108
1167
  lastChangedAt: Date.now(),
1109
1168
  status: "closing"
1110
1169
  }));
1111
- closeSocket(code, reason);
1112
- };
1113
- const send = (message) => {
1170
+ closeSocket({
1171
+ code,
1172
+ reason,
1173
+ trackClose: true
1174
+ });
1175
+ });
1176
+ const send = useStableCallback((message) => {
1114
1177
  const socket2 = socketRef.current;
1115
1178
  if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
1116
1179
  return false;
@@ -1119,7 +1182,7 @@ var useWebSocket = (options) => {
1119
1182
  socket2.send(serializer(message));
1120
1183
  updateBufferedAmount();
1121
1184
  return true;
1122
- };
1185
+ });
1123
1186
  react.useEffect(() => {
1124
1187
  if (!supported) {
1125
1188
  socketKeyRef.current = null;
@@ -1159,9 +1222,13 @@ var useWebSocket = (options) => {
1159
1222
  if (socketRef.current !== null) {
1160
1223
  return;
1161
1224
  }
1162
- const socket2 = new WebSocket(resolvedUrl, options.protocols);
1225
+ const socket2 = new WebSocket(resolvedUrl, protocols);
1226
+ const socketEpoch = nextSocketEpochRef.current + 1;
1163
1227
  socketRef.current = socket2;
1164
1228
  socketKeyRef.current = nextSocketKey;
1229
+ activeSocketEpochRef.current = socketEpoch;
1230
+ closingSocketEpochRef.current = null;
1231
+ nextSocketEpochRef.current = socketEpoch;
1165
1232
  socket2.binaryType = options.binaryType ?? "blob";
1166
1233
  commitState((current) => ({
1167
1234
  ...current,
@@ -1170,16 +1237,22 @@ var useWebSocket = (options) => {
1170
1237
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1171
1238
  }));
1172
1239
  const handleSocketOpen = (event) => {
1240
+ if (!isActiveSocketEvent(socketEpoch)) {
1241
+ return;
1242
+ }
1173
1243
  handleOpen(event, socket2);
1174
1244
  };
1175
1245
  const handleSocketMessage = (event) => {
1246
+ if (!isActiveSocketEvent(socketEpoch)) {
1247
+ return;
1248
+ }
1176
1249
  handleMessage(event);
1177
1250
  };
1178
1251
  const handleSocketError = (event) => {
1179
- handleError(event);
1252
+ handleError(event, socketEpoch);
1180
1253
  };
1181
1254
  const handleSocketClose = (event) => {
1182
- handleClose(event);
1255
+ handleClose(event, socketEpoch);
1183
1256
  };
1184
1257
  socket2.addEventListener("open", handleSocketOpen);
1185
1258
  socket2.addEventListener("message", handleSocketMessage);
@@ -1192,9 +1265,17 @@ var useWebSocket = (options) => {
1192
1265
  socket2.removeEventListener("close", handleSocketClose);
1193
1266
  };
1194
1267
  }, [
1268
+ closeSocket,
1269
+ commitState,
1195
1270
  connect,
1271
+ handleClose,
1272
+ handleError,
1273
+ handleMessage,
1274
+ handleOpen,
1275
+ isActiveSocketEvent,
1196
1276
  openNonce,
1197
1277
  options.binaryType,
1278
+ protocols,
1198
1279
  protocolsDependency,
1199
1280
  reconnect.status,
1200
1281
  resolvedUrl,
@@ -1203,6 +1284,8 @@ var useWebSocket = (options) => {
1203
1284
  react.useEffect(() => () => {
1204
1285
  suppressReconnectRef.current = true;
1205
1286
  socketKeyRef.current = null;
1287
+ activeSocketEpochRef.current = null;
1288
+ closingSocketEpochRef.current = null;
1206
1289
  terminalErrorRef.current = null;
1207
1290
  const socket2 = socketRef.current;
1208
1291
  socketRef.current = null;
@@ -1213,11 +1296,12 @@ var useWebSocket = (options) => {
1213
1296
  socket2.close();
1214
1297
  }
1215
1298
  }, []);
1299
+ const stopHeartbeat = heartbeat.stop;
1216
1300
  react.useEffect(() => {
1217
1301
  if (state.status !== "open") {
1218
- heartbeat.stop();
1302
+ stopHeartbeat();
1219
1303
  }
1220
- }, [state.status]);
1304
+ }, [state.status, stopHeartbeat]);
1221
1305
  const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1222
1306
  const snapshot = createConnectionStateSnapshot(status, {
1223
1307
  isSupported: supported,
@@ -1273,29 +1357,27 @@ var createInitialState4 = (status = "idle") => ({
1273
1357
  status
1274
1358
  });
1275
1359
  var defaultParseMessage2 = (event) => event.data;
1276
- var toEventsDependency = (events) => {
1277
- if (events === void 0 || events.length === 0) {
1278
- return "";
1279
- }
1280
- return [...new Set(events)].sort().join("|");
1281
- };
1282
1360
  var normalizeNamedEvents = (events) => {
1283
1361
  if (events === void 0 || events.length === 0) {
1284
1362
  return [];
1285
1363
  }
1286
- return [...new Set(events)].filter((eventName) => eventName !== "message");
1364
+ return [...new Set(events)].filter((eventName) => eventName !== "message").sort();
1287
1365
  };
1366
+ var toEventsDependency = (events) => JSON.stringify(normalizeNamedEvents(events));
1367
+ var parseEventsDependency = (eventsDependency) => JSON.parse(eventsDependency);
1288
1368
  var useEventSource = (options) => {
1289
1369
  const connect = options.connect ?? true;
1290
1370
  const supported = isEventSourceSupported();
1291
1371
  const resolvedUrl = react.useMemo(() => resolveUrlProvider(options.url), [options.url]);
1292
1372
  const eventsDependency = toEventsDependency(options.events);
1293
1373
  const namedEvents = react.useMemo(
1294
- () => normalizeNamedEvents(options.events),
1374
+ () => parseEventsDependency(eventsDependency),
1295
1375
  [eventsDependency]
1296
1376
  );
1297
1377
  const eventSourceRef = react.useRef(null);
1298
1378
  const eventSourceKeyRef = react.useRef(null);
1379
+ const activeEventSourceEpochRef = react.useRef(null);
1380
+ const nextEventSourceEpochRef = react.useRef(0);
1299
1381
  const manualCloseRef = react.useRef(false);
1300
1382
  const manualOpenRef = react.useRef(false);
1301
1383
  const skipErrorReconnectRef = react.useRef(false);
@@ -1314,25 +1396,29 @@ var useEventSource = (options) => {
1314
1396
  enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
1315
1397
  }
1316
1398
  );
1317
- const commitState = (next) => {
1399
+ const commitState = useStableCallback((next) => {
1318
1400
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
1319
1401
  stateRef.current = resolved;
1320
1402
  setState(resolved);
1321
- };
1322
- const closeEventSource = react.useEffectEvent(() => {
1403
+ });
1404
+ const closeEventSource = useStableCallback(() => {
1323
1405
  const source = eventSourceRef.current;
1324
1406
  if (source === null) {
1325
1407
  return;
1326
1408
  }
1327
1409
  eventSourceRef.current = null;
1328
1410
  eventSourceKeyRef.current = null;
1411
+ activeEventSourceEpochRef.current = null;
1329
1412
  source.close();
1330
1413
  });
1331
- const parseMessage = react.useEffectEvent((event) => {
1414
+ const isActiveEventSourceEvent = useStableCallback((sourceEpoch) => {
1415
+ return activeEventSourceEpochRef.current === sourceEpoch;
1416
+ });
1417
+ const parseMessage = useStableCallback((event) => {
1332
1418
  const parser = options.parseMessage ?? defaultParseMessage2;
1333
1419
  return parser(event);
1334
1420
  });
1335
- const handleOpen = react.useEffectEvent((event, source) => {
1421
+ const handleOpen = useStableCallback((event, source) => {
1336
1422
  manualCloseRef.current = false;
1337
1423
  manualOpenRef.current = false;
1338
1424
  suppressReconnectRef.current = false;
@@ -1345,7 +1431,7 @@ var useEventSource = (options) => {
1345
1431
  }));
1346
1432
  options.onOpen?.(event, source);
1347
1433
  });
1348
- const commitParsedMessage = react.useEffectEvent(
1434
+ const commitParsedMessage = useStableCallback(
1349
1435
  (eventName, event, isNamedEvent) => {
1350
1436
  try {
1351
1437
  const message = parseMessage(event);
@@ -1360,8 +1446,11 @@ var useEventSource = (options) => {
1360
1446
  return;
1361
1447
  }
1362
1448
  options.onMessage?.(message, event);
1363
- } catch {
1364
- const parseError = new Event("error");
1449
+ } catch (error) {
1450
+ const parseError = new RealtimeErrorEvent("error", {
1451
+ cause: error,
1452
+ kind: "parse-error"
1453
+ });
1365
1454
  terminalErrorRef.current = parseError;
1366
1455
  manualOpenRef.current = false;
1367
1456
  suppressReconnectRef.current = true;
@@ -1377,7 +1466,10 @@ var useEventSource = (options) => {
1377
1466
  }
1378
1467
  }
1379
1468
  );
1380
- const handleError = react.useEffectEvent((event, source) => {
1469
+ const handleError = useStableCallback((event, source, sourceEpoch) => {
1470
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1471
+ return;
1472
+ }
1381
1473
  const terminalError = terminalErrorRef.current;
1382
1474
  if (terminalError !== null) {
1383
1475
  suppressReconnectRef.current = false;
@@ -1392,12 +1484,11 @@ var useEventSource = (options) => {
1392
1484
  const skipErrorReconnect = skipErrorReconnectRef.current;
1393
1485
  skipErrorReconnectRef.current = false;
1394
1486
  const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
1395
- const readyState = source.readyState;
1396
1487
  commitState((current) => ({
1397
1488
  ...current,
1398
1489
  lastChangedAt: Date.now(),
1399
1490
  lastError: event,
1400
- status: readyState === EventSource.OPEN ? "open" : shouldReconnect ? "reconnecting" : "closed"
1491
+ status: shouldReconnect ? "reconnecting" : "closed"
1401
1492
  }));
1402
1493
  options.onError?.(event);
1403
1494
  if (!shouldReconnect) {
@@ -1405,21 +1496,18 @@ var useEventSource = (options) => {
1405
1496
  closeEventSource();
1406
1497
  return;
1407
1498
  }
1408
- if (readyState === EventSource.CLOSED) {
1409
- eventSourceRef.current = null;
1410
- eventSourceKeyRef.current = null;
1411
- reconnect.schedule("error");
1412
- }
1499
+ closeEventSource();
1500
+ reconnect.schedule("error");
1413
1501
  });
1414
- const open = () => {
1502
+ const open = useStableCallback(() => {
1415
1503
  manualCloseRef.current = false;
1416
1504
  manualOpenRef.current = true;
1417
1505
  suppressReconnectRef.current = false;
1418
1506
  terminalErrorRef.current = null;
1419
1507
  reconnect.cancel();
1420
1508
  setOpenNonce((current) => current + 1);
1421
- };
1422
- const reconnectNow = () => {
1509
+ });
1510
+ const reconnectNow = useStableCallback(() => {
1423
1511
  manualCloseRef.current = false;
1424
1512
  manualOpenRef.current = true;
1425
1513
  skipErrorReconnectRef.current = true;
@@ -1428,8 +1516,8 @@ var useEventSource = (options) => {
1428
1516
  closeEventSource();
1429
1517
  suppressReconnectRef.current = false;
1430
1518
  reconnect.schedule("manual");
1431
- };
1432
- const close = () => {
1519
+ });
1520
+ const close = useStableCallback(() => {
1433
1521
  manualCloseRef.current = true;
1434
1522
  manualOpenRef.current = false;
1435
1523
  suppressReconnectRef.current = true;
@@ -1441,7 +1529,7 @@ var useEventSource = (options) => {
1441
1529
  lastChangedAt: Date.now(),
1442
1530
  status: "closed"
1443
1531
  }));
1444
- };
1532
+ });
1445
1533
  react.useEffect(() => {
1446
1534
  if (!supported) {
1447
1535
  eventSourceKeyRef.current = null;
@@ -1460,7 +1548,7 @@ var useEventSource = (options) => {
1460
1548
  }));
1461
1549
  return;
1462
1550
  }
1463
- const shouldConnect = terminalErrorRef.current === null && (connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running");
1551
+ const shouldConnect = terminalErrorRef.current === null && (reconnect.status === "running" || reconnect.status !== "scheduled" && (connect && !manualCloseRef.current || manualOpenRef.current));
1464
1552
  const nextEventSourceKey = [
1465
1553
  resolvedUrl,
1466
1554
  options.withCredentials ? "credentials" : "anonymous",
@@ -1488,27 +1576,39 @@ var useEventSource = (options) => {
1488
1576
  const source = new EventSource(resolvedUrl, {
1489
1577
  withCredentials: options.withCredentials ?? false
1490
1578
  });
1579
+ const sourceEpoch = nextEventSourceEpochRef.current + 1;
1491
1580
  eventSourceRef.current = source;
1492
1581
  eventSourceKeyRef.current = nextEventSourceKey;
1582
+ activeEventSourceEpochRef.current = sourceEpoch;
1583
+ nextEventSourceEpochRef.current = sourceEpoch;
1493
1584
  commitState((current) => ({
1494
1585
  ...current,
1495
1586
  lastChangedAt: Date.now(),
1496
1587
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1497
1588
  }));
1498
1589
  const handleSourceOpen = (event) => {
1590
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1591
+ return;
1592
+ }
1499
1593
  handleOpen(event, source);
1500
1594
  };
1501
1595
  const handleSourceMessage = (event) => {
1596
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1597
+ return;
1598
+ }
1502
1599
  commitParsedMessage("message", event, false);
1503
1600
  };
1504
1601
  const namedEventHandlers = /* @__PURE__ */ new Map();
1505
1602
  const handleSourceError = (event) => {
1506
- handleError(event, source);
1603
+ handleError(event, source, sourceEpoch);
1507
1604
  };
1508
1605
  source.addEventListener("open", handleSourceOpen);
1509
1606
  source.addEventListener("message", handleSourceMessage);
1510
1607
  for (const eventName of namedEvents) {
1511
1608
  const handler = (event) => {
1609
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1610
+ return;
1611
+ }
1512
1612
  commitParsedMessage(eventName, event, true);
1513
1613
  };
1514
1614
  namedEventHandlers.set(eventName, handler);
@@ -1524,8 +1624,14 @@ var useEventSource = (options) => {
1524
1624
  source.removeEventListener("error", handleSourceError);
1525
1625
  };
1526
1626
  }, [
1627
+ closeEventSource,
1628
+ commitParsedMessage,
1629
+ commitState,
1527
1630
  connect,
1528
1631
  eventsDependency,
1632
+ handleError,
1633
+ handleOpen,
1634
+ isActiveEventSourceEvent,
1529
1635
  namedEvents,
1530
1636
  openNonce,
1531
1637
  options.withCredentials,
@@ -1536,6 +1642,7 @@ var useEventSource = (options) => {
1536
1642
  react.useEffect(() => () => {
1537
1643
  suppressReconnectRef.current = true;
1538
1644
  eventSourceKeyRef.current = null;
1645
+ activeEventSourceEpochRef.current = null;
1539
1646
  terminalErrorRef.current = null;
1540
1647
  const source = eventSourceRef.current;
1541
1648
  eventSourceRef.current = null;
@@ -1580,6 +1687,7 @@ var useEventSource = (options) => {
1580
1687
  };
1581
1688
  };
1582
1689
 
1690
+ exports.RealtimeErrorEvent = RealtimeErrorEvent;
1583
1691
  exports.useConnectionGate = useConnectionGate;
1584
1692
  exports.useEventSource = useEventSource;
1585
1693
  exports.useHeartbeat = useHeartbeat;