react-realtime-hooks 1.2.1 → 1.3.0

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,6 +841,10 @@ 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);
829
850
  const activeSocketEpochRef = react.useRef(null);
@@ -893,14 +914,19 @@ var useWebSocket = (options) => {
893
914
  heartbeatHookOptions.onTimeout = () => {
894
915
  applyHeartbeatAction(
895
916
  heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
896
- new Event("heartbeat-timeout"),
917
+ new RealtimeErrorEvent("heartbeat-timeout", {
918
+ kind: "heartbeat-timeout"
919
+ }),
897
920
  "heartbeat-timeout"
898
921
  );
899
922
  onTimeout?.();
900
923
  };
901
924
  const onError = heartbeatConfig.onError;
902
925
  heartbeatHookOptions.onError = (error) => {
903
- 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
+ });
904
930
  applyHeartbeatAction(
905
931
  heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
906
932
  event,
@@ -912,18 +938,18 @@ var useWebSocket = (options) => {
912
938
  const heartbeat = useHeartbeat(
913
939
  heartbeatHookOptions
914
940
  );
915
- const commitState = (next) => {
941
+ const commitState = useStableCallback((next) => {
916
942
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
917
943
  stateRef.current = resolved;
918
944
  setState(resolved);
919
- };
920
- const isActiveSocketEvent = react.useEffectEvent((socketEpoch) => {
945
+ });
946
+ const isActiveSocketEvent = useStableCallback((socketEpoch) => {
921
947
  return activeSocketEpochRef.current === socketEpoch;
922
948
  });
923
- const shouldHandleSocketClose = react.useEffectEvent((socketEpoch) => {
949
+ const shouldHandleSocketClose = useStableCallback((socketEpoch) => {
924
950
  return activeSocketEpochRef.current === socketEpoch || closingSocketEpochRef.current === socketEpoch;
925
951
  });
926
- const closeSocket = react.useEffectEvent(
952
+ const closeSocket = useStableCallback(
927
953
  (config = {}) => {
928
954
  const socket2 = socketRef.current;
929
955
  const socketEpoch = activeSocketEpochRef.current;
@@ -939,7 +965,7 @@ var useWebSocket = (options) => {
939
965
  }
940
966
  }
941
967
  );
942
- const applyHeartbeatAction = react.useEffectEvent(
968
+ const applyHeartbeatAction = useStableCallback(
943
969
  (action, error, reconnectTrigger) => {
944
970
  heartbeat.stop();
945
971
  options.onError?.(error);
@@ -976,17 +1002,17 @@ var useWebSocket = (options) => {
976
1002
  closeSocket({ trackClose: true });
977
1003
  }
978
1004
  );
979
- const parseMessage = react.useEffectEvent((event) => {
1005
+ const parseMessage = useStableCallback((event) => {
980
1006
  const parser = options.parseMessage ?? defaultParseMessage;
981
1007
  return parser(event);
982
1008
  });
983
- const updateBufferedAmount = react.useEffectEvent(() => {
1009
+ const updateBufferedAmount = useStableCallback(() => {
984
1010
  commitState((current) => ({
985
1011
  ...current,
986
1012
  bufferedAmount: socketRef.current?.bufferedAmount ?? 0
987
1013
  }));
988
1014
  });
989
- const handleOpen = react.useEffectEvent((event, socket2) => {
1015
+ const handleOpen = useStableCallback((event, socket2) => {
990
1016
  manualCloseRef.current = false;
991
1017
  manualOpenRef.current = false;
992
1018
  suppressReconnectRef.current = false;
@@ -1001,7 +1027,7 @@ var useWebSocket = (options) => {
1001
1027
  }));
1002
1028
  options.onOpen?.(event, socket2);
1003
1029
  });
1004
- const handleMessage = react.useEffectEvent((event) => {
1030
+ const handleMessage = useStableCallback((event) => {
1005
1031
  try {
1006
1032
  const message = parseMessage(event);
1007
1033
  heartbeat.notifyAck(message);
@@ -1012,8 +1038,11 @@ var useWebSocket = (options) => {
1012
1038
  lastMessageEvent: event
1013
1039
  }));
1014
1040
  options.onMessage?.(message, event);
1015
- } catch {
1016
- const parseError = new Event("error");
1041
+ } catch (error) {
1042
+ const parseError = new RealtimeErrorEvent("error", {
1043
+ cause: error,
1044
+ kind: "parse-error"
1045
+ });
1017
1046
  terminalErrorRef.current = parseError;
1018
1047
  manualOpenRef.current = false;
1019
1048
  skipCloseReconnectRef.current = true;
@@ -1034,7 +1063,7 @@ var useWebSocket = (options) => {
1034
1063
  });
1035
1064
  }
1036
1065
  });
1037
- const handleError = react.useEffectEvent((event, socketEpoch) => {
1066
+ const handleError = useStableCallback((event, socketEpoch) => {
1038
1067
  if (!isActiveSocketEvent(socketEpoch)) {
1039
1068
  return;
1040
1069
  }
@@ -1047,7 +1076,7 @@ var useWebSocket = (options) => {
1047
1076
  }));
1048
1077
  options.onError?.(event);
1049
1078
  });
1050
- const handleClose = react.useEffectEvent((event, socketEpoch) => {
1079
+ const handleClose = useStableCallback((event, socketEpoch) => {
1051
1080
  if (!shouldHandleSocketClose(socketEpoch)) {
1052
1081
  return;
1053
1082
  }
@@ -1107,15 +1136,15 @@ var useWebSocket = (options) => {
1107
1136
  suppressReconnectRef.current = false;
1108
1137
  }
1109
1138
  });
1110
- const open = () => {
1139
+ const open = useStableCallback(() => {
1111
1140
  manualCloseRef.current = false;
1112
1141
  manualOpenRef.current = true;
1113
1142
  suppressReconnectRef.current = false;
1114
1143
  terminalErrorRef.current = null;
1115
1144
  reconnect.cancel();
1116
1145
  setOpenNonce((current) => current + 1);
1117
- };
1118
- const reconnectNow = () => {
1146
+ });
1147
+ const reconnectNow = useStableCallback(() => {
1119
1148
  manualCloseRef.current = false;
1120
1149
  manualOpenRef.current = true;
1121
1150
  skipCloseReconnectRef.current = true;
@@ -1125,8 +1154,8 @@ var useWebSocket = (options) => {
1125
1154
  closeSocket();
1126
1155
  suppressReconnectRef.current = false;
1127
1156
  reconnect.schedule("manual");
1128
- };
1129
- const close = (code, reason) => {
1157
+ });
1158
+ const close = useStableCallback((code, reason) => {
1130
1159
  manualCloseRef.current = true;
1131
1160
  manualOpenRef.current = false;
1132
1161
  suppressReconnectRef.current = true;
@@ -1143,8 +1172,8 @@ var useWebSocket = (options) => {
1143
1172
  reason,
1144
1173
  trackClose: true
1145
1174
  });
1146
- };
1147
- const send = (message) => {
1175
+ });
1176
+ const send = useStableCallback((message) => {
1148
1177
  const socket2 = socketRef.current;
1149
1178
  if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
1150
1179
  return false;
@@ -1153,7 +1182,7 @@ var useWebSocket = (options) => {
1153
1182
  socket2.send(serializer(message));
1154
1183
  updateBufferedAmount();
1155
1184
  return true;
1156
- };
1185
+ });
1157
1186
  react.useEffect(() => {
1158
1187
  if (!supported) {
1159
1188
  socketKeyRef.current = null;
@@ -1193,7 +1222,7 @@ var useWebSocket = (options) => {
1193
1222
  if (socketRef.current !== null) {
1194
1223
  return;
1195
1224
  }
1196
- const socket2 = new WebSocket(resolvedUrl, options.protocols);
1225
+ const socket2 = new WebSocket(resolvedUrl, protocols);
1197
1226
  const socketEpoch = nextSocketEpochRef.current + 1;
1198
1227
  socketRef.current = socket2;
1199
1228
  socketKeyRef.current = nextSocketKey;
@@ -1236,9 +1265,17 @@ var useWebSocket = (options) => {
1236
1265
  socket2.removeEventListener("close", handleSocketClose);
1237
1266
  };
1238
1267
  }, [
1268
+ closeSocket,
1269
+ commitState,
1239
1270
  connect,
1271
+ handleClose,
1272
+ handleError,
1273
+ handleMessage,
1274
+ handleOpen,
1275
+ isActiveSocketEvent,
1240
1276
  openNonce,
1241
1277
  options.binaryType,
1278
+ protocols,
1242
1279
  protocolsDependency,
1243
1280
  reconnect.status,
1244
1281
  resolvedUrl,
@@ -1259,11 +1296,12 @@ var useWebSocket = (options) => {
1259
1296
  socket2.close();
1260
1297
  }
1261
1298
  }, []);
1299
+ const stopHeartbeat = heartbeat.stop;
1262
1300
  react.useEffect(() => {
1263
1301
  if (state.status !== "open") {
1264
- heartbeat.stop();
1302
+ stopHeartbeat();
1265
1303
  }
1266
- }, [state.status]);
1304
+ }, [state.status, stopHeartbeat]);
1267
1305
  const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1268
1306
  const snapshot = createConnectionStateSnapshot(status, {
1269
1307
  isSupported: supported,
@@ -1319,29 +1357,27 @@ var createInitialState4 = (status = "idle") => ({
1319
1357
  status
1320
1358
  });
1321
1359
  var defaultParseMessage2 = (event) => event.data;
1322
- var toEventsDependency = (events) => {
1323
- if (events === void 0 || events.length === 0) {
1324
- return "";
1325
- }
1326
- return [...new Set(events)].sort().join("|");
1327
- };
1328
1360
  var normalizeNamedEvents = (events) => {
1329
1361
  if (events === void 0 || events.length === 0) {
1330
1362
  return [];
1331
1363
  }
1332
- return [...new Set(events)].filter((eventName) => eventName !== "message");
1364
+ return [...new Set(events)].filter((eventName) => eventName !== "message").sort();
1333
1365
  };
1366
+ var toEventsDependency = (events) => JSON.stringify(normalizeNamedEvents(events));
1367
+ var parseEventsDependency = (eventsDependency) => JSON.parse(eventsDependency);
1334
1368
  var useEventSource = (options) => {
1335
1369
  const connect = options.connect ?? true;
1336
1370
  const supported = isEventSourceSupported();
1337
1371
  const resolvedUrl = react.useMemo(() => resolveUrlProvider(options.url), [options.url]);
1338
1372
  const eventsDependency = toEventsDependency(options.events);
1339
1373
  const namedEvents = react.useMemo(
1340
- () => normalizeNamedEvents(options.events),
1374
+ () => parseEventsDependency(eventsDependency),
1341
1375
  [eventsDependency]
1342
1376
  );
1343
1377
  const eventSourceRef = react.useRef(null);
1344
1378
  const eventSourceKeyRef = react.useRef(null);
1379
+ const activeEventSourceEpochRef = react.useRef(null);
1380
+ const nextEventSourceEpochRef = react.useRef(0);
1345
1381
  const manualCloseRef = react.useRef(false);
1346
1382
  const manualOpenRef = react.useRef(false);
1347
1383
  const skipErrorReconnectRef = react.useRef(false);
@@ -1360,25 +1396,29 @@ var useEventSource = (options) => {
1360
1396
  enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
1361
1397
  }
1362
1398
  );
1363
- const commitState = (next) => {
1399
+ const commitState = useStableCallback((next) => {
1364
1400
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
1365
1401
  stateRef.current = resolved;
1366
1402
  setState(resolved);
1367
- };
1368
- const closeEventSource = react.useEffectEvent(() => {
1403
+ });
1404
+ const closeEventSource = useStableCallback(() => {
1369
1405
  const source = eventSourceRef.current;
1370
1406
  if (source === null) {
1371
1407
  return;
1372
1408
  }
1373
1409
  eventSourceRef.current = null;
1374
1410
  eventSourceKeyRef.current = null;
1411
+ activeEventSourceEpochRef.current = null;
1375
1412
  source.close();
1376
1413
  });
1377
- const parseMessage = react.useEffectEvent((event) => {
1414
+ const isActiveEventSourceEvent = useStableCallback((sourceEpoch) => {
1415
+ return activeEventSourceEpochRef.current === sourceEpoch;
1416
+ });
1417
+ const parseMessage = useStableCallback((event) => {
1378
1418
  const parser = options.parseMessage ?? defaultParseMessage2;
1379
1419
  return parser(event);
1380
1420
  });
1381
- const handleOpen = react.useEffectEvent((event, source) => {
1421
+ const handleOpen = useStableCallback((event, source) => {
1382
1422
  manualCloseRef.current = false;
1383
1423
  manualOpenRef.current = false;
1384
1424
  suppressReconnectRef.current = false;
@@ -1391,7 +1431,7 @@ var useEventSource = (options) => {
1391
1431
  }));
1392
1432
  options.onOpen?.(event, source);
1393
1433
  });
1394
- const commitParsedMessage = react.useEffectEvent(
1434
+ const commitParsedMessage = useStableCallback(
1395
1435
  (eventName, event, isNamedEvent) => {
1396
1436
  try {
1397
1437
  const message = parseMessage(event);
@@ -1406,8 +1446,11 @@ var useEventSource = (options) => {
1406
1446
  return;
1407
1447
  }
1408
1448
  options.onMessage?.(message, event);
1409
- } catch {
1410
- const parseError = new Event("error");
1449
+ } catch (error) {
1450
+ const parseError = new RealtimeErrorEvent("error", {
1451
+ cause: error,
1452
+ kind: "parse-error"
1453
+ });
1411
1454
  terminalErrorRef.current = parseError;
1412
1455
  manualOpenRef.current = false;
1413
1456
  suppressReconnectRef.current = true;
@@ -1423,7 +1466,10 @@ var useEventSource = (options) => {
1423
1466
  }
1424
1467
  }
1425
1468
  );
1426
- const handleError = react.useEffectEvent((event, source) => {
1469
+ const handleError = useStableCallback((event, source, sourceEpoch) => {
1470
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1471
+ return;
1472
+ }
1427
1473
  const terminalError = terminalErrorRef.current;
1428
1474
  if (terminalError !== null) {
1429
1475
  suppressReconnectRef.current = false;
@@ -1438,12 +1484,11 @@ var useEventSource = (options) => {
1438
1484
  const skipErrorReconnect = skipErrorReconnectRef.current;
1439
1485
  skipErrorReconnectRef.current = false;
1440
1486
  const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
1441
- const readyState = source.readyState;
1442
1487
  commitState((current) => ({
1443
1488
  ...current,
1444
1489
  lastChangedAt: Date.now(),
1445
1490
  lastError: event,
1446
- status: readyState === EventSource.OPEN ? "open" : shouldReconnect ? "reconnecting" : "closed"
1491
+ status: shouldReconnect ? "reconnecting" : "closed"
1447
1492
  }));
1448
1493
  options.onError?.(event);
1449
1494
  if (!shouldReconnect) {
@@ -1451,21 +1496,18 @@ var useEventSource = (options) => {
1451
1496
  closeEventSource();
1452
1497
  return;
1453
1498
  }
1454
- if (readyState === EventSource.CLOSED) {
1455
- eventSourceRef.current = null;
1456
- eventSourceKeyRef.current = null;
1457
- reconnect.schedule("error");
1458
- }
1499
+ closeEventSource();
1500
+ reconnect.schedule("error");
1459
1501
  });
1460
- const open = () => {
1502
+ const open = useStableCallback(() => {
1461
1503
  manualCloseRef.current = false;
1462
1504
  manualOpenRef.current = true;
1463
1505
  suppressReconnectRef.current = false;
1464
1506
  terminalErrorRef.current = null;
1465
1507
  reconnect.cancel();
1466
1508
  setOpenNonce((current) => current + 1);
1467
- };
1468
- const reconnectNow = () => {
1509
+ });
1510
+ const reconnectNow = useStableCallback(() => {
1469
1511
  manualCloseRef.current = false;
1470
1512
  manualOpenRef.current = true;
1471
1513
  skipErrorReconnectRef.current = true;
@@ -1474,8 +1516,8 @@ var useEventSource = (options) => {
1474
1516
  closeEventSource();
1475
1517
  suppressReconnectRef.current = false;
1476
1518
  reconnect.schedule("manual");
1477
- };
1478
- const close = () => {
1519
+ });
1520
+ const close = useStableCallback(() => {
1479
1521
  manualCloseRef.current = true;
1480
1522
  manualOpenRef.current = false;
1481
1523
  suppressReconnectRef.current = true;
@@ -1487,7 +1529,7 @@ var useEventSource = (options) => {
1487
1529
  lastChangedAt: Date.now(),
1488
1530
  status: "closed"
1489
1531
  }));
1490
- };
1532
+ });
1491
1533
  react.useEffect(() => {
1492
1534
  if (!supported) {
1493
1535
  eventSourceKeyRef.current = null;
@@ -1506,7 +1548,7 @@ var useEventSource = (options) => {
1506
1548
  }));
1507
1549
  return;
1508
1550
  }
1509
- 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));
1510
1552
  const nextEventSourceKey = [
1511
1553
  resolvedUrl,
1512
1554
  options.withCredentials ? "credentials" : "anonymous",
@@ -1534,27 +1576,39 @@ var useEventSource = (options) => {
1534
1576
  const source = new EventSource(resolvedUrl, {
1535
1577
  withCredentials: options.withCredentials ?? false
1536
1578
  });
1579
+ const sourceEpoch = nextEventSourceEpochRef.current + 1;
1537
1580
  eventSourceRef.current = source;
1538
1581
  eventSourceKeyRef.current = nextEventSourceKey;
1582
+ activeEventSourceEpochRef.current = sourceEpoch;
1583
+ nextEventSourceEpochRef.current = sourceEpoch;
1539
1584
  commitState((current) => ({
1540
1585
  ...current,
1541
1586
  lastChangedAt: Date.now(),
1542
1587
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1543
1588
  }));
1544
1589
  const handleSourceOpen = (event) => {
1590
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1591
+ return;
1592
+ }
1545
1593
  handleOpen(event, source);
1546
1594
  };
1547
1595
  const handleSourceMessage = (event) => {
1596
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1597
+ return;
1598
+ }
1548
1599
  commitParsedMessage("message", event, false);
1549
1600
  };
1550
1601
  const namedEventHandlers = /* @__PURE__ */ new Map();
1551
1602
  const handleSourceError = (event) => {
1552
- handleError(event, source);
1603
+ handleError(event, source, sourceEpoch);
1553
1604
  };
1554
1605
  source.addEventListener("open", handleSourceOpen);
1555
1606
  source.addEventListener("message", handleSourceMessage);
1556
1607
  for (const eventName of namedEvents) {
1557
1608
  const handler = (event) => {
1609
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1610
+ return;
1611
+ }
1558
1612
  commitParsedMessage(eventName, event, true);
1559
1613
  };
1560
1614
  namedEventHandlers.set(eventName, handler);
@@ -1570,8 +1624,14 @@ var useEventSource = (options) => {
1570
1624
  source.removeEventListener("error", handleSourceError);
1571
1625
  };
1572
1626
  }, [
1627
+ closeEventSource,
1628
+ commitParsedMessage,
1629
+ commitState,
1573
1630
  connect,
1574
1631
  eventsDependency,
1632
+ handleError,
1633
+ handleOpen,
1634
+ isActiveEventSourceEvent,
1575
1635
  namedEvents,
1576
1636
  openNonce,
1577
1637
  options.withCredentials,
@@ -1582,6 +1642,7 @@ var useEventSource = (options) => {
1582
1642
  react.useEffect(() => () => {
1583
1643
  suppressReconnectRef.current = true;
1584
1644
  eventSourceKeyRef.current = null;
1645
+ activeEventSourceEpochRef.current = null;
1585
1646
  terminalErrorRef.current = null;
1586
1647
  const source = eventSourceRef.current;
1587
1648
  eventSourceRef.current = null;
@@ -1626,6 +1687,7 @@ var useEventSource = (options) => {
1626
1687
  };
1627
1688
  };
1628
1689
 
1690
+ exports.RealtimeErrorEvent = RealtimeErrorEvent;
1629
1691
  exports.useConnectionGate = useConnectionGate;
1630
1692
  exports.useEventSource = useEventSource;
1631
1693
  exports.useHeartbeat = useHeartbeat;