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 CHANGED
@@ -7,9 +7,9 @@
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-typed-3178c6)](https://www.typescriptlang.org/)
8
8
  [![react](https://img.shields.io/badge/react-19.x-149eca)](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, and page visibility, so your UI can react to realtime state without rebuilding the same connection logic in every screen.
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 `usePageVisibility` for browser state |
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 closeSocket = react.useEffectEvent((code, reason) => {
798
- const socket2 = socketRef.current;
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(1003, "parse-error");
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
- socketRef.current = null;
911
- socketKeyRef.current = null;
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(code, reason);
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;