react-realtime-hooks 1.2.1 → 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.d.ts CHANGED
@@ -263,4 +263,15 @@ type UseEventSourceHook = <TMessage = unknown>(options: UseEventSourceOptions<TM
263
263
 
264
264
  declare const useEventSource: UseEventSourceHook;
265
265
 
266
- export { type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
266
+ type RealtimeErrorKind = "heartbeat-error" | "heartbeat-timeout" | "parse-error";
267
+ interface RealtimeErrorEventInit extends EventInit {
268
+ cause?: unknown;
269
+ kind: RealtimeErrorKind;
270
+ }
271
+ declare class RealtimeErrorEvent extends Event {
272
+ readonly cause: unknown;
273
+ readonly kind: RealtimeErrorKind;
274
+ constructor(type: string, init: RealtimeErrorEventInit);
275
+ }
276
+
277
+ export { type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, RealtimeErrorEvent, type RealtimeErrorEventInit, type RealtimeErrorKind, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
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,6 +839,10 @@ 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);
827
848
  const activeSocketEpochRef = useRef(null);
@@ -891,14 +912,19 @@ var useWebSocket = (options) => {
891
912
  heartbeatHookOptions.onTimeout = () => {
892
913
  applyHeartbeatAction(
893
914
  heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
894
- new Event("heartbeat-timeout"),
915
+ new RealtimeErrorEvent("heartbeat-timeout", {
916
+ kind: "heartbeat-timeout"
917
+ }),
895
918
  "heartbeat-timeout"
896
919
  );
897
920
  onTimeout?.();
898
921
  };
899
922
  const onError = heartbeatConfig.onError;
900
923
  heartbeatHookOptions.onError = (error) => {
901
- 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
+ });
902
928
  applyHeartbeatAction(
903
929
  heartbeatConfig.errorAction ?? heartbeatConfig.timeoutAction ?? defaultHeartbeatAction,
904
930
  event,
@@ -910,18 +936,18 @@ var useWebSocket = (options) => {
910
936
  const heartbeat = useHeartbeat(
911
937
  heartbeatHookOptions
912
938
  );
913
- const commitState = (next) => {
939
+ const commitState = useStableCallback((next) => {
914
940
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
915
941
  stateRef.current = resolved;
916
942
  setState(resolved);
917
- };
918
- const isActiveSocketEvent = useEffectEvent((socketEpoch) => {
943
+ });
944
+ const isActiveSocketEvent = useStableCallback((socketEpoch) => {
919
945
  return activeSocketEpochRef.current === socketEpoch;
920
946
  });
921
- const shouldHandleSocketClose = useEffectEvent((socketEpoch) => {
947
+ const shouldHandleSocketClose = useStableCallback((socketEpoch) => {
922
948
  return activeSocketEpochRef.current === socketEpoch || closingSocketEpochRef.current === socketEpoch;
923
949
  });
924
- const closeSocket = useEffectEvent(
950
+ const closeSocket = useStableCallback(
925
951
  (config = {}) => {
926
952
  const socket2 = socketRef.current;
927
953
  const socketEpoch = activeSocketEpochRef.current;
@@ -937,7 +963,7 @@ var useWebSocket = (options) => {
937
963
  }
938
964
  }
939
965
  );
940
- const applyHeartbeatAction = useEffectEvent(
966
+ const applyHeartbeatAction = useStableCallback(
941
967
  (action, error, reconnectTrigger) => {
942
968
  heartbeat.stop();
943
969
  options.onError?.(error);
@@ -974,17 +1000,17 @@ var useWebSocket = (options) => {
974
1000
  closeSocket({ trackClose: true });
975
1001
  }
976
1002
  );
977
- const parseMessage = useEffectEvent((event) => {
1003
+ const parseMessage = useStableCallback((event) => {
978
1004
  const parser = options.parseMessage ?? defaultParseMessage;
979
1005
  return parser(event);
980
1006
  });
981
- const updateBufferedAmount = useEffectEvent(() => {
1007
+ const updateBufferedAmount = useStableCallback(() => {
982
1008
  commitState((current) => ({
983
1009
  ...current,
984
1010
  bufferedAmount: socketRef.current?.bufferedAmount ?? 0
985
1011
  }));
986
1012
  });
987
- const handleOpen = useEffectEvent((event, socket2) => {
1013
+ const handleOpen = useStableCallback((event, socket2) => {
988
1014
  manualCloseRef.current = false;
989
1015
  manualOpenRef.current = false;
990
1016
  suppressReconnectRef.current = false;
@@ -999,7 +1025,7 @@ var useWebSocket = (options) => {
999
1025
  }));
1000
1026
  options.onOpen?.(event, socket2);
1001
1027
  });
1002
- const handleMessage = useEffectEvent((event) => {
1028
+ const handleMessage = useStableCallback((event) => {
1003
1029
  try {
1004
1030
  const message = parseMessage(event);
1005
1031
  heartbeat.notifyAck(message);
@@ -1010,8 +1036,11 @@ var useWebSocket = (options) => {
1010
1036
  lastMessageEvent: event
1011
1037
  }));
1012
1038
  options.onMessage?.(message, event);
1013
- } catch {
1014
- const parseError = new Event("error");
1039
+ } catch (error) {
1040
+ const parseError = new RealtimeErrorEvent("error", {
1041
+ cause: error,
1042
+ kind: "parse-error"
1043
+ });
1015
1044
  terminalErrorRef.current = parseError;
1016
1045
  manualOpenRef.current = false;
1017
1046
  skipCloseReconnectRef.current = true;
@@ -1032,7 +1061,7 @@ var useWebSocket = (options) => {
1032
1061
  });
1033
1062
  }
1034
1063
  });
1035
- const handleError = useEffectEvent((event, socketEpoch) => {
1064
+ const handleError = useStableCallback((event, socketEpoch) => {
1036
1065
  if (!isActiveSocketEvent(socketEpoch)) {
1037
1066
  return;
1038
1067
  }
@@ -1045,7 +1074,7 @@ var useWebSocket = (options) => {
1045
1074
  }));
1046
1075
  options.onError?.(event);
1047
1076
  });
1048
- const handleClose = useEffectEvent((event, socketEpoch) => {
1077
+ const handleClose = useStableCallback((event, socketEpoch) => {
1049
1078
  if (!shouldHandleSocketClose(socketEpoch)) {
1050
1079
  return;
1051
1080
  }
@@ -1105,15 +1134,15 @@ var useWebSocket = (options) => {
1105
1134
  suppressReconnectRef.current = false;
1106
1135
  }
1107
1136
  });
1108
- const open = () => {
1137
+ const open = useStableCallback(() => {
1109
1138
  manualCloseRef.current = false;
1110
1139
  manualOpenRef.current = true;
1111
1140
  suppressReconnectRef.current = false;
1112
1141
  terminalErrorRef.current = null;
1113
1142
  reconnect.cancel();
1114
1143
  setOpenNonce((current) => current + 1);
1115
- };
1116
- const reconnectNow = () => {
1144
+ });
1145
+ const reconnectNow = useStableCallback(() => {
1117
1146
  manualCloseRef.current = false;
1118
1147
  manualOpenRef.current = true;
1119
1148
  skipCloseReconnectRef.current = true;
@@ -1123,8 +1152,8 @@ var useWebSocket = (options) => {
1123
1152
  closeSocket();
1124
1153
  suppressReconnectRef.current = false;
1125
1154
  reconnect.schedule("manual");
1126
- };
1127
- const close = (code, reason) => {
1155
+ });
1156
+ const close = useStableCallback((code, reason) => {
1128
1157
  manualCloseRef.current = true;
1129
1158
  manualOpenRef.current = false;
1130
1159
  suppressReconnectRef.current = true;
@@ -1141,8 +1170,8 @@ var useWebSocket = (options) => {
1141
1170
  reason,
1142
1171
  trackClose: true
1143
1172
  });
1144
- };
1145
- const send = (message) => {
1173
+ });
1174
+ const send = useStableCallback((message) => {
1146
1175
  const socket2 = socketRef.current;
1147
1176
  if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
1148
1177
  return false;
@@ -1151,7 +1180,7 @@ var useWebSocket = (options) => {
1151
1180
  socket2.send(serializer(message));
1152
1181
  updateBufferedAmount();
1153
1182
  return true;
1154
- };
1183
+ });
1155
1184
  useEffect(() => {
1156
1185
  if (!supported) {
1157
1186
  socketKeyRef.current = null;
@@ -1191,7 +1220,7 @@ var useWebSocket = (options) => {
1191
1220
  if (socketRef.current !== null) {
1192
1221
  return;
1193
1222
  }
1194
- const socket2 = new WebSocket(resolvedUrl, options.protocols);
1223
+ const socket2 = new WebSocket(resolvedUrl, protocols);
1195
1224
  const socketEpoch = nextSocketEpochRef.current + 1;
1196
1225
  socketRef.current = socket2;
1197
1226
  socketKeyRef.current = nextSocketKey;
@@ -1234,9 +1263,17 @@ var useWebSocket = (options) => {
1234
1263
  socket2.removeEventListener("close", handleSocketClose);
1235
1264
  };
1236
1265
  }, [
1266
+ closeSocket,
1267
+ commitState,
1237
1268
  connect,
1269
+ handleClose,
1270
+ handleError,
1271
+ handleMessage,
1272
+ handleOpen,
1273
+ isActiveSocketEvent,
1238
1274
  openNonce,
1239
1275
  options.binaryType,
1276
+ protocols,
1240
1277
  protocolsDependency,
1241
1278
  reconnect.status,
1242
1279
  resolvedUrl,
@@ -1257,11 +1294,12 @@ var useWebSocket = (options) => {
1257
1294
  socket2.close();
1258
1295
  }
1259
1296
  }, []);
1297
+ const stopHeartbeat = heartbeat.stop;
1260
1298
  useEffect(() => {
1261
1299
  if (state.status !== "open") {
1262
- heartbeat.stop();
1300
+ stopHeartbeat();
1263
1301
  }
1264
- }, [state.status]);
1302
+ }, [state.status, stopHeartbeat]);
1265
1303
  const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1266
1304
  const snapshot = createConnectionStateSnapshot(status, {
1267
1305
  isSupported: supported,
@@ -1317,29 +1355,27 @@ var createInitialState4 = (status = "idle") => ({
1317
1355
  status
1318
1356
  });
1319
1357
  var defaultParseMessage2 = (event) => event.data;
1320
- var toEventsDependency = (events) => {
1321
- if (events === void 0 || events.length === 0) {
1322
- return "";
1323
- }
1324
- return [...new Set(events)].sort().join("|");
1325
- };
1326
1358
  var normalizeNamedEvents = (events) => {
1327
1359
  if (events === void 0 || events.length === 0) {
1328
1360
  return [];
1329
1361
  }
1330
- return [...new Set(events)].filter((eventName) => eventName !== "message");
1362
+ return [...new Set(events)].filter((eventName) => eventName !== "message").sort();
1331
1363
  };
1364
+ var toEventsDependency = (events) => JSON.stringify(normalizeNamedEvents(events));
1365
+ var parseEventsDependency = (eventsDependency) => JSON.parse(eventsDependency);
1332
1366
  var useEventSource = (options) => {
1333
1367
  const connect = options.connect ?? true;
1334
1368
  const supported = isEventSourceSupported();
1335
1369
  const resolvedUrl = useMemo(() => resolveUrlProvider(options.url), [options.url]);
1336
1370
  const eventsDependency = toEventsDependency(options.events);
1337
1371
  const namedEvents = useMemo(
1338
- () => normalizeNamedEvents(options.events),
1372
+ () => parseEventsDependency(eventsDependency),
1339
1373
  [eventsDependency]
1340
1374
  );
1341
1375
  const eventSourceRef = useRef(null);
1342
1376
  const eventSourceKeyRef = useRef(null);
1377
+ const activeEventSourceEpochRef = useRef(null);
1378
+ const nextEventSourceEpochRef = useRef(0);
1343
1379
  const manualCloseRef = useRef(false);
1344
1380
  const manualOpenRef = useRef(false);
1345
1381
  const skipErrorReconnectRef = useRef(false);
@@ -1358,25 +1394,29 @@ var useEventSource = (options) => {
1358
1394
  enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
1359
1395
  }
1360
1396
  );
1361
- const commitState = (next) => {
1397
+ const commitState = useStableCallback((next) => {
1362
1398
  const resolved = typeof next === "function" ? next(stateRef.current) : next;
1363
1399
  stateRef.current = resolved;
1364
1400
  setState(resolved);
1365
- };
1366
- const closeEventSource = useEffectEvent(() => {
1401
+ });
1402
+ const closeEventSource = useStableCallback(() => {
1367
1403
  const source = eventSourceRef.current;
1368
1404
  if (source === null) {
1369
1405
  return;
1370
1406
  }
1371
1407
  eventSourceRef.current = null;
1372
1408
  eventSourceKeyRef.current = null;
1409
+ activeEventSourceEpochRef.current = null;
1373
1410
  source.close();
1374
1411
  });
1375
- const parseMessage = useEffectEvent((event) => {
1412
+ const isActiveEventSourceEvent = useStableCallback((sourceEpoch) => {
1413
+ return activeEventSourceEpochRef.current === sourceEpoch;
1414
+ });
1415
+ const parseMessage = useStableCallback((event) => {
1376
1416
  const parser = options.parseMessage ?? defaultParseMessage2;
1377
1417
  return parser(event);
1378
1418
  });
1379
- const handleOpen = useEffectEvent((event, source) => {
1419
+ const handleOpen = useStableCallback((event, source) => {
1380
1420
  manualCloseRef.current = false;
1381
1421
  manualOpenRef.current = false;
1382
1422
  suppressReconnectRef.current = false;
@@ -1389,7 +1429,7 @@ var useEventSource = (options) => {
1389
1429
  }));
1390
1430
  options.onOpen?.(event, source);
1391
1431
  });
1392
- const commitParsedMessage = useEffectEvent(
1432
+ const commitParsedMessage = useStableCallback(
1393
1433
  (eventName, event, isNamedEvent) => {
1394
1434
  try {
1395
1435
  const message = parseMessage(event);
@@ -1404,8 +1444,11 @@ var useEventSource = (options) => {
1404
1444
  return;
1405
1445
  }
1406
1446
  options.onMessage?.(message, event);
1407
- } catch {
1408
- const parseError = new Event("error");
1447
+ } catch (error) {
1448
+ const parseError = new RealtimeErrorEvent("error", {
1449
+ cause: error,
1450
+ kind: "parse-error"
1451
+ });
1409
1452
  terminalErrorRef.current = parseError;
1410
1453
  manualOpenRef.current = false;
1411
1454
  suppressReconnectRef.current = true;
@@ -1421,7 +1464,10 @@ var useEventSource = (options) => {
1421
1464
  }
1422
1465
  }
1423
1466
  );
1424
- const handleError = useEffectEvent((event, source) => {
1467
+ const handleError = useStableCallback((event, source, sourceEpoch) => {
1468
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1469
+ return;
1470
+ }
1425
1471
  const terminalError = terminalErrorRef.current;
1426
1472
  if (terminalError !== null) {
1427
1473
  suppressReconnectRef.current = false;
@@ -1436,12 +1482,11 @@ var useEventSource = (options) => {
1436
1482
  const skipErrorReconnect = skipErrorReconnectRef.current;
1437
1483
  skipErrorReconnectRef.current = false;
1438
1484
  const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
1439
- const readyState = source.readyState;
1440
1485
  commitState((current) => ({
1441
1486
  ...current,
1442
1487
  lastChangedAt: Date.now(),
1443
1488
  lastError: event,
1444
- status: readyState === EventSource.OPEN ? "open" : shouldReconnect ? "reconnecting" : "closed"
1489
+ status: shouldReconnect ? "reconnecting" : "closed"
1445
1490
  }));
1446
1491
  options.onError?.(event);
1447
1492
  if (!shouldReconnect) {
@@ -1449,21 +1494,18 @@ var useEventSource = (options) => {
1449
1494
  closeEventSource();
1450
1495
  return;
1451
1496
  }
1452
- if (readyState === EventSource.CLOSED) {
1453
- eventSourceRef.current = null;
1454
- eventSourceKeyRef.current = null;
1455
- reconnect.schedule("error");
1456
- }
1497
+ closeEventSource();
1498
+ reconnect.schedule("error");
1457
1499
  });
1458
- const open = () => {
1500
+ const open = useStableCallback(() => {
1459
1501
  manualCloseRef.current = false;
1460
1502
  manualOpenRef.current = true;
1461
1503
  suppressReconnectRef.current = false;
1462
1504
  terminalErrorRef.current = null;
1463
1505
  reconnect.cancel();
1464
1506
  setOpenNonce((current) => current + 1);
1465
- };
1466
- const reconnectNow = () => {
1507
+ });
1508
+ const reconnectNow = useStableCallback(() => {
1467
1509
  manualCloseRef.current = false;
1468
1510
  manualOpenRef.current = true;
1469
1511
  skipErrorReconnectRef.current = true;
@@ -1472,8 +1514,8 @@ var useEventSource = (options) => {
1472
1514
  closeEventSource();
1473
1515
  suppressReconnectRef.current = false;
1474
1516
  reconnect.schedule("manual");
1475
- };
1476
- const close = () => {
1517
+ });
1518
+ const close = useStableCallback(() => {
1477
1519
  manualCloseRef.current = true;
1478
1520
  manualOpenRef.current = false;
1479
1521
  suppressReconnectRef.current = true;
@@ -1485,7 +1527,7 @@ var useEventSource = (options) => {
1485
1527
  lastChangedAt: Date.now(),
1486
1528
  status: "closed"
1487
1529
  }));
1488
- };
1530
+ });
1489
1531
  useEffect(() => {
1490
1532
  if (!supported) {
1491
1533
  eventSourceKeyRef.current = null;
@@ -1504,7 +1546,7 @@ var useEventSource = (options) => {
1504
1546
  }));
1505
1547
  return;
1506
1548
  }
1507
- 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));
1508
1550
  const nextEventSourceKey = [
1509
1551
  resolvedUrl,
1510
1552
  options.withCredentials ? "credentials" : "anonymous",
@@ -1532,27 +1574,39 @@ var useEventSource = (options) => {
1532
1574
  const source = new EventSource(resolvedUrl, {
1533
1575
  withCredentials: options.withCredentials ?? false
1534
1576
  });
1577
+ const sourceEpoch = nextEventSourceEpochRef.current + 1;
1535
1578
  eventSourceRef.current = source;
1536
1579
  eventSourceKeyRef.current = nextEventSourceKey;
1580
+ activeEventSourceEpochRef.current = sourceEpoch;
1581
+ nextEventSourceEpochRef.current = sourceEpoch;
1537
1582
  commitState((current) => ({
1538
1583
  ...current,
1539
1584
  lastChangedAt: Date.now(),
1540
1585
  status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1541
1586
  }));
1542
1587
  const handleSourceOpen = (event) => {
1588
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1589
+ return;
1590
+ }
1543
1591
  handleOpen(event, source);
1544
1592
  };
1545
1593
  const handleSourceMessage = (event) => {
1594
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1595
+ return;
1596
+ }
1546
1597
  commitParsedMessage("message", event, false);
1547
1598
  };
1548
1599
  const namedEventHandlers = /* @__PURE__ */ new Map();
1549
1600
  const handleSourceError = (event) => {
1550
- handleError(event, source);
1601
+ handleError(event, source, sourceEpoch);
1551
1602
  };
1552
1603
  source.addEventListener("open", handleSourceOpen);
1553
1604
  source.addEventListener("message", handleSourceMessage);
1554
1605
  for (const eventName of namedEvents) {
1555
1606
  const handler = (event) => {
1607
+ if (!isActiveEventSourceEvent(sourceEpoch)) {
1608
+ return;
1609
+ }
1556
1610
  commitParsedMessage(eventName, event, true);
1557
1611
  };
1558
1612
  namedEventHandlers.set(eventName, handler);
@@ -1568,8 +1622,14 @@ var useEventSource = (options) => {
1568
1622
  source.removeEventListener("error", handleSourceError);
1569
1623
  };
1570
1624
  }, [
1625
+ closeEventSource,
1626
+ commitParsedMessage,
1627
+ commitState,
1571
1628
  connect,
1572
1629
  eventsDependency,
1630
+ handleError,
1631
+ handleOpen,
1632
+ isActiveEventSourceEvent,
1573
1633
  namedEvents,
1574
1634
  openNonce,
1575
1635
  options.withCredentials,
@@ -1580,6 +1640,7 @@ var useEventSource = (options) => {
1580
1640
  useEffect(() => () => {
1581
1641
  suppressReconnectRef.current = true;
1582
1642
  eventSourceKeyRef.current = null;
1643
+ activeEventSourceEpochRef.current = null;
1583
1644
  terminalErrorRef.current = null;
1584
1645
  const source = eventSourceRef.current;
1585
1646
  eventSourceRef.current = null;
@@ -1624,6 +1685,6 @@ var useEventSource = (options) => {
1624
1685
  };
1625
1686
  };
1626
1687
 
1627
- export { useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
1688
+ export { RealtimeErrorEvent, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
1628
1689
  //# sourceMappingURL=index.js.map
1629
1690
  //# sourceMappingURL=index.js.map