react-realtime-hooks 1.1.0 → 1.2.1
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 +71 -4
- package/dist/index.cjs +237 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +237 -71
- 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 including page visibility.
|
|
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, online status,
|
|
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
|
|
|
@@ -24,6 +24,7 @@ Real apps need:
|
|
|
24
24
|
- heartbeat and timeout tracking
|
|
25
25
|
- clean SSR behavior
|
|
26
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
|
-
| Browser awareness | Separate browser event wiring | `useOnlineStatus` and `
|
|
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
|
|
|
@@ -151,7 +153,7 @@ Browser APIs
|
|
|
151
153
|
WebSocket / EventSource / navigator.onLine
|
|
152
154
|
|
|
153
155
|
Core hooks
|
|
154
|
-
useReconnect / useHeartbeat / useOnlineStatus / usePageVisibility
|
|
156
|
+
useReconnect / useHeartbeat / useOnlineStatus / usePageVisibility / useConnectionGate
|
|
155
157
|
|
|
156
158
|
Transport hooks
|
|
157
159
|
useWebSocket / useEventSource
|
|
@@ -209,6 +211,7 @@ This library already models those edges in a reusable way.
|
|
|
209
211
|
| `useEventSource` | Server-Sent Events streams | `status`, `eventSource`, `lastMessage`, `lastEventName`, `reconnect()` |
|
|
210
212
|
| `useReconnect` | Reusable retry and backoff logic | `schedule()`, `cancel()`, `reset()`, `attempt`, `status` |
|
|
211
213
|
| `useHeartbeat` | Liveness checks and timeout tracking | `start()`, `stop()`, `beat()`, `notifyAck()`, `latencyMs` |
|
|
214
|
+
| `useConnectionGate` | Browser-aware transport gating | `connect`, `reason`, `isBlocked`, gate transition timestamps |
|
|
212
215
|
| `useOnlineStatus` | Browser online/offline state | `isOnline`, `isSupported`, transition timestamps |
|
|
213
216
|
| `usePageVisibility` | Browser tab/page visibility state | `isVisible`, `visibilityState`, `isSupported`, transition timestamps |
|
|
214
217
|
|
|
@@ -357,6 +360,33 @@ export function AttentionAwareBadge() {
|
|
|
357
360
|
);
|
|
358
361
|
}
|
|
359
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
|
+
```
|
|
360
390
|
## API Reference
|
|
361
391
|
|
|
362
392
|
<details>
|
|
@@ -509,6 +539,42 @@ When you configure `useWebSocket` heartbeat, you can also set `timeoutAction` an
|
|
|
509
539
|
|
|
510
540
|
</details>
|
|
511
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>
|
|
512
578
|
<details>
|
|
513
579
|
<summary><strong>useOnlineStatus</strong></summary>
|
|
514
580
|
|
|
@@ -600,3 +666,4 @@ MIT
|
|
|
600
666
|
|
|
601
667
|
|
|
602
668
|
|
|
669
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -138,6 +138,177 @@ var usePageVisibility = (options = {}) => {
|
|
|
138
138
|
};
|
|
139
139
|
};
|
|
140
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
|
+
};
|
|
311
|
+
|
|
141
312
|
// src/core/reconnect.ts
|
|
142
313
|
var DEFAULT_RECONNECT_OPTIONS = {
|
|
143
314
|
backoffFactor: 2,
|
|
@@ -274,57 +445,6 @@ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {
|
|
|
274
445
|
};
|
|
275
446
|
};
|
|
276
447
|
|
|
277
|
-
// src/core/timers.ts
|
|
278
|
-
var sanitizeTimerDelay = (delayMs) => {
|
|
279
|
-
if (!Number.isFinite(delayMs)) {
|
|
280
|
-
return 0;
|
|
281
|
-
}
|
|
282
|
-
return Math.max(0, Math.round(delayMs));
|
|
283
|
-
};
|
|
284
|
-
var createManagedTimeout = () => {
|
|
285
|
-
let timeoutId = null;
|
|
286
|
-
return {
|
|
287
|
-
cancel() {
|
|
288
|
-
if (timeoutId !== null) {
|
|
289
|
-
clearTimeout(timeoutId);
|
|
290
|
-
timeoutId = null;
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
isActive() {
|
|
294
|
-
return timeoutId !== null;
|
|
295
|
-
},
|
|
296
|
-
schedule(callback, delayMs) {
|
|
297
|
-
if (timeoutId !== null) {
|
|
298
|
-
clearTimeout(timeoutId);
|
|
299
|
-
}
|
|
300
|
-
timeoutId = setTimeout(() => {
|
|
301
|
-
timeoutId = null;
|
|
302
|
-
callback();
|
|
303
|
-
}, sanitizeTimerDelay(delayMs));
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
};
|
|
307
|
-
var createManagedInterval = () => {
|
|
308
|
-
let intervalId = null;
|
|
309
|
-
return {
|
|
310
|
-
cancel() {
|
|
311
|
-
if (intervalId !== null) {
|
|
312
|
-
clearInterval(intervalId);
|
|
313
|
-
intervalId = null;
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
isActive() {
|
|
317
|
-
return intervalId !== null;
|
|
318
|
-
},
|
|
319
|
-
start(callback, intervalMs) {
|
|
320
|
-
if (intervalId !== null) {
|
|
321
|
-
clearInterval(intervalId);
|
|
322
|
-
}
|
|
323
|
-
intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
};
|
|
327
|
-
|
|
328
448
|
// src/hooks/useReconnect.ts
|
|
329
449
|
var createInitialState = (enabled) => ({
|
|
330
450
|
attempt: 0,
|
|
@@ -706,6 +826,9 @@ var useWebSocket = (options) => {
|
|
|
706
826
|
const protocolsDependency = toProtocolsDependency(options.protocols);
|
|
707
827
|
const socketRef = react.useRef(null);
|
|
708
828
|
const socketKeyRef = react.useRef(null);
|
|
829
|
+
const activeSocketEpochRef = react.useRef(null);
|
|
830
|
+
const closingSocketEpochRef = react.useRef(null);
|
|
831
|
+
const nextSocketEpochRef = react.useRef(0);
|
|
709
832
|
const manualCloseRef = react.useRef(false);
|
|
710
833
|
const manualOpenRef = react.useRef(false);
|
|
711
834
|
const skipCloseReconnectRef = react.useRef(false);
|
|
@@ -794,17 +917,28 @@ var useWebSocket = (options) => {
|
|
|
794
917
|
stateRef.current = resolved;
|
|
795
918
|
setState(resolved);
|
|
796
919
|
};
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
if (socket2 === null) {
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
socketRef.current = null;
|
|
803
|
-
socketKeyRef.current = null;
|
|
804
|
-
if (isSocketActive(socket2)) {
|
|
805
|
-
socket2.close(code, reason);
|
|
806
|
-
}
|
|
920
|
+
const isActiveSocketEvent = react.useEffectEvent((socketEpoch) => {
|
|
921
|
+
return activeSocketEpochRef.current === socketEpoch;
|
|
807
922
|
});
|
|
923
|
+
const shouldHandleSocketClose = react.useEffectEvent((socketEpoch) => {
|
|
924
|
+
return activeSocketEpochRef.current === socketEpoch || closingSocketEpochRef.current === socketEpoch;
|
|
925
|
+
});
|
|
926
|
+
const closeSocket = react.useEffectEvent(
|
|
927
|
+
(config = {}) => {
|
|
928
|
+
const socket2 = socketRef.current;
|
|
929
|
+
const socketEpoch = activeSocketEpochRef.current;
|
|
930
|
+
if (socket2 === null || socketEpoch === null) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
socketRef.current = null;
|
|
934
|
+
socketKeyRef.current = null;
|
|
935
|
+
activeSocketEpochRef.current = null;
|
|
936
|
+
closingSocketEpochRef.current = config.trackClose ? socketEpoch : null;
|
|
937
|
+
if (isSocketActive(socket2)) {
|
|
938
|
+
socket2.close(config.code, config.reason);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
);
|
|
808
942
|
const applyHeartbeatAction = react.useEffectEvent(
|
|
809
943
|
(action, error, reconnectTrigger) => {
|
|
810
944
|
heartbeat.stop();
|
|
@@ -839,7 +973,7 @@ var useWebSocket = (options) => {
|
|
|
839
973
|
};
|
|
840
974
|
skipCloseReconnectRef.current = true;
|
|
841
975
|
suppressReconnectRef.current = true;
|
|
842
|
-
closeSocket();
|
|
976
|
+
closeSocket({ trackClose: true });
|
|
843
977
|
}
|
|
844
978
|
);
|
|
845
979
|
const parseMessage = react.useEffectEvent((event) => {
|
|
@@ -893,10 +1027,17 @@ var useWebSocket = (options) => {
|
|
|
893
1027
|
lastError: parseError,
|
|
894
1028
|
status: "error"
|
|
895
1029
|
}));
|
|
896
|
-
closeSocket(
|
|
1030
|
+
closeSocket({
|
|
1031
|
+
code: 1003,
|
|
1032
|
+
reason: "parse-error",
|
|
1033
|
+
trackClose: true
|
|
1034
|
+
});
|
|
897
1035
|
}
|
|
898
1036
|
});
|
|
899
|
-
const handleError = react.useEffectEvent((event) => {
|
|
1037
|
+
const handleError = react.useEffectEvent((event, socketEpoch) => {
|
|
1038
|
+
if (!isActiveSocketEvent(socketEpoch)) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
900
1041
|
heartbeat.stop();
|
|
901
1042
|
commitState((current) => ({
|
|
902
1043
|
...current,
|
|
@@ -906,9 +1047,18 @@ var useWebSocket = (options) => {
|
|
|
906
1047
|
}));
|
|
907
1048
|
options.onError?.(event);
|
|
908
1049
|
});
|
|
909
|
-
const handleClose = react.useEffectEvent((event) => {
|
|
910
|
-
|
|
911
|
-
|
|
1050
|
+
const handleClose = react.useEffectEvent((event, socketEpoch) => {
|
|
1051
|
+
if (!shouldHandleSocketClose(socketEpoch)) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (activeSocketEpochRef.current === socketEpoch) {
|
|
1055
|
+
socketRef.current = null;
|
|
1056
|
+
socketKeyRef.current = null;
|
|
1057
|
+
activeSocketEpochRef.current = null;
|
|
1058
|
+
}
|
|
1059
|
+
if (closingSocketEpochRef.current === socketEpoch) {
|
|
1060
|
+
closingSocketEpochRef.current = null;
|
|
1061
|
+
}
|
|
912
1062
|
heartbeat.stop();
|
|
913
1063
|
updateBufferedAmount();
|
|
914
1064
|
const pendingCloseAction = pendingCloseActionRef.current;
|
|
@@ -988,7 +1138,11 @@ var useWebSocket = (options) => {
|
|
|
988
1138
|
lastChangedAt: Date.now(),
|
|
989
1139
|
status: "closing"
|
|
990
1140
|
}));
|
|
991
|
-
closeSocket(
|
|
1141
|
+
closeSocket({
|
|
1142
|
+
code,
|
|
1143
|
+
reason,
|
|
1144
|
+
trackClose: true
|
|
1145
|
+
});
|
|
992
1146
|
};
|
|
993
1147
|
const send = (message) => {
|
|
994
1148
|
const socket2 = socketRef.current;
|
|
@@ -1040,8 +1194,12 @@ var useWebSocket = (options) => {
|
|
|
1040
1194
|
return;
|
|
1041
1195
|
}
|
|
1042
1196
|
const socket2 = new WebSocket(resolvedUrl, options.protocols);
|
|
1197
|
+
const socketEpoch = nextSocketEpochRef.current + 1;
|
|
1043
1198
|
socketRef.current = socket2;
|
|
1044
1199
|
socketKeyRef.current = nextSocketKey;
|
|
1200
|
+
activeSocketEpochRef.current = socketEpoch;
|
|
1201
|
+
closingSocketEpochRef.current = null;
|
|
1202
|
+
nextSocketEpochRef.current = socketEpoch;
|
|
1045
1203
|
socket2.binaryType = options.binaryType ?? "blob";
|
|
1046
1204
|
commitState((current) => ({
|
|
1047
1205
|
...current,
|
|
@@ -1050,16 +1208,22 @@ var useWebSocket = (options) => {
|
|
|
1050
1208
|
status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
|
|
1051
1209
|
}));
|
|
1052
1210
|
const handleSocketOpen = (event) => {
|
|
1211
|
+
if (!isActiveSocketEvent(socketEpoch)) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1053
1214
|
handleOpen(event, socket2);
|
|
1054
1215
|
};
|
|
1055
1216
|
const handleSocketMessage = (event) => {
|
|
1217
|
+
if (!isActiveSocketEvent(socketEpoch)) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1056
1220
|
handleMessage(event);
|
|
1057
1221
|
};
|
|
1058
1222
|
const handleSocketError = (event) => {
|
|
1059
|
-
handleError(event);
|
|
1223
|
+
handleError(event, socketEpoch);
|
|
1060
1224
|
};
|
|
1061
1225
|
const handleSocketClose = (event) => {
|
|
1062
|
-
handleClose(event);
|
|
1226
|
+
handleClose(event, socketEpoch);
|
|
1063
1227
|
};
|
|
1064
1228
|
socket2.addEventListener("open", handleSocketOpen);
|
|
1065
1229
|
socket2.addEventListener("message", handleSocketMessage);
|
|
@@ -1083,6 +1247,8 @@ var useWebSocket = (options) => {
|
|
|
1083
1247
|
react.useEffect(() => () => {
|
|
1084
1248
|
suppressReconnectRef.current = true;
|
|
1085
1249
|
socketKeyRef.current = null;
|
|
1250
|
+
activeSocketEpochRef.current = null;
|
|
1251
|
+
closingSocketEpochRef.current = null;
|
|
1086
1252
|
terminalErrorRef.current = null;
|
|
1087
1253
|
const socket2 = socketRef.current;
|
|
1088
1254
|
socketRef.current = null;
|
|
@@ -1460,6 +1626,7 @@ var useEventSource = (options) => {
|
|
|
1460
1626
|
};
|
|
1461
1627
|
};
|
|
1462
1628
|
|
|
1629
|
+
exports.useConnectionGate = useConnectionGate;
|
|
1463
1630
|
exports.useEventSource = useEventSource;
|
|
1464
1631
|
exports.useHeartbeat = useHeartbeat;
|
|
1465
1632
|
exports.useOnlineStatus = useOnlineStatus;
|