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