react-realtime-hooks 1.0.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.js ADDED
@@ -0,0 +1,1239 @@
1
+ import { useSyncExternalStore, useRef, useState, useEffect, useEffectEvent, useMemo, startTransition } from 'react';
2
+
3
+ // src/hooks/useOnlineStatus.ts
4
+
5
+ // src/core/env.ts
6
+ var isWebSocketSupported = () => typeof WebSocket !== "undefined";
7
+ var isEventSourceSupported = () => typeof EventSource !== "undefined";
8
+ var hasNavigatorOnLineSupport = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean";
9
+ var readOnlineStatus = (initialOnline = true) => {
10
+ if (!hasNavigatorOnLineSupport()) {
11
+ return {
12
+ isOnline: initialOnline,
13
+ isSupported: false
14
+ };
15
+ }
16
+ return {
17
+ isOnline: navigator.onLine,
18
+ isSupported: true
19
+ };
20
+ };
21
+
22
+ // src/hooks/useOnlineStatus.ts
23
+ var subscribeToOnlineStatus = (onStoreChange) => {
24
+ if (typeof window === "undefined") {
25
+ return () => {
26
+ };
27
+ }
28
+ window.addEventListener("online", onStoreChange);
29
+ window.addEventListener("offline", onStoreChange);
30
+ return () => {
31
+ window.removeEventListener("online", onStoreChange);
32
+ window.removeEventListener("offline", onStoreChange);
33
+ };
34
+ };
35
+ var createEmptyTransitionState = () => ({
36
+ lastChangedAt: null,
37
+ wentOfflineAt: null,
38
+ wentOnlineAt: null
39
+ });
40
+ var useOnlineStatus = (options = {}) => {
41
+ const initialOnline = options.initialOnline ?? true;
42
+ const trackTransitions = options.trackTransitions ?? true;
43
+ const isOnline = useSyncExternalStore(
44
+ subscribeToOnlineStatus,
45
+ () => readOnlineStatus(initialOnline).isOnline,
46
+ () => initialOnline
47
+ );
48
+ const previousOnlineRef = useRef(isOnline);
49
+ const [transitions, setTransitions] = useState(createEmptyTransitionState);
50
+ useEffect(() => {
51
+ if (!trackTransitions) {
52
+ previousOnlineRef.current = isOnline;
53
+ setTransitions(createEmptyTransitionState);
54
+ return;
55
+ }
56
+ if (previousOnlineRef.current === isOnline) {
57
+ return;
58
+ }
59
+ const changedAt = Date.now();
60
+ previousOnlineRef.current = isOnline;
61
+ setTransitions((current) => ({
62
+ lastChangedAt: changedAt,
63
+ wentOfflineAt: isOnline ? current.wentOfflineAt : changedAt,
64
+ wentOnlineAt: isOnline ? changedAt : current.wentOnlineAt
65
+ }));
66
+ }, [isOnline, trackTransitions]);
67
+ return {
68
+ isOnline,
69
+ isSupported: hasNavigatorOnLineSupport(),
70
+ ...transitions
71
+ };
72
+ };
73
+
74
+ // src/core/reconnect.ts
75
+ var DEFAULT_RECONNECT_OPTIONS = {
76
+ backoffFactor: 2,
77
+ enabled: true,
78
+ initialDelayMs: 1e3,
79
+ jitterRatio: 0.2,
80
+ maxDelayMs: 3e4,
81
+ resetOnSuccess: true
82
+ };
83
+ var clampNumber = (value, min, max) => Math.min(max, Math.max(min, value));
84
+ var sanitizeDelay = (value) => {
85
+ if (!Number.isFinite(value)) {
86
+ return 0;
87
+ }
88
+ return Math.max(0, Math.round(value));
89
+ };
90
+ var sanitizeBackoffFactor = (value) => {
91
+ if (!Number.isFinite(value)) {
92
+ return DEFAULT_RECONNECT_OPTIONS.backoffFactor;
93
+ }
94
+ return Math.max(1, value);
95
+ };
96
+ var sanitizeMaxAttempts = (value) => {
97
+ if (value === null || value === void 0) {
98
+ return null;
99
+ }
100
+ if (!Number.isFinite(value)) {
101
+ return null;
102
+ }
103
+ return Math.max(0, Math.floor(value));
104
+ };
105
+ var sanitizeJitterRatio = (value) => {
106
+ if (!Number.isFinite(value)) {
107
+ return DEFAULT_RECONNECT_OPTIONS.jitterRatio;
108
+ }
109
+ return clampNumber(value, 0, 1);
110
+ };
111
+ var normalizeReconnectOptions = (options) => {
112
+ if (options === false) {
113
+ return null;
114
+ }
115
+ const initialDelayMs = sanitizeDelay(
116
+ options?.initialDelayMs ?? DEFAULT_RECONNECT_OPTIONS.initialDelayMs
117
+ );
118
+ const maxDelayMs = Math.max(
119
+ initialDelayMs,
120
+ sanitizeDelay(options?.maxDelayMs ?? DEFAULT_RECONNECT_OPTIONS.maxDelayMs)
121
+ );
122
+ const normalized = {
123
+ backoffFactor: sanitizeBackoffFactor(
124
+ options?.backoffFactor ?? DEFAULT_RECONNECT_OPTIONS.backoffFactor
125
+ ),
126
+ enabled: options?.enabled ?? DEFAULT_RECONNECT_OPTIONS.enabled,
127
+ initialDelayMs,
128
+ jitterRatio: sanitizeJitterRatio(
129
+ options?.jitterRatio ?? DEFAULT_RECONNECT_OPTIONS.jitterRatio
130
+ ),
131
+ maxAttempts: sanitizeMaxAttempts(options?.maxAttempts),
132
+ maxDelayMs,
133
+ resetOnSuccess: options?.resetOnSuccess ?? DEFAULT_RECONNECT_OPTIONS.resetOnSuccess
134
+ };
135
+ if (options?.getDelayMs !== void 0) {
136
+ normalized.getDelayMs = options.getDelayMs;
137
+ }
138
+ if (options?.onCancel !== void 0) {
139
+ normalized.onCancel = options.onCancel;
140
+ }
141
+ if (options?.onReset !== void 0) {
142
+ normalized.onReset = options.onReset;
143
+ }
144
+ if (options?.onSchedule !== void 0) {
145
+ normalized.onSchedule = options.onSchedule;
146
+ }
147
+ return normalized;
148
+ };
149
+ var createReconnectDelayContext = (attempt, options, lastDelayMs) => ({
150
+ attempt: Math.max(1, Math.floor(attempt)),
151
+ backoffFactor: options.backoffFactor,
152
+ initialDelayMs: options.initialDelayMs,
153
+ jitterRatio: options.jitterRatio,
154
+ lastDelayMs,
155
+ maxDelayMs: options.maxDelayMs
156
+ });
157
+ var defaultReconnectDelayStrategy = (context) => {
158
+ const exponent = Math.max(0, context.attempt - 1);
159
+ const baseDelay = context.initialDelayMs * context.backoffFactor ** exponent;
160
+ return Math.min(context.maxDelayMs, sanitizeDelay(baseDelay));
161
+ };
162
+ var applyJitterToDelay = (delayMs, jitterRatio, random = Math.random) => {
163
+ if (delayMs === 0 || jitterRatio === 0) {
164
+ return delayMs;
165
+ }
166
+ const safeRandom = clampNumber(random(), 0, 1);
167
+ const variance = delayMs * jitterRatio;
168
+ const offset = (safeRandom * 2 - 1) * variance;
169
+ return sanitizeDelay(delayMs + offset);
170
+ };
171
+ var calculateReconnectDelay = (context, options = {}) => {
172
+ const baseDelay = sanitizeDelay(
173
+ (options.strategy ?? defaultReconnectDelayStrategy)(context)
174
+ );
175
+ const cappedDelay = Math.min(context.maxDelayMs, baseDelay);
176
+ return Math.min(
177
+ context.maxDelayMs,
178
+ applyJitterToDelay(cappedDelay, context.jitterRatio, options.random)
179
+ );
180
+ };
181
+ var canScheduleReconnectAttempt = (attempt, options) => {
182
+ if (!options.enabled) {
183
+ return false;
184
+ }
185
+ if (attempt < 1) {
186
+ return false;
187
+ }
188
+ return options.maxAttempts === null || attempt <= options.maxAttempts;
189
+ };
190
+ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {}) => {
191
+ if (!canScheduleReconnectAttempt(attempt, options)) {
192
+ return null;
193
+ }
194
+ const context = createReconnectDelayContext(attempt, options, lastDelayMs);
195
+ const calculationOptions = {};
196
+ if (config.random !== void 0) {
197
+ calculationOptions.random = config.random;
198
+ }
199
+ if (options.getDelayMs !== void 0) {
200
+ calculationOptions.strategy = options.getDelayMs;
201
+ }
202
+ return {
203
+ attempt,
204
+ delayMs: calculateReconnectDelay(context, calculationOptions),
205
+ scheduledAt: config.now ?? Date.now(),
206
+ trigger
207
+ };
208
+ };
209
+
210
+ // src/core/timers.ts
211
+ var sanitizeTimerDelay = (delayMs) => {
212
+ if (!Number.isFinite(delayMs)) {
213
+ return 0;
214
+ }
215
+ return Math.max(0, Math.round(delayMs));
216
+ };
217
+ var createManagedTimeout = () => {
218
+ let timeoutId = null;
219
+ return {
220
+ cancel() {
221
+ if (timeoutId !== null) {
222
+ clearTimeout(timeoutId);
223
+ timeoutId = null;
224
+ }
225
+ },
226
+ isActive() {
227
+ return timeoutId !== null;
228
+ },
229
+ schedule(callback, delayMs) {
230
+ if (timeoutId !== null) {
231
+ clearTimeout(timeoutId);
232
+ }
233
+ timeoutId = setTimeout(() => {
234
+ timeoutId = null;
235
+ callback();
236
+ }, sanitizeTimerDelay(delayMs));
237
+ }
238
+ };
239
+ };
240
+ var createManagedInterval = () => {
241
+ let intervalId = null;
242
+ return {
243
+ cancel() {
244
+ if (intervalId !== null) {
245
+ clearInterval(intervalId);
246
+ intervalId = null;
247
+ }
248
+ },
249
+ isActive() {
250
+ return intervalId !== null;
251
+ },
252
+ start(callback, intervalMs) {
253
+ if (intervalId !== null) {
254
+ clearInterval(intervalId);
255
+ }
256
+ intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
257
+ }
258
+ };
259
+ };
260
+
261
+ // src/hooks/useReconnect.ts
262
+ var createInitialState = (enabled) => ({
263
+ attempt: 0,
264
+ nextDelayMs: null,
265
+ status: enabled ? "idle" : "stopped"
266
+ });
267
+ var useReconnect = (options = {}) => {
268
+ const normalizedOptions = normalizeReconnectOptions(options) ?? normalizeReconnectOptions();
269
+ const timeoutRef = useRef(createManagedTimeout());
270
+ const lastDelayRef = useRef(null);
271
+ const [state, setState] = useState(
272
+ () => createInitialState(normalizedOptions.enabled)
273
+ );
274
+ const stateRef = useRef(state);
275
+ stateRef.current = state;
276
+ const commitState = (next) => {
277
+ const resolved = typeof next === "function" ? next(stateRef.current) : next;
278
+ stateRef.current = resolved;
279
+ startTransition(() => {
280
+ setState(resolved);
281
+ });
282
+ };
283
+ const runAttempt = useEffectEvent((attempt) => {
284
+ commitState({
285
+ attempt,
286
+ nextDelayMs: null,
287
+ status: "running"
288
+ });
289
+ });
290
+ const emitSchedule = useEffectEvent((attempt) => {
291
+ normalizedOptions.onSchedule?.(attempt);
292
+ });
293
+ const emitCancel = useEffectEvent(() => {
294
+ normalizedOptions.onCancel?.();
295
+ });
296
+ const emitReset = useEffectEvent(() => {
297
+ normalizedOptions.onReset?.();
298
+ });
299
+ useEffect(() => {
300
+ if (!normalizedOptions.enabled) {
301
+ timeoutRef.current.cancel();
302
+ commitState((current) => ({
303
+ ...current,
304
+ nextDelayMs: null,
305
+ status: "stopped"
306
+ }));
307
+ return;
308
+ }
309
+ commitState(
310
+ (current) => current.status === "stopped" ? {
311
+ ...current,
312
+ status: "idle"
313
+ } : current
314
+ );
315
+ }, [normalizedOptions.enabled]);
316
+ useEffect(() => () => {
317
+ timeoutRef.current.cancel();
318
+ }, []);
319
+ const schedule = (trigger = "manual") => {
320
+ const current = stateRef.current;
321
+ const nextAttempt = current.attempt + 1;
322
+ const attempt = createReconnectAttempt(
323
+ nextAttempt,
324
+ trigger,
325
+ normalizedOptions,
326
+ lastDelayRef.current
327
+ );
328
+ timeoutRef.current.cancel();
329
+ if (attempt === null) {
330
+ commitState((snapshot) => ({
331
+ ...snapshot,
332
+ nextDelayMs: null,
333
+ status: "stopped"
334
+ }));
335
+ return;
336
+ }
337
+ lastDelayRef.current = attempt.delayMs;
338
+ timeoutRef.current.schedule(() => {
339
+ runAttempt(attempt.attempt);
340
+ }, attempt.delayMs);
341
+ commitState({
342
+ attempt: attempt.attempt,
343
+ nextDelayMs: attempt.delayMs,
344
+ status: "scheduled"
345
+ });
346
+ emitSchedule(attempt);
347
+ };
348
+ const cancel = () => {
349
+ const current = stateRef.current;
350
+ const shouldEmitCancel = timeoutRef.current.isActive() || current.status === "scheduled" || current.status === "running";
351
+ timeoutRef.current.cancel();
352
+ commitState((snapshot) => ({
353
+ ...snapshot,
354
+ nextDelayMs: null,
355
+ status: "stopped"
356
+ }));
357
+ if (shouldEmitCancel) {
358
+ emitCancel();
359
+ }
360
+ };
361
+ const reset = () => {
362
+ timeoutRef.current.cancel();
363
+ lastDelayRef.current = null;
364
+ commitState(createInitialState(normalizedOptions.enabled));
365
+ emitReset();
366
+ };
367
+ const markConnected = () => {
368
+ timeoutRef.current.cancel();
369
+ if (normalizedOptions.resetOnSuccess) {
370
+ lastDelayRef.current = null;
371
+ commitState(createInitialState(normalizedOptions.enabled));
372
+ emitReset();
373
+ return;
374
+ }
375
+ commitState((current) => ({
376
+ ...current,
377
+ nextDelayMs: null,
378
+ status: normalizedOptions.enabled ? "idle" : "stopped"
379
+ }));
380
+ };
381
+ return {
382
+ attempt: state.attempt,
383
+ cancel,
384
+ isActive: state.status === "scheduled" || state.status === "running",
385
+ isScheduled: state.status === "scheduled",
386
+ markConnected,
387
+ nextDelayMs: state.nextDelayMs,
388
+ reset,
389
+ schedule,
390
+ status: state.status
391
+ };
392
+ };
393
+ var createInitialState2 = (isRunning) => ({
394
+ hasTimedOut: false,
395
+ isRunning,
396
+ lastAckAt: null,
397
+ lastBeatAt: null,
398
+ latencyMs: null
399
+ });
400
+ var useHeartbeat = (options) => {
401
+ const enabled = options.enabled ?? true;
402
+ const startOnMount = options.startOnMount ?? true;
403
+ const intervalRef = useRef(createManagedInterval());
404
+ const timeoutRef = useRef(createManagedTimeout());
405
+ const [state, setState] = useState(
406
+ () => createInitialState2(enabled && startOnMount)
407
+ );
408
+ const stateRef = useRef(state);
409
+ stateRef.current = state;
410
+ const commitState = (next) => {
411
+ const resolved = typeof next === "function" ? next(stateRef.current) : next;
412
+ stateRef.current = resolved;
413
+ setState(resolved);
414
+ };
415
+ const handleTimeout = useEffectEvent(() => {
416
+ commitState((current) => ({
417
+ ...current,
418
+ hasTimedOut: true
419
+ }));
420
+ options.onTimeout?.();
421
+ });
422
+ const scheduleTimeout = useEffectEvent(() => {
423
+ if (options.timeoutMs === void 0) {
424
+ timeoutRef.current.cancel();
425
+ return;
426
+ }
427
+ timeoutRef.current.schedule(() => {
428
+ handleTimeout();
429
+ }, options.timeoutMs);
430
+ });
431
+ const runBeat = useEffectEvent(() => {
432
+ const performedAt = Date.now();
433
+ commitState((current) => ({
434
+ ...current,
435
+ hasTimedOut: false,
436
+ lastBeatAt: performedAt
437
+ }));
438
+ scheduleTimeout();
439
+ options.onBeat?.();
440
+ void options.beat?.();
441
+ });
442
+ const start = () => {
443
+ if (!enabled) {
444
+ return;
445
+ }
446
+ if (!intervalRef.current.isActive()) {
447
+ intervalRef.current.start(() => {
448
+ runBeat();
449
+ }, options.intervalMs);
450
+ }
451
+ commitState((current) => ({
452
+ ...current,
453
+ hasTimedOut: false,
454
+ isRunning: true
455
+ }));
456
+ };
457
+ const stop = () => {
458
+ intervalRef.current.cancel();
459
+ timeoutRef.current.cancel();
460
+ commitState((current) => ({
461
+ ...current,
462
+ hasTimedOut: false,
463
+ isRunning: false
464
+ }));
465
+ };
466
+ const beat = () => {
467
+ if (!enabled) {
468
+ return;
469
+ }
470
+ runBeat();
471
+ };
472
+ const notifyAck = (message) => {
473
+ if (stateRef.current.lastBeatAt === null) {
474
+ return false;
475
+ }
476
+ const matchesAck = options.matchesAck === void 0 || options.matchesAck(message);
477
+ if (!matchesAck) {
478
+ return false;
479
+ }
480
+ const acknowledgedAt = Date.now();
481
+ timeoutRef.current.cancel();
482
+ commitState((current) => ({
483
+ ...current,
484
+ hasTimedOut: false,
485
+ lastAckAt: acknowledgedAt,
486
+ latencyMs: current.lastBeatAt === null ? null : acknowledgedAt - current.lastBeatAt
487
+ }));
488
+ return true;
489
+ };
490
+ useEffect(() => {
491
+ if (!enabled) {
492
+ stop();
493
+ return;
494
+ }
495
+ if (startOnMount) {
496
+ start();
497
+ return stop;
498
+ }
499
+ stop();
500
+ return void 0;
501
+ }, [enabled, options.intervalMs, startOnMount]);
502
+ useEffect(() => () => {
503
+ intervalRef.current.cancel();
504
+ timeoutRef.current.cancel();
505
+ }, []);
506
+ return {
507
+ beat,
508
+ hasTimedOut: state.hasTimedOut,
509
+ isRunning: state.isRunning,
510
+ lastAckAt: state.lastAckAt,
511
+ lastBeatAt: state.lastBeatAt,
512
+ latencyMs: state.latencyMs,
513
+ notifyAck,
514
+ start,
515
+ stop
516
+ };
517
+ };
518
+
519
+ // src/core/connection-state.ts
520
+ var createConnectionStateSnapshot = (status, config = {}) => {
521
+ const base = {
522
+ isSupported: config.isSupported ?? true,
523
+ lastChangedAt: config.lastChangedAt ?? null
524
+ };
525
+ switch (status) {
526
+ case "open":
527
+ return {
528
+ ...base,
529
+ isClosed: false,
530
+ isConnected: true,
531
+ isConnecting: false,
532
+ status
533
+ };
534
+ case "connecting":
535
+ case "reconnecting":
536
+ return {
537
+ ...base,
538
+ isClosed: false,
539
+ isConnected: false,
540
+ isConnecting: true,
541
+ status
542
+ };
543
+ case "closing":
544
+ return {
545
+ ...base,
546
+ isClosed: false,
547
+ isConnected: false,
548
+ isConnecting: false,
549
+ status
550
+ };
551
+ case "idle":
552
+ case "closed":
553
+ case "error":
554
+ return {
555
+ ...base,
556
+ isClosed: true,
557
+ isConnected: false,
558
+ isConnecting: false,
559
+ status
560
+ };
561
+ }
562
+ };
563
+
564
+ // src/core/url.ts
565
+ var normalizeResolvedUrl = (value) => {
566
+ if (value === null) {
567
+ return null;
568
+ }
569
+ if (value instanceof URL) {
570
+ return value.toString();
571
+ }
572
+ const trimmed = value.trim();
573
+ return trimmed.length > 0 ? trimmed : null;
574
+ };
575
+ var resolveUrlProvider = (url) => {
576
+ const resolved = typeof url === "function" ? url() : url;
577
+ return normalizeResolvedUrl(resolved ?? null);
578
+ };
579
+
580
+ // src/hooks/useWebSocket.ts
581
+ var createInitialState3 = (status = "idle") => ({
582
+ bufferedAmount: 0,
583
+ lastChangedAt: null,
584
+ lastCloseEvent: null,
585
+ lastError: null,
586
+ lastMessage: null,
587
+ lastMessageEvent: null,
588
+ status
589
+ });
590
+ var defaultParseMessage = (event) => event.data;
591
+ var defaultSerializeMessage = (message) => {
592
+ if (typeof message === "string" || message instanceof Blob || message instanceof ArrayBuffer) {
593
+ return message;
594
+ }
595
+ if (ArrayBuffer.isView(message)) {
596
+ return message;
597
+ }
598
+ return JSON.stringify(message);
599
+ };
600
+ var resolveFactoryValue = (value) => typeof value === "function" ? value() : value;
601
+ var toProtocolsDependency = (protocols) => {
602
+ if (protocols === void 0) {
603
+ return "";
604
+ }
605
+ return Array.isArray(protocols) ? protocols.join("|") : protocols;
606
+ };
607
+ var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
608
+ var useWebSocket = (options) => {
609
+ const connect = options.connect ?? true;
610
+ const supported = isWebSocketSupported();
611
+ const resolvedUrl = useMemo(() => resolveUrlProvider(options.url), [options.url]);
612
+ const protocolsDependency = toProtocolsDependency(options.protocols);
613
+ const socketRef = useRef(null);
614
+ const socketKeyRef = useRef(null);
615
+ const manualCloseRef = useRef(false);
616
+ const manualOpenRef = useRef(false);
617
+ const skipCloseReconnectRef = useRef(false);
618
+ const suppressReconnectRef = useRef(false);
619
+ const [openNonce, setOpenNonce] = useState(0);
620
+ const [state, setState] = useState(
621
+ () => createInitialState3(connect ? "connecting" : "idle")
622
+ );
623
+ const stateRef = useRef(state);
624
+ stateRef.current = state;
625
+ const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
626
+ const reconnect = useReconnect(
627
+ options.reconnect === false ? { enabled: false } : {
628
+ ...options.reconnect,
629
+ enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
630
+ }
631
+ );
632
+ const heartbeatEnabled = options.heartbeat !== false && supported && resolvedUrl !== null;
633
+ const heartbeatConfig = toHeartbeatConfig(
634
+ options.heartbeat
635
+ );
636
+ const heartbeatHookOptions = heartbeatConfig === null ? {
637
+ enabled: false,
638
+ intervalMs: 1e3,
639
+ startOnMount: false
640
+ } : {
641
+ beat: () => {
642
+ const socket2 = socketRef.current;
643
+ if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
644
+ return false;
645
+ }
646
+ const heartbeatMessage = heartbeatConfig.message;
647
+ if (heartbeatMessage !== void 0) {
648
+ const serialized = (options.serializeMessage ?? defaultSerializeMessage)(
649
+ resolveFactoryValue(heartbeatMessage)
650
+ );
651
+ socket2.send(serialized);
652
+ }
653
+ return heartbeatConfig.beat?.() ?? true;
654
+ },
655
+ enabled: heartbeatEnabled && (heartbeatConfig.enabled ?? true),
656
+ intervalMs: heartbeatConfig.intervalMs,
657
+ startOnMount: false
658
+ };
659
+ if (heartbeatConfig !== null && heartbeatConfig.timeoutMs !== void 0) {
660
+ heartbeatHookOptions.timeoutMs = heartbeatConfig.timeoutMs;
661
+ }
662
+ if (heartbeatConfig !== null && heartbeatConfig.matchesAck !== void 0) {
663
+ heartbeatHookOptions.matchesAck = heartbeatConfig.matchesAck;
664
+ }
665
+ if (heartbeatConfig !== null && heartbeatConfig.onBeat !== void 0) {
666
+ heartbeatHookOptions.onBeat = heartbeatConfig.onBeat;
667
+ }
668
+ if (heartbeatConfig !== null && heartbeatConfig.onTimeout !== void 0) {
669
+ heartbeatHookOptions.onTimeout = heartbeatConfig.onTimeout;
670
+ }
671
+ const heartbeat = useHeartbeat(
672
+ heartbeatHookOptions
673
+ );
674
+ const commitState = (next) => {
675
+ const resolved = typeof next === "function" ? next(stateRef.current) : next;
676
+ stateRef.current = resolved;
677
+ setState(resolved);
678
+ };
679
+ const closeSocket = useEffectEvent((code, reason) => {
680
+ const socket2 = socketRef.current;
681
+ if (socket2 === null) {
682
+ return;
683
+ }
684
+ socketRef.current = null;
685
+ socketKeyRef.current = null;
686
+ if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
687
+ socket2.close(code, reason);
688
+ }
689
+ });
690
+ const parseMessage = useEffectEvent((event) => {
691
+ const parser = options.parseMessage ?? defaultParseMessage;
692
+ return parser(event);
693
+ });
694
+ const updateBufferedAmount = useEffectEvent(() => {
695
+ commitState((current) => ({
696
+ ...current,
697
+ bufferedAmount: socketRef.current?.bufferedAmount ?? 0
698
+ }));
699
+ });
700
+ const handleOpen = useEffectEvent((event, socket2) => {
701
+ manualCloseRef.current = false;
702
+ suppressReconnectRef.current = false;
703
+ reconnect.markConnected();
704
+ heartbeat.start();
705
+ commitState((current) => ({
706
+ ...current,
707
+ bufferedAmount: socket2.bufferedAmount,
708
+ lastChangedAt: Date.now(),
709
+ status: "open"
710
+ }));
711
+ options.onOpen?.(event, socket2);
712
+ });
713
+ const handleMessage = useEffectEvent((event) => {
714
+ try {
715
+ const message = parseMessage(event);
716
+ heartbeat.notifyAck(message);
717
+ commitState((current) => ({
718
+ ...current,
719
+ bufferedAmount: socketRef.current?.bufferedAmount ?? current.bufferedAmount,
720
+ lastMessage: message,
721
+ lastMessageEvent: event
722
+ }));
723
+ options.onMessage?.(message, event);
724
+ } catch {
725
+ const parseError = new Event("error");
726
+ commitState((current) => ({
727
+ ...current,
728
+ lastError: parseError,
729
+ status: "error"
730
+ }));
731
+ }
732
+ });
733
+ const handleError = useEffectEvent((event) => {
734
+ heartbeat.stop();
735
+ commitState((current) => ({
736
+ ...current,
737
+ lastError: event,
738
+ status: "error"
739
+ }));
740
+ options.onError?.(event);
741
+ });
742
+ const handleClose = useEffectEvent((event) => {
743
+ socketRef.current = null;
744
+ socketKeyRef.current = null;
745
+ heartbeat.stop();
746
+ updateBufferedAmount();
747
+ const skipCloseReconnect = skipCloseReconnectRef.current;
748
+ skipCloseReconnectRef.current = false;
749
+ const shouldReconnect = !suppressReconnectRef.current && !skipCloseReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
750
+ commitState((current) => ({
751
+ ...current,
752
+ lastChangedAt: Date.now(),
753
+ lastCloseEvent: event,
754
+ status: shouldReconnect ? "reconnecting" : "closed"
755
+ }));
756
+ options.onClose?.(event);
757
+ if (shouldReconnect) {
758
+ reconnect.schedule("close");
759
+ } else {
760
+ suppressReconnectRef.current = false;
761
+ }
762
+ });
763
+ const open = () => {
764
+ manualCloseRef.current = false;
765
+ manualOpenRef.current = true;
766
+ suppressReconnectRef.current = false;
767
+ reconnect.cancel();
768
+ setOpenNonce((current) => current + 1);
769
+ };
770
+ const reconnectNow = () => {
771
+ manualCloseRef.current = false;
772
+ manualOpenRef.current = true;
773
+ skipCloseReconnectRef.current = true;
774
+ suppressReconnectRef.current = true;
775
+ heartbeat.stop();
776
+ closeSocket();
777
+ suppressReconnectRef.current = false;
778
+ reconnect.schedule("manual");
779
+ };
780
+ const close = (code, reason) => {
781
+ manualCloseRef.current = true;
782
+ manualOpenRef.current = false;
783
+ suppressReconnectRef.current = true;
784
+ reconnect.cancel();
785
+ heartbeat.stop();
786
+ commitState((current) => ({
787
+ ...current,
788
+ lastChangedAt: Date.now(),
789
+ status: "closing"
790
+ }));
791
+ closeSocket(code, reason);
792
+ };
793
+ const send = (message) => {
794
+ const socket2 = socketRef.current;
795
+ if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
796
+ return false;
797
+ }
798
+ const serializer = options.serializeMessage ?? defaultSerializeMessage;
799
+ socket2.send(serializer(message));
800
+ updateBufferedAmount();
801
+ return true;
802
+ };
803
+ useEffect(() => {
804
+ if (!supported) {
805
+ socketKeyRef.current = null;
806
+ commitState((current) => ({
807
+ ...current,
808
+ status: "closed"
809
+ }));
810
+ return;
811
+ }
812
+ if (resolvedUrl === null) {
813
+ socketKeyRef.current = null;
814
+ closeSocket();
815
+ commitState((current) => ({
816
+ ...current,
817
+ status: "closed"
818
+ }));
819
+ return;
820
+ }
821
+ const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
822
+ const nextSocketKey = `${resolvedUrl}::${protocolsDependency}::${options.binaryType ?? "blob"}`;
823
+ if (!shouldConnect) {
824
+ if (socketRef.current !== null) {
825
+ suppressReconnectRef.current = true;
826
+ closeSocket();
827
+ }
828
+ socketKeyRef.current = null;
829
+ commitState((current) => ({
830
+ ...current,
831
+ status: manualCloseRef.current ? "closed" : "idle"
832
+ }));
833
+ return;
834
+ }
835
+ if (socketRef.current !== null && socketKeyRef.current !== nextSocketKey) {
836
+ suppressReconnectRef.current = true;
837
+ closeSocket();
838
+ }
839
+ if (socketRef.current !== null) {
840
+ return;
841
+ }
842
+ const socket2 = new WebSocket(resolvedUrl, options.protocols);
843
+ socketRef.current = socket2;
844
+ socketKeyRef.current = nextSocketKey;
845
+ socket2.binaryType = options.binaryType ?? "blob";
846
+ commitState((current) => ({
847
+ ...current,
848
+ bufferedAmount: socket2.bufferedAmount,
849
+ lastChangedAt: Date.now(),
850
+ status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
851
+ }));
852
+ const handleSocketOpen = (event) => {
853
+ handleOpen(event, socket2);
854
+ };
855
+ const handleSocketMessage = (event) => {
856
+ handleMessage(event);
857
+ };
858
+ const handleSocketError = (event) => {
859
+ handleError(event);
860
+ };
861
+ const handleSocketClose = (event) => {
862
+ handleClose(event);
863
+ };
864
+ socket2.addEventListener("open", handleSocketOpen);
865
+ socket2.addEventListener("message", handleSocketMessage);
866
+ socket2.addEventListener("error", handleSocketError);
867
+ socket2.addEventListener("close", handleSocketClose);
868
+ return () => {
869
+ socket2.removeEventListener("open", handleSocketOpen);
870
+ socket2.removeEventListener("message", handleSocketMessage);
871
+ socket2.removeEventListener("error", handleSocketError);
872
+ socket2.removeEventListener("close", handleSocketClose);
873
+ };
874
+ }, [
875
+ connect,
876
+ openNonce,
877
+ options.binaryType,
878
+ protocolsDependency,
879
+ reconnect.status,
880
+ resolvedUrl,
881
+ supported
882
+ ]);
883
+ useEffect(() => () => {
884
+ suppressReconnectRef.current = true;
885
+ socketKeyRef.current = null;
886
+ const socket2 = socketRef.current;
887
+ socketRef.current = null;
888
+ if (socket2 === null) {
889
+ return;
890
+ }
891
+ if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
892
+ socket2.close();
893
+ }
894
+ }, []);
895
+ useEffect(() => {
896
+ if (state.status !== "open") {
897
+ heartbeat.stop();
898
+ }
899
+ }, [state.status]);
900
+ const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
901
+ const snapshot = createConnectionStateSnapshot(status, {
902
+ isSupported: supported,
903
+ lastChangedAt: state.lastChangedAt
904
+ });
905
+ const heartbeatState = options.heartbeat === false ? null : {
906
+ hasTimedOut: heartbeat.hasTimedOut,
907
+ isRunning: heartbeat.isRunning,
908
+ lastAckAt: heartbeat.lastAckAt,
909
+ lastBeatAt: heartbeat.lastBeatAt,
910
+ latencyMs: heartbeat.latencyMs
911
+ };
912
+ const reconnectState = options.reconnect === false ? null : {
913
+ attempt: reconnect.attempt,
914
+ isScheduled: reconnect.isScheduled,
915
+ nextDelayMs: reconnect.nextDelayMs,
916
+ status: reconnect.status
917
+ };
918
+ const commonResult = {
919
+ bufferedAmount: state.bufferedAmount,
920
+ close,
921
+ heartbeatState,
922
+ lastCloseEvent: state.lastCloseEvent,
923
+ lastError: state.lastError,
924
+ lastMessage: state.lastMessage,
925
+ lastMessageEvent: state.lastMessageEvent,
926
+ open,
927
+ reconnect: reconnectNow,
928
+ reconnectState,
929
+ send,
930
+ transport: "websocket"
931
+ };
932
+ const socket = socketRef.current;
933
+ if (snapshot.status === "open") {
934
+ return {
935
+ ...snapshot,
936
+ ...commonResult,
937
+ socket
938
+ };
939
+ }
940
+ return {
941
+ ...snapshot,
942
+ ...commonResult,
943
+ socket
944
+ };
945
+ };
946
+ var createInitialState4 = (status = "idle") => ({
947
+ lastChangedAt: null,
948
+ lastError: null,
949
+ lastEventName: null,
950
+ lastMessage: null,
951
+ lastMessageEvent: null,
952
+ status
953
+ });
954
+ var defaultParseMessage2 = (event) => event.data;
955
+ var toEventsDependency = (events) => {
956
+ if (events === void 0 || events.length === 0) {
957
+ return "";
958
+ }
959
+ return [...new Set(events)].sort().join("|");
960
+ };
961
+ var normalizeNamedEvents = (events) => {
962
+ if (events === void 0 || events.length === 0) {
963
+ return [];
964
+ }
965
+ return [...new Set(events)].filter((eventName) => eventName !== "message");
966
+ };
967
+ var useEventSource = (options) => {
968
+ const connect = options.connect ?? true;
969
+ const supported = isEventSourceSupported();
970
+ const resolvedUrl = useMemo(() => resolveUrlProvider(options.url), [options.url]);
971
+ const eventsDependency = toEventsDependency(options.events);
972
+ const namedEvents = useMemo(
973
+ () => normalizeNamedEvents(options.events),
974
+ [eventsDependency]
975
+ );
976
+ const eventSourceRef = useRef(null);
977
+ const eventSourceKeyRef = useRef(null);
978
+ const manualCloseRef = useRef(false);
979
+ const manualOpenRef = useRef(false);
980
+ const skipErrorReconnectRef = useRef(false);
981
+ const suppressReconnectRef = useRef(false);
982
+ const [openNonce, setOpenNonce] = useState(0);
983
+ const [state, setState] = useState(
984
+ () => createInitialState4(connect ? "connecting" : "idle")
985
+ );
986
+ const stateRef = useRef(state);
987
+ stateRef.current = state;
988
+ const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
989
+ const reconnect = useReconnect(
990
+ options.reconnect === false ? { enabled: false } : {
991
+ ...options.reconnect,
992
+ enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
993
+ }
994
+ );
995
+ const commitState = (next) => {
996
+ const resolved = typeof next === "function" ? next(stateRef.current) : next;
997
+ stateRef.current = resolved;
998
+ setState(resolved);
999
+ };
1000
+ const closeEventSource = useEffectEvent(() => {
1001
+ const source = eventSourceRef.current;
1002
+ if (source === null) {
1003
+ return;
1004
+ }
1005
+ eventSourceRef.current = null;
1006
+ eventSourceKeyRef.current = null;
1007
+ source.close();
1008
+ });
1009
+ const parseMessage = useEffectEvent((event) => {
1010
+ const parser = options.parseMessage ?? defaultParseMessage2;
1011
+ return parser(event);
1012
+ });
1013
+ const handleOpen = useEffectEvent((event, source) => {
1014
+ manualCloseRef.current = false;
1015
+ suppressReconnectRef.current = false;
1016
+ reconnect.markConnected();
1017
+ commitState((current) => ({
1018
+ ...current,
1019
+ lastChangedAt: Date.now(),
1020
+ status: "open"
1021
+ }));
1022
+ options.onOpen?.(event, source);
1023
+ });
1024
+ const commitParsedMessage = useEffectEvent(
1025
+ (eventName, event, isNamedEvent) => {
1026
+ try {
1027
+ const message = parseMessage(event);
1028
+ commitState((current) => ({
1029
+ ...current,
1030
+ lastEventName: eventName,
1031
+ lastMessage: message,
1032
+ lastMessageEvent: event
1033
+ }));
1034
+ if (isNamedEvent) {
1035
+ options.onEvent?.(eventName, message, event);
1036
+ return;
1037
+ }
1038
+ options.onMessage?.(message, event);
1039
+ } catch {
1040
+ const parseError = new Event("error");
1041
+ commitState((current) => ({
1042
+ ...current,
1043
+ lastError: parseError,
1044
+ status: "error"
1045
+ }));
1046
+ }
1047
+ }
1048
+ );
1049
+ const handleError = useEffectEvent((event, source) => {
1050
+ const skipErrorReconnect = skipErrorReconnectRef.current;
1051
+ skipErrorReconnectRef.current = false;
1052
+ const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
1053
+ const readyState = source.readyState;
1054
+ commitState((current) => ({
1055
+ ...current,
1056
+ lastChangedAt: Date.now(),
1057
+ lastError: event,
1058
+ status: readyState === EventSource.OPEN ? "open" : shouldReconnect ? "reconnecting" : "closed"
1059
+ }));
1060
+ options.onError?.(event);
1061
+ if (!shouldReconnect) {
1062
+ suppressReconnectRef.current = false;
1063
+ closeEventSource();
1064
+ return;
1065
+ }
1066
+ if (readyState === EventSource.CLOSED) {
1067
+ eventSourceRef.current = null;
1068
+ eventSourceKeyRef.current = null;
1069
+ reconnect.schedule("error");
1070
+ }
1071
+ });
1072
+ const open = () => {
1073
+ manualCloseRef.current = false;
1074
+ manualOpenRef.current = true;
1075
+ suppressReconnectRef.current = false;
1076
+ reconnect.cancel();
1077
+ setOpenNonce((current) => current + 1);
1078
+ };
1079
+ const reconnectNow = () => {
1080
+ manualCloseRef.current = false;
1081
+ manualOpenRef.current = true;
1082
+ skipErrorReconnectRef.current = true;
1083
+ suppressReconnectRef.current = true;
1084
+ closeEventSource();
1085
+ suppressReconnectRef.current = false;
1086
+ reconnect.schedule("manual");
1087
+ };
1088
+ const close = () => {
1089
+ manualCloseRef.current = true;
1090
+ manualOpenRef.current = false;
1091
+ suppressReconnectRef.current = true;
1092
+ reconnect.cancel();
1093
+ closeEventSource();
1094
+ commitState((current) => ({
1095
+ ...current,
1096
+ lastChangedAt: Date.now(),
1097
+ status: "closed"
1098
+ }));
1099
+ };
1100
+ useEffect(() => {
1101
+ if (!supported) {
1102
+ eventSourceKeyRef.current = null;
1103
+ commitState((current) => ({
1104
+ ...current,
1105
+ status: "closed"
1106
+ }));
1107
+ return;
1108
+ }
1109
+ if (resolvedUrl === null) {
1110
+ eventSourceKeyRef.current = null;
1111
+ closeEventSource();
1112
+ commitState((current) => ({
1113
+ ...current,
1114
+ status: "closed"
1115
+ }));
1116
+ return;
1117
+ }
1118
+ const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
1119
+ const nextEventSourceKey = [
1120
+ resolvedUrl,
1121
+ options.withCredentials ? "credentials" : "anonymous",
1122
+ eventsDependency
1123
+ ].join("::");
1124
+ if (!shouldConnect) {
1125
+ if (eventSourceRef.current !== null) {
1126
+ suppressReconnectRef.current = true;
1127
+ closeEventSource();
1128
+ }
1129
+ eventSourceKeyRef.current = null;
1130
+ commitState((current) => ({
1131
+ ...current,
1132
+ status: manualCloseRef.current ? "closed" : "idle"
1133
+ }));
1134
+ return;
1135
+ }
1136
+ if (eventSourceRef.current !== null && eventSourceKeyRef.current !== nextEventSourceKey) {
1137
+ suppressReconnectRef.current = true;
1138
+ closeEventSource();
1139
+ }
1140
+ if (eventSourceRef.current !== null) {
1141
+ return;
1142
+ }
1143
+ const source = new EventSource(resolvedUrl, {
1144
+ withCredentials: options.withCredentials ?? false
1145
+ });
1146
+ eventSourceRef.current = source;
1147
+ eventSourceKeyRef.current = nextEventSourceKey;
1148
+ commitState((current) => ({
1149
+ ...current,
1150
+ lastChangedAt: Date.now(),
1151
+ status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
1152
+ }));
1153
+ const handleSourceOpen = (event) => {
1154
+ handleOpen(event, source);
1155
+ };
1156
+ const handleSourceMessage = (event) => {
1157
+ commitParsedMessage("message", event, false);
1158
+ };
1159
+ const namedEventHandlers = /* @__PURE__ */ new Map();
1160
+ const handleSourceError = (event) => {
1161
+ handleError(event, source);
1162
+ };
1163
+ source.addEventListener("open", handleSourceOpen);
1164
+ source.addEventListener("message", handleSourceMessage);
1165
+ for (const eventName of namedEvents) {
1166
+ const handler = (event) => {
1167
+ commitParsedMessage(eventName, event, true);
1168
+ };
1169
+ namedEventHandlers.set(eventName, handler);
1170
+ source.addEventListener(eventName, handler);
1171
+ }
1172
+ source.addEventListener("error", handleSourceError);
1173
+ return () => {
1174
+ source.removeEventListener("open", handleSourceOpen);
1175
+ source.removeEventListener("message", handleSourceMessage);
1176
+ for (const [eventName, handler] of namedEventHandlers) {
1177
+ source.removeEventListener(eventName, handler);
1178
+ }
1179
+ source.removeEventListener("error", handleSourceError);
1180
+ };
1181
+ }, [
1182
+ connect,
1183
+ eventsDependency,
1184
+ namedEvents,
1185
+ openNonce,
1186
+ options.withCredentials,
1187
+ reconnect.status,
1188
+ resolvedUrl,
1189
+ supported
1190
+ ]);
1191
+ useEffect(() => () => {
1192
+ suppressReconnectRef.current = true;
1193
+ eventSourceKeyRef.current = null;
1194
+ const source = eventSourceRef.current;
1195
+ eventSourceRef.current = null;
1196
+ if (source !== null) {
1197
+ source.close();
1198
+ }
1199
+ }, []);
1200
+ const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
1201
+ const snapshot = createConnectionStateSnapshot(status, {
1202
+ isSupported: supported,
1203
+ lastChangedAt: state.lastChangedAt
1204
+ });
1205
+ const reconnectState = options.reconnect === false ? null : {
1206
+ attempt: reconnect.attempt,
1207
+ isScheduled: reconnect.isScheduled,
1208
+ nextDelayMs: reconnect.nextDelayMs,
1209
+ status: reconnect.status
1210
+ };
1211
+ const commonResult = {
1212
+ close,
1213
+ lastError: state.lastError,
1214
+ lastEventName: state.lastEventName,
1215
+ lastMessage: state.lastMessage,
1216
+ lastMessageEvent: state.lastMessageEvent,
1217
+ open,
1218
+ reconnect: reconnectNow,
1219
+ reconnectState,
1220
+ transport: "eventsource"
1221
+ };
1222
+ const eventSource = eventSourceRef.current;
1223
+ if (snapshot.status === "open") {
1224
+ return {
1225
+ ...snapshot,
1226
+ ...commonResult,
1227
+ eventSource
1228
+ };
1229
+ }
1230
+ return {
1231
+ ...snapshot,
1232
+ ...commonResult,
1233
+ eventSource
1234
+ };
1235
+ };
1236
+
1237
+ export { useEventSource, useHeartbeat, useOnlineStatus, useReconnect, useWebSocket };
1238
+ //# sourceMappingURL=index.js.map
1239
+ //# sourceMappingURL=index.js.map