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