react-realtime-hooks 1.0.4 → 1.2.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/README.md +125 -9
- package/dist/index.cjs +238 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +237 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://www.npmjs.com/package/react)
|
|
9
9
|
|
|
10
|
-
Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness.
|
|
10
|
+
Production-ready React hooks for WebSocket and SSE with auto-reconnect, heartbeat, typed connection state, and browser network awareness including page visibility and connection gating.
|
|
11
11
|
|
|
12
|
-
`react-realtime-hooks` is for apps that need more than "open a socket and hope for the best". It gives you composable hooks for transport lifecycle, retry strategy, heartbeat,
|
|
12
|
+
`react-realtime-hooks` is for apps that need more than "open a socket and hope for the best". It gives you composable hooks for transport lifecycle, retry strategy, heartbeat, online status, page visibility, and connection gating, so your UI can react to realtime state without rebuilding the same connection logic in every screen.
|
|
13
13
|
|
|
14
14
|
Live demo: https://volkov85.github.io/react-realtime-hooks/
|
|
15
15
|
|
|
@@ -23,7 +23,8 @@ Real apps need:
|
|
|
23
23
|
- reconnect strategy with caps, jitter, and manual control
|
|
24
24
|
- heartbeat and timeout tracking
|
|
25
25
|
- clean SSR behavior
|
|
26
|
-
- browser network awareness
|
|
26
|
+
- browser network and page visibility awareness
|
|
27
|
+
- environment-aware connection gating for offline state and background tabs
|
|
27
28
|
- typed message parsing and sending
|
|
28
29
|
|
|
29
30
|
`react-realtime-hooks` packages those concerns into small hooks that compose cleanly in React.
|
|
@@ -33,6 +34,7 @@ Real apps need:
|
|
|
33
34
|
- `useWebSocket` and `useEventSource` return state you can render, not just transport instances.
|
|
34
35
|
- Built-in reconnect flow with exponential backoff, jitter, attempt limits, and manual restart.
|
|
35
36
|
- Heartbeat support with ack matching, timeout detection, and latency measurement.
|
|
37
|
+
- `useConnectionGate` turns online and visibility signals into a single `connect` flag for transport hooks.
|
|
36
38
|
- Discriminated connection snapshots: `idle`, `connecting`, `open`, `reconnecting`, `closing`, `closed`, `error`.
|
|
37
39
|
- First-class TypeScript support with generic message types and custom parsers/serializers.
|
|
38
40
|
- SSR-safe by default. No browser-only globals are touched during server render.
|
|
@@ -46,7 +48,7 @@ Real apps need:
|
|
|
46
48
|
| Connection state | You model it yourself | Built-in status model you can render directly |
|
|
47
49
|
| Reconnect flow | Manual timers and teardown | `useReconnect` with backoff, jitter, and limits |
|
|
48
50
|
| Heartbeat | Custom ping/pong loop | `heartbeat` support with timeout and latency |
|
|
49
|
-
|
|
|
51
|
+
| Browser awareness | Separate browser event wiring | `useOnlineStatus`, `usePageVisibility`, and `useConnectionGate` for browser-aware state |
|
|
50
52
|
| SSR safety | Easy to break during render | Browser-only behavior stays out of server render |
|
|
51
53
|
| UI ergonomics | Event handlers and refs everywhere | Hook result already shaped for product UI |
|
|
52
54
|
|
|
@@ -65,7 +67,11 @@ Peer dependency:
|
|
|
65
67
|
## How It Feels
|
|
66
68
|
|
|
67
69
|
```tsx
|
|
68
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
useOnlineStatus,
|
|
72
|
+
usePageVisibility,
|
|
73
|
+
useWebSocket
|
|
74
|
+
} from "react-realtime-hooks";
|
|
69
75
|
|
|
70
76
|
type IncomingMessage =
|
|
71
77
|
| { type: "notification"; text: string }
|
|
@@ -75,6 +81,7 @@ type OutgoingMessage = { type: "ack"; id: string } | { type: "ping" };
|
|
|
75
81
|
|
|
76
82
|
export function NotificationsPanel() {
|
|
77
83
|
const network = useOnlineStatus();
|
|
84
|
+
const page = usePageVisibility();
|
|
78
85
|
const socket = useWebSocket<IncomingMessage, OutgoingMessage>({
|
|
79
86
|
url: "ws://localhost:8080/notifications",
|
|
80
87
|
parseMessage: (event) => JSON.parse(String(event.data)) as IncomingMessage,
|
|
@@ -93,8 +100,8 @@ export function NotificationsPanel() {
|
|
|
93
100
|
return (
|
|
94
101
|
<section>
|
|
95
102
|
<p>
|
|
96
|
-
|
|
97
|
-
{socket.status}
|
|
103
|
+
Page: {page.isVisible ? "visible" : "hidden"} | Network:{" "}
|
|
104
|
+
{network.isOnline ? "online" : "offline"} | Transport: {socket.status}
|
|
98
105
|
</p>
|
|
99
106
|
|
|
100
107
|
{socket.status === "reconnecting" && (
|
|
@@ -146,7 +153,7 @@ Browser APIs
|
|
|
146
153
|
WebSocket / EventSource / navigator.onLine
|
|
147
154
|
|
|
148
155
|
Core hooks
|
|
149
|
-
useReconnect / useHeartbeat / useOnlineStatus
|
|
156
|
+
useReconnect / useHeartbeat / useOnlineStatus / usePageVisibility / useConnectionGate
|
|
150
157
|
|
|
151
158
|
Transport hooks
|
|
152
159
|
useWebSocket / useEventSource
|
|
@@ -204,7 +211,9 @@ This library already models those edges in a reusable way.
|
|
|
204
211
|
| `useEventSource` | Server-Sent Events streams | `status`, `eventSource`, `lastMessage`, `lastEventName`, `reconnect()` |
|
|
205
212
|
| `useReconnect` | Reusable retry and backoff logic | `schedule()`, `cancel()`, `reset()`, `attempt`, `status` |
|
|
206
213
|
| `useHeartbeat` | Liveness checks and timeout tracking | `start()`, `stop()`, `beat()`, `notifyAck()`, `latencyMs` |
|
|
214
|
+
| `useConnectionGate` | Browser-aware transport gating | `connect`, `reason`, `isBlocked`, gate transition timestamps |
|
|
207
215
|
| `useOnlineStatus` | Browser online/offline state | `isOnline`, `isSupported`, transition timestamps |
|
|
216
|
+
| `usePageVisibility` | Browser tab/page visibility state | `isVisible`, `visibilityState`, `isSupported`, transition timestamps |
|
|
208
217
|
|
|
209
218
|
## Transport Examples
|
|
210
219
|
|
|
@@ -334,6 +343,50 @@ export function NetworkIndicator() {
|
|
|
334
343
|
}
|
|
335
344
|
```
|
|
336
345
|
|
|
346
|
+
### `usePageVisibility`
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { usePageVisibility } from "react-realtime-hooks";
|
|
350
|
+
|
|
351
|
+
export function AttentionAwareBadge() {
|
|
352
|
+
const page = usePageVisibility({
|
|
353
|
+
trackTransitions: true,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<span>
|
|
358
|
+
{page.isVisible ? "Active tab" : "Background tab"} ({page.visibilityState})
|
|
359
|
+
</span>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
### `useConnectionGate`
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
import { useConnectionGate, useWebSocket } from "react-realtime-hooks";
|
|
367
|
+
|
|
368
|
+
export function GatedNotifications() {
|
|
369
|
+
const gate = useConnectionGate({
|
|
370
|
+
requireOnline: true,
|
|
371
|
+
requireVisible: true,
|
|
372
|
+
hiddenGraceMs: 30_000,
|
|
373
|
+
});
|
|
374
|
+
const socket = useWebSocket({
|
|
375
|
+
connect: gate.connect,
|
|
376
|
+
reconnect: {
|
|
377
|
+
initialDelayMs: 1_000,
|
|
378
|
+
maxAttempts: null,
|
|
379
|
+
},
|
|
380
|
+
url: "ws://localhost:8080/notifications",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div>
|
|
385
|
+
Gate: {gate.reason} | Transport: {socket.status}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
337
390
|
## API Reference
|
|
338
391
|
|
|
339
392
|
<details>
|
|
@@ -486,6 +539,42 @@ When you configure `useWebSocket` heartbeat, you can also set `timeoutAction` an
|
|
|
486
539
|
|
|
487
540
|
</details>
|
|
488
541
|
|
|
542
|
+
<details>
|
|
543
|
+
<summary><strong>useConnectionGate</strong></summary>
|
|
544
|
+
|
|
545
|
+
### Options
|
|
546
|
+
|
|
547
|
+
| Option | Type | Default | Description |
|
|
548
|
+
| ------------------ | --------- | ------- | ------------------------------------------------------------------ |
|
|
549
|
+
| `enabled` | `boolean` | `true` | Master on/off switch for the gate |
|
|
550
|
+
| `requireOnline` | `boolean` | `true` | Blocks `connect` when the browser reports offline |
|
|
551
|
+
| `requireVisible` | `boolean` | `false` | Blocks `connect` when the page is hidden |
|
|
552
|
+
| `hiddenGraceMs` | `number` | `0` | Delay before hidden pages are blocked |
|
|
553
|
+
| `initialOnline` | `boolean` | `true` | Fallback value when `navigator.onLine` is unavailable |
|
|
554
|
+
| `initialVisible` | `boolean` | `true` | Fallback value when the Visibility API is unavailable |
|
|
555
|
+
| `trackTransitions` | `boolean` | `true` | Tracks `lastChangedAt`, `becameReadyAt`, and `becameBlockedAt` |
|
|
556
|
+
|
|
557
|
+
### Result
|
|
558
|
+
|
|
559
|
+
| Field | Type | Description |
|
|
560
|
+
| -------------------------- | ------------------------------------------ | ---------------------------------------------------------- |
|
|
561
|
+
| `connect` | `boolean` | Flag to pass into `useWebSocket` or `useEventSource` |
|
|
562
|
+
| `isBlocked` | `boolean` | Whether the gate is currently blocking connection |
|
|
563
|
+
| `reason` | `"ready" \| "manual" \| "offline" \| "hidden"` | Current gate reason |
|
|
564
|
+
| `isWaitingForVisibleGrace` | `boolean` | `true` while a hidden-tab grace window is still active |
|
|
565
|
+
| `isOnline` | `boolean` | Current browser online state |
|
|
566
|
+
| `isOnlineSupported` | `boolean` | Whether `navigator.onLine` is available |
|
|
567
|
+
| `isVisible` | `boolean` | Whether the current page is visible |
|
|
568
|
+
| `isVisibilitySupported` | `boolean` | Whether `document.visibilityState` is available |
|
|
569
|
+
| `visibilityState` | `DocumentVisibilityState \| "visible"` | Current browser visibility state |
|
|
570
|
+
| `lastChangedAt` | `number \| null` | Timestamp of the last gate state change |
|
|
571
|
+
| `becameReadyAt` | `number \| null` | Timestamp of the last transition into `reason === "ready"` |
|
|
572
|
+
| `becameBlockedAt` | `number \| null` | Timestamp of the last transition into a blocked state |
|
|
573
|
+
|
|
574
|
+
`reason` priority is deterministic: `manual` overrides `offline`, and `offline` overrides `hidden`.
|
|
575
|
+
That keeps the gate predictable when multiple blockers apply at once.
|
|
576
|
+
|
|
577
|
+
</details>
|
|
489
578
|
<details>
|
|
490
579
|
<summary><strong>useOnlineStatus</strong></summary>
|
|
491
580
|
|
|
@@ -508,6 +597,28 @@ When you configure `useWebSocket` heartbeat, you can also set `timeoutAction` an
|
|
|
508
597
|
|
|
509
598
|
</details>
|
|
510
599
|
|
|
600
|
+
<details>
|
|
601
|
+
<summary><strong>usePageVisibility</strong></summary>
|
|
602
|
+
|
|
603
|
+
### Options
|
|
604
|
+
|
|
605
|
+
| Option | Type | Default | Description |
|
|
606
|
+
| ------------------ | --------- | ------- | ----------------------------------------------------------- |
|
|
607
|
+
| `initialVisible` | `boolean` | `true` | Fallback value when the Visibility API is unavailable |
|
|
608
|
+
| `trackTransitions` | `boolean` | `true` | Tracks `lastChangedAt`, `becameVisibleAt`, `becameHiddenAt` |
|
|
609
|
+
|
|
610
|
+
### Result
|
|
611
|
+
|
|
612
|
+
| Field | Type | Description |
|
|
613
|
+
| ----------------- | ------------------------------------ | ------------------------------------------------ |
|
|
614
|
+
| `isVisible` | `boolean` | Whether the current page is visible |
|
|
615
|
+
| `visibilityState` | `DocumentVisibilityState \| "visible"` | Current browser visibility state |
|
|
616
|
+
| `isSupported` | `boolean` | Whether `document.visibilityState` is available |
|
|
617
|
+
| `lastChangedAt` | `number \| null` | Timestamp of the last visibility transition |
|
|
618
|
+
| `becameVisibleAt` | `number \| null` | Timestamp of the last visible transition |
|
|
619
|
+
| `becameHiddenAt` | `number \| null` | Timestamp of the last hidden transition |
|
|
620
|
+
|
|
621
|
+
</details>
|
|
511
622
|
## Limitations And Edge Cases
|
|
512
623
|
|
|
513
624
|
- `useEventSource` is receive-only by design. SSE is not a bidirectional transport.
|
|
@@ -527,7 +638,7 @@ The package includes behavior tests for:
|
|
|
527
638
|
- exponential backoff
|
|
528
639
|
- timer and listener cleanup
|
|
529
640
|
- heartbeat start / stop / timeout
|
|
530
|
-
- browser offline / online transitions
|
|
641
|
+
- browser offline / online and page visibility transitions
|
|
531
642
|
- invalid payload and parse errors
|
|
532
643
|
- manual reconnect and manual close
|
|
533
644
|
|
|
@@ -551,3 +662,8 @@ Development and release workflow live in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
|
551
662
|
## License
|
|
552
663
|
|
|
553
664
|
MIT
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var react = require('react');
|
|
|
8
8
|
var isWebSocketSupported = () => typeof WebSocket !== "undefined";
|
|
9
9
|
var isEventSourceSupported = () => typeof EventSource !== "undefined";
|
|
10
10
|
var hasNavigatorOnLineSupport = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean";
|
|
11
|
+
var hasDocumentVisibilitySupport = () => typeof document !== "undefined" && typeof document.visibilityState === "string";
|
|
11
12
|
var readOnlineStatus = (initialOnline = true) => {
|
|
12
13
|
if (!hasNavigatorOnLineSupport()) {
|
|
13
14
|
return {
|
|
@@ -20,6 +21,20 @@ var readOnlineStatus = (initialOnline = true) => {
|
|
|
20
21
|
isSupported: true
|
|
21
22
|
};
|
|
22
23
|
};
|
|
24
|
+
var readPageVisibility = (initialVisible = true) => {
|
|
25
|
+
if (!hasDocumentVisibilitySupport()) {
|
|
26
|
+
return {
|
|
27
|
+
isVisible: initialVisible,
|
|
28
|
+
isSupported: false,
|
|
29
|
+
visibilityState: initialVisible ? "visible" : "hidden"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
isVisible: document.visibilityState === "visible",
|
|
34
|
+
isSupported: true,
|
|
35
|
+
visibilityState: document.visibilityState
|
|
36
|
+
};
|
|
37
|
+
};
|
|
23
38
|
|
|
24
39
|
// src/hooks/useOnlineStatus.ts
|
|
25
40
|
var subscribeToOnlineStatus = (onStoreChange) => {
|
|
@@ -72,6 +87,227 @@ var useOnlineStatus = (options = {}) => {
|
|
|
72
87
|
...transitions
|
|
73
88
|
};
|
|
74
89
|
};
|
|
90
|
+
var subscribeToPageVisibility = (onStoreChange) => {
|
|
91
|
+
if (typeof document === "undefined" || !hasDocumentVisibilitySupport()) {
|
|
92
|
+
return () => {
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
document.addEventListener("visibilitychange", onStoreChange);
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener("visibilitychange", onStoreChange);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
var createEmptyTransitionState2 = () => ({
|
|
101
|
+
lastChangedAt: null,
|
|
102
|
+
becameHiddenAt: null,
|
|
103
|
+
becameVisibleAt: null
|
|
104
|
+
});
|
|
105
|
+
var usePageVisibility = (options = {}) => {
|
|
106
|
+
const initialVisible = options.initialVisible ?? true;
|
|
107
|
+
const trackTransitions = options.trackTransitions ?? true;
|
|
108
|
+
const visibilityState = react.useSyncExternalStore(
|
|
109
|
+
subscribeToPageVisibility,
|
|
110
|
+
() => readPageVisibility(initialVisible).visibilityState,
|
|
111
|
+
() => initialVisible ? "visible" : "hidden"
|
|
112
|
+
);
|
|
113
|
+
const isVisible = visibilityState === "visible";
|
|
114
|
+
const previousVisibleRef = react.useRef(isVisible);
|
|
115
|
+
const [transitions, setTransitions] = react.useState(createEmptyTransitionState2);
|
|
116
|
+
react.useEffect(() => {
|
|
117
|
+
if (!trackTransitions) {
|
|
118
|
+
previousVisibleRef.current = isVisible;
|
|
119
|
+
setTransitions(createEmptyTransitionState2);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (previousVisibleRef.current === isVisible) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const changedAt = Date.now();
|
|
126
|
+
previousVisibleRef.current = isVisible;
|
|
127
|
+
setTransitions((current) => ({
|
|
128
|
+
lastChangedAt: changedAt,
|
|
129
|
+
becameHiddenAt: isVisible ? current.becameHiddenAt : changedAt,
|
|
130
|
+
becameVisibleAt: isVisible ? changedAt : current.becameVisibleAt
|
|
131
|
+
}));
|
|
132
|
+
}, [isVisible, trackTransitions]);
|
|
133
|
+
return {
|
|
134
|
+
isSupported: hasDocumentVisibilitySupport(),
|
|
135
|
+
isVisible,
|
|
136
|
+
visibilityState,
|
|
137
|
+
...transitions
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/core/timers.ts
|
|
142
|
+
var sanitizeTimerDelay = (delayMs) => {
|
|
143
|
+
if (!Number.isFinite(delayMs)) {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
return Math.max(0, Math.round(delayMs));
|
|
147
|
+
};
|
|
148
|
+
var createManagedTimeout = () => {
|
|
149
|
+
let timeoutId = null;
|
|
150
|
+
return {
|
|
151
|
+
cancel() {
|
|
152
|
+
if (timeoutId !== null) {
|
|
153
|
+
clearTimeout(timeoutId);
|
|
154
|
+
timeoutId = null;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
isActive() {
|
|
158
|
+
return timeoutId !== null;
|
|
159
|
+
},
|
|
160
|
+
schedule(callback, delayMs) {
|
|
161
|
+
if (timeoutId !== null) {
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
}
|
|
164
|
+
timeoutId = setTimeout(() => {
|
|
165
|
+
timeoutId = null;
|
|
166
|
+
callback();
|
|
167
|
+
}, sanitizeTimerDelay(delayMs));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
var createManagedInterval = () => {
|
|
172
|
+
let intervalId = null;
|
|
173
|
+
return {
|
|
174
|
+
cancel() {
|
|
175
|
+
if (intervalId !== null) {
|
|
176
|
+
clearInterval(intervalId);
|
|
177
|
+
intervalId = null;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
isActive() {
|
|
181
|
+
return intervalId !== null;
|
|
182
|
+
},
|
|
183
|
+
start(callback, intervalMs) {
|
|
184
|
+
if (intervalId !== null) {
|
|
185
|
+
clearInterval(intervalId);
|
|
186
|
+
}
|
|
187
|
+
intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/hooks/useConnectionGate.ts
|
|
193
|
+
var createEmptyTransitionState3 = () => ({
|
|
194
|
+
becameBlockedAt: null,
|
|
195
|
+
becameReadyAt: null,
|
|
196
|
+
lastChangedAt: null
|
|
197
|
+
});
|
|
198
|
+
var normalizeHiddenGraceMs = (value) => {
|
|
199
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
return Math.max(0, value);
|
|
203
|
+
};
|
|
204
|
+
var useConnectionGate = (options = {}) => {
|
|
205
|
+
const enabled = options.enabled ?? true;
|
|
206
|
+
const requireOnline = options.requireOnline ?? true;
|
|
207
|
+
const requireVisible = options.requireVisible ?? false;
|
|
208
|
+
const hiddenGraceMs = normalizeHiddenGraceMs(options.hiddenGraceMs);
|
|
209
|
+
const trackTransitions = options.trackTransitions ?? true;
|
|
210
|
+
const onlineStatus = useOnlineStatus({
|
|
211
|
+
...options.initialOnline === void 0 ? {} : { initialOnline: options.initialOnline },
|
|
212
|
+
trackTransitions: false
|
|
213
|
+
});
|
|
214
|
+
const pageVisibility = usePageVisibility({
|
|
215
|
+
...options.initialVisible === void 0 ? {} : { initialVisible: options.initialVisible },
|
|
216
|
+
trackTransitions: false
|
|
217
|
+
});
|
|
218
|
+
const hiddenGraceTimeoutRef = react.useRef(createManagedTimeout());
|
|
219
|
+
const hiddenSinceRef = react.useRef(null);
|
|
220
|
+
const previousStateRef = react.useRef(null);
|
|
221
|
+
const [hasExceededHiddenGrace, setHasExceededHiddenGrace] = react.useState(false);
|
|
222
|
+
const [isWaitingForVisibleGrace, setIsWaitingForVisibleGrace] = react.useState(false);
|
|
223
|
+
const [transitions, setTransitions] = react.useState(createEmptyTransitionState3);
|
|
224
|
+
react.useEffect(() => () => {
|
|
225
|
+
hiddenGraceTimeoutRef.current.cancel();
|
|
226
|
+
}, []);
|
|
227
|
+
react.useEffect(() => {
|
|
228
|
+
hiddenGraceTimeoutRef.current.cancel();
|
|
229
|
+
if (!requireVisible || pageVisibility.isVisible) {
|
|
230
|
+
hiddenSinceRef.current = null;
|
|
231
|
+
setHasExceededHiddenGrace(false);
|
|
232
|
+
setIsWaitingForVisibleGrace(false);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const hiddenSince = hiddenSinceRef.current ?? Date.now();
|
|
236
|
+
hiddenSinceRef.current = hiddenSince;
|
|
237
|
+
if (hiddenGraceMs <= 0) {
|
|
238
|
+
setHasExceededHiddenGrace(true);
|
|
239
|
+
setIsWaitingForVisibleGrace(false);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const elapsedMs = Date.now() - hiddenSince;
|
|
243
|
+
if (elapsedMs >= hiddenGraceMs) {
|
|
244
|
+
setHasExceededHiddenGrace(true);
|
|
245
|
+
setIsWaitingForVisibleGrace(false);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
setHasExceededHiddenGrace(false);
|
|
249
|
+
setIsWaitingForVisibleGrace(true);
|
|
250
|
+
hiddenGraceTimeoutRef.current.schedule(() => {
|
|
251
|
+
setHasExceededHiddenGrace(true);
|
|
252
|
+
setIsWaitingForVisibleGrace(false);
|
|
253
|
+
}, hiddenGraceMs - elapsedMs);
|
|
254
|
+
}, [hiddenGraceMs, pageVisibility.isVisible, requireVisible]);
|
|
255
|
+
let reason = "ready";
|
|
256
|
+
if (!enabled) {
|
|
257
|
+
reason = "manual";
|
|
258
|
+
} else if (requireOnline && !onlineStatus.isOnline) {
|
|
259
|
+
reason = "offline";
|
|
260
|
+
} else if (requireVisible && !pageVisibility.isVisible && hasExceededHiddenGrace) {
|
|
261
|
+
reason = "hidden";
|
|
262
|
+
}
|
|
263
|
+
const connect = reason === "ready";
|
|
264
|
+
const isBlocked = !connect;
|
|
265
|
+
react.useEffect(() => {
|
|
266
|
+
if (!trackTransitions) {
|
|
267
|
+
previousStateRef.current = {
|
|
268
|
+
connect,
|
|
269
|
+
reason
|
|
270
|
+
};
|
|
271
|
+
setTransitions(createEmptyTransitionState3);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const previousState = previousStateRef.current;
|
|
275
|
+
if (previousState === null) {
|
|
276
|
+
previousStateRef.current = {
|
|
277
|
+
connect,
|
|
278
|
+
reason
|
|
279
|
+
};
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (previousState.connect === connect && previousState.reason === reason) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const changedAt = Date.now();
|
|
286
|
+
previousStateRef.current = {
|
|
287
|
+
connect,
|
|
288
|
+
reason
|
|
289
|
+
};
|
|
290
|
+
setTransitions((current) => ({
|
|
291
|
+
becameBlockedAt: connect ? current.becameBlockedAt : changedAt,
|
|
292
|
+
becameReadyAt: connect ? changedAt : current.becameReadyAt,
|
|
293
|
+
lastChangedAt: changedAt
|
|
294
|
+
}));
|
|
295
|
+
}, [connect, reason, trackTransitions]);
|
|
296
|
+
return {
|
|
297
|
+
becameBlockedAt: transitions.becameBlockedAt,
|
|
298
|
+
becameReadyAt: transitions.becameReadyAt,
|
|
299
|
+
connect,
|
|
300
|
+
isBlocked,
|
|
301
|
+
isOnline: onlineStatus.isOnline,
|
|
302
|
+
isOnlineSupported: onlineStatus.isSupported,
|
|
303
|
+
isVisibilitySupported: pageVisibility.isSupported,
|
|
304
|
+
isVisible: pageVisibility.isVisible,
|
|
305
|
+
isWaitingForVisibleGrace,
|
|
306
|
+
lastChangedAt: transitions.lastChangedAt,
|
|
307
|
+
reason,
|
|
308
|
+
visibilityState: pageVisibility.visibilityState
|
|
309
|
+
};
|
|
310
|
+
};
|
|
75
311
|
|
|
76
312
|
// src/core/reconnect.ts
|
|
77
313
|
var DEFAULT_RECONNECT_OPTIONS = {
|
|
@@ -209,57 +445,6 @@ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {
|
|
|
209
445
|
};
|
|
210
446
|
};
|
|
211
447
|
|
|
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
448
|
// src/hooks/useReconnect.ts
|
|
264
449
|
var createInitialState = (enabled) => ({
|
|
265
450
|
attempt: 0,
|
|
@@ -1395,9 +1580,11 @@ var useEventSource = (options) => {
|
|
|
1395
1580
|
};
|
|
1396
1581
|
};
|
|
1397
1582
|
|
|
1583
|
+
exports.useConnectionGate = useConnectionGate;
|
|
1398
1584
|
exports.useEventSource = useEventSource;
|
|
1399
1585
|
exports.useHeartbeat = useHeartbeat;
|
|
1400
1586
|
exports.useOnlineStatus = useOnlineStatus;
|
|
1587
|
+
exports.usePageVisibility = usePageVisibility;
|
|
1401
1588
|
exports.useReconnect = useReconnect;
|
|
1402
1589
|
exports.useWebSocket = useWebSocket;
|
|
1403
1590
|
//# sourceMappingURL=index.cjs.map
|