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/dist/index.d.ts CHANGED
@@ -13,6 +13,50 @@ type UseOnlineStatusHook = (options?: UseOnlineStatusOptions) => UseOnlineStatus
13
13
 
14
14
  declare const useOnlineStatus: UseOnlineStatusHook;
15
15
 
16
+ interface UsePageVisibilityOptions {
17
+ initialVisible?: boolean;
18
+ trackTransitions?: boolean;
19
+ }
20
+ interface UsePageVisibilityResult {
21
+ isVisible: boolean;
22
+ visibilityState: DocumentVisibilityState | "visible";
23
+ isSupported: boolean;
24
+ lastChangedAt: number | null;
25
+ becameVisibleAt: number | null;
26
+ becameHiddenAt: number | null;
27
+ }
28
+ type UsePageVisibilityHook = (options?: UsePageVisibilityOptions) => UsePageVisibilityResult;
29
+
30
+ declare const usePageVisibility: UsePageVisibilityHook;
31
+
32
+ type ConnectionGateReason = "ready" | "manual" | "offline" | "hidden";
33
+ interface UseConnectionGateOptions {
34
+ enabled?: boolean;
35
+ requireOnline?: boolean;
36
+ requireVisible?: boolean;
37
+ hiddenGraceMs?: number;
38
+ initialOnline?: boolean;
39
+ initialVisible?: boolean;
40
+ trackTransitions?: boolean;
41
+ }
42
+ interface UseConnectionGateResult {
43
+ connect: boolean;
44
+ isBlocked: boolean;
45
+ isWaitingForVisibleGrace: boolean;
46
+ reason: ConnectionGateReason;
47
+ isOnline: boolean;
48
+ isOnlineSupported: boolean;
49
+ isVisible: boolean;
50
+ isVisibilitySupported: boolean;
51
+ visibilityState: DocumentVisibilityState | "visible";
52
+ lastChangedAt: number | null;
53
+ becameReadyAt: number | null;
54
+ becameBlockedAt: number | null;
55
+ }
56
+ type UseConnectionGateHook = (options?: UseConnectionGateOptions) => UseConnectionGateResult;
57
+
58
+ declare const useConnectionGate: UseConnectionGateHook;
59
+
16
60
  type ReconnectStatus = "idle" | "scheduled" | "running" | "stopped";
17
61
  type ReconnectTrigger = "mount" | "manual" | "close" | "error" | "heartbeat-timeout" | "offline" | "online" | "visibility";
18
62
  interface ReconnectAttempt {
@@ -219,4 +263,4 @@ type UseEventSourceHook = <TMessage = unknown>(options: UseEventSourceOptions<TM
219
263
 
220
264
  declare const useEventSource: UseEventSourceHook;
221
265
 
222
- export { type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useEventSource, useHeartbeat, useOnlineStatus, useReconnect, useWebSocket };
266
+ export { type ConnectionGateReason, type ConnectionStateSnapshot, type HeartbeatAckMatcher, type HeartbeatBeatFn, type MessageParser, type MessageSerializer, type Milliseconds, type RealtimeConnectionStatus, type RealtimeTransport, type ReconnectAttempt, type ReconnectDelayContext, type ReconnectDelayStrategy, type ReconnectStatus, type ReconnectTrigger, type UrlProvider, type UseConnectionGateHook, type UseConnectionGateOptions, type UseConnectionGateResult, type UseEventSourceHook, type UseEventSourceOptions, type UseEventSourceResult, type UseHeartbeatHook, type UseHeartbeatOptions, type UseHeartbeatResult, type UseOnlineStatusHook, type UseOnlineStatusOptions, type UseOnlineStatusResult, type UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { useSyncExternalStore, useRef, useState, useEffect, useEffectEvent, useM
6
6
  var isWebSocketSupported = () => typeof WebSocket !== "undefined";
7
7
  var isEventSourceSupported = () => typeof EventSource !== "undefined";
8
8
  var hasNavigatorOnLineSupport = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean";
9
+ var hasDocumentVisibilitySupport = () => typeof document !== "undefined" && typeof document.visibilityState === "string";
9
10
  var readOnlineStatus = (initialOnline = true) => {
10
11
  if (!hasNavigatorOnLineSupport()) {
11
12
  return {
@@ -18,6 +19,20 @@ var readOnlineStatus = (initialOnline = true) => {
18
19
  isSupported: true
19
20
  };
20
21
  };
22
+ var readPageVisibility = (initialVisible = true) => {
23
+ if (!hasDocumentVisibilitySupport()) {
24
+ return {
25
+ isVisible: initialVisible,
26
+ isSupported: false,
27
+ visibilityState: initialVisible ? "visible" : "hidden"
28
+ };
29
+ }
30
+ return {
31
+ isVisible: document.visibilityState === "visible",
32
+ isSupported: true,
33
+ visibilityState: document.visibilityState
34
+ };
35
+ };
21
36
 
22
37
  // src/hooks/useOnlineStatus.ts
23
38
  var subscribeToOnlineStatus = (onStoreChange) => {
@@ -70,6 +85,227 @@ var useOnlineStatus = (options = {}) => {
70
85
  ...transitions
71
86
  };
72
87
  };
88
+ var subscribeToPageVisibility = (onStoreChange) => {
89
+ if (typeof document === "undefined" || !hasDocumentVisibilitySupport()) {
90
+ return () => {
91
+ };
92
+ }
93
+ document.addEventListener("visibilitychange", onStoreChange);
94
+ return () => {
95
+ document.removeEventListener("visibilitychange", onStoreChange);
96
+ };
97
+ };
98
+ var createEmptyTransitionState2 = () => ({
99
+ lastChangedAt: null,
100
+ becameHiddenAt: null,
101
+ becameVisibleAt: null
102
+ });
103
+ var usePageVisibility = (options = {}) => {
104
+ const initialVisible = options.initialVisible ?? true;
105
+ const trackTransitions = options.trackTransitions ?? true;
106
+ const visibilityState = useSyncExternalStore(
107
+ subscribeToPageVisibility,
108
+ () => readPageVisibility(initialVisible).visibilityState,
109
+ () => initialVisible ? "visible" : "hidden"
110
+ );
111
+ const isVisible = visibilityState === "visible";
112
+ const previousVisibleRef = useRef(isVisible);
113
+ const [transitions, setTransitions] = useState(createEmptyTransitionState2);
114
+ useEffect(() => {
115
+ if (!trackTransitions) {
116
+ previousVisibleRef.current = isVisible;
117
+ setTransitions(createEmptyTransitionState2);
118
+ return;
119
+ }
120
+ if (previousVisibleRef.current === isVisible) {
121
+ return;
122
+ }
123
+ const changedAt = Date.now();
124
+ previousVisibleRef.current = isVisible;
125
+ setTransitions((current) => ({
126
+ lastChangedAt: changedAt,
127
+ becameHiddenAt: isVisible ? current.becameHiddenAt : changedAt,
128
+ becameVisibleAt: isVisible ? changedAt : current.becameVisibleAt
129
+ }));
130
+ }, [isVisible, trackTransitions]);
131
+ return {
132
+ isSupported: hasDocumentVisibilitySupport(),
133
+ isVisible,
134
+ visibilityState,
135
+ ...transitions
136
+ };
137
+ };
138
+
139
+ // src/core/timers.ts
140
+ var sanitizeTimerDelay = (delayMs) => {
141
+ if (!Number.isFinite(delayMs)) {
142
+ return 0;
143
+ }
144
+ return Math.max(0, Math.round(delayMs));
145
+ };
146
+ var createManagedTimeout = () => {
147
+ let timeoutId = null;
148
+ return {
149
+ cancel() {
150
+ if (timeoutId !== null) {
151
+ clearTimeout(timeoutId);
152
+ timeoutId = null;
153
+ }
154
+ },
155
+ isActive() {
156
+ return timeoutId !== null;
157
+ },
158
+ schedule(callback, delayMs) {
159
+ if (timeoutId !== null) {
160
+ clearTimeout(timeoutId);
161
+ }
162
+ timeoutId = setTimeout(() => {
163
+ timeoutId = null;
164
+ callback();
165
+ }, sanitizeTimerDelay(delayMs));
166
+ }
167
+ };
168
+ };
169
+ var createManagedInterval = () => {
170
+ let intervalId = null;
171
+ return {
172
+ cancel() {
173
+ if (intervalId !== null) {
174
+ clearInterval(intervalId);
175
+ intervalId = null;
176
+ }
177
+ },
178
+ isActive() {
179
+ return intervalId !== null;
180
+ },
181
+ start(callback, intervalMs) {
182
+ if (intervalId !== null) {
183
+ clearInterval(intervalId);
184
+ }
185
+ intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
186
+ }
187
+ };
188
+ };
189
+
190
+ // src/hooks/useConnectionGate.ts
191
+ var createEmptyTransitionState3 = () => ({
192
+ becameBlockedAt: null,
193
+ becameReadyAt: null,
194
+ lastChangedAt: null
195
+ });
196
+ var normalizeHiddenGraceMs = (value) => {
197
+ if (value === void 0 || !Number.isFinite(value)) {
198
+ return 0;
199
+ }
200
+ return Math.max(0, value);
201
+ };
202
+ var useConnectionGate = (options = {}) => {
203
+ const enabled = options.enabled ?? true;
204
+ const requireOnline = options.requireOnline ?? true;
205
+ const requireVisible = options.requireVisible ?? false;
206
+ const hiddenGraceMs = normalizeHiddenGraceMs(options.hiddenGraceMs);
207
+ const trackTransitions = options.trackTransitions ?? true;
208
+ const onlineStatus = useOnlineStatus({
209
+ ...options.initialOnline === void 0 ? {} : { initialOnline: options.initialOnline },
210
+ trackTransitions: false
211
+ });
212
+ const pageVisibility = usePageVisibility({
213
+ ...options.initialVisible === void 0 ? {} : { initialVisible: options.initialVisible },
214
+ trackTransitions: false
215
+ });
216
+ const hiddenGraceTimeoutRef = useRef(createManagedTimeout());
217
+ const hiddenSinceRef = useRef(null);
218
+ const previousStateRef = useRef(null);
219
+ const [hasExceededHiddenGrace, setHasExceededHiddenGrace] = useState(false);
220
+ const [isWaitingForVisibleGrace, setIsWaitingForVisibleGrace] = useState(false);
221
+ const [transitions, setTransitions] = useState(createEmptyTransitionState3);
222
+ useEffect(() => () => {
223
+ hiddenGraceTimeoutRef.current.cancel();
224
+ }, []);
225
+ useEffect(() => {
226
+ hiddenGraceTimeoutRef.current.cancel();
227
+ if (!requireVisible || pageVisibility.isVisible) {
228
+ hiddenSinceRef.current = null;
229
+ setHasExceededHiddenGrace(false);
230
+ setIsWaitingForVisibleGrace(false);
231
+ return;
232
+ }
233
+ const hiddenSince = hiddenSinceRef.current ?? Date.now();
234
+ hiddenSinceRef.current = hiddenSince;
235
+ if (hiddenGraceMs <= 0) {
236
+ setHasExceededHiddenGrace(true);
237
+ setIsWaitingForVisibleGrace(false);
238
+ return;
239
+ }
240
+ const elapsedMs = Date.now() - hiddenSince;
241
+ if (elapsedMs >= hiddenGraceMs) {
242
+ setHasExceededHiddenGrace(true);
243
+ setIsWaitingForVisibleGrace(false);
244
+ return;
245
+ }
246
+ setHasExceededHiddenGrace(false);
247
+ setIsWaitingForVisibleGrace(true);
248
+ hiddenGraceTimeoutRef.current.schedule(() => {
249
+ setHasExceededHiddenGrace(true);
250
+ setIsWaitingForVisibleGrace(false);
251
+ }, hiddenGraceMs - elapsedMs);
252
+ }, [hiddenGraceMs, pageVisibility.isVisible, requireVisible]);
253
+ let reason = "ready";
254
+ if (!enabled) {
255
+ reason = "manual";
256
+ } else if (requireOnline && !onlineStatus.isOnline) {
257
+ reason = "offline";
258
+ } else if (requireVisible && !pageVisibility.isVisible && hasExceededHiddenGrace) {
259
+ reason = "hidden";
260
+ }
261
+ const connect = reason === "ready";
262
+ const isBlocked = !connect;
263
+ useEffect(() => {
264
+ if (!trackTransitions) {
265
+ previousStateRef.current = {
266
+ connect,
267
+ reason
268
+ };
269
+ setTransitions(createEmptyTransitionState3);
270
+ return;
271
+ }
272
+ const previousState = previousStateRef.current;
273
+ if (previousState === null) {
274
+ previousStateRef.current = {
275
+ connect,
276
+ reason
277
+ };
278
+ return;
279
+ }
280
+ if (previousState.connect === connect && previousState.reason === reason) {
281
+ return;
282
+ }
283
+ const changedAt = Date.now();
284
+ previousStateRef.current = {
285
+ connect,
286
+ reason
287
+ };
288
+ setTransitions((current) => ({
289
+ becameBlockedAt: connect ? current.becameBlockedAt : changedAt,
290
+ becameReadyAt: connect ? changedAt : current.becameReadyAt,
291
+ lastChangedAt: changedAt
292
+ }));
293
+ }, [connect, reason, trackTransitions]);
294
+ return {
295
+ becameBlockedAt: transitions.becameBlockedAt,
296
+ becameReadyAt: transitions.becameReadyAt,
297
+ connect,
298
+ isBlocked,
299
+ isOnline: onlineStatus.isOnline,
300
+ isOnlineSupported: onlineStatus.isSupported,
301
+ isVisibilitySupported: pageVisibility.isSupported,
302
+ isVisible: pageVisibility.isVisible,
303
+ isWaitingForVisibleGrace,
304
+ lastChangedAt: transitions.lastChangedAt,
305
+ reason,
306
+ visibilityState: pageVisibility.visibilityState
307
+ };
308
+ };
73
309
 
74
310
  // src/core/reconnect.ts
75
311
  var DEFAULT_RECONNECT_OPTIONS = {
@@ -207,57 +443,6 @@ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {
207
443
  };
208
444
  };
209
445
 
210
- // src/core/timers.ts
211
- var sanitizeTimerDelay = (delayMs) => {
212
- if (!Number.isFinite(delayMs)) {
213
- return 0;
214
- }
215
- return Math.max(0, Math.round(delayMs));
216
- };
217
- var createManagedTimeout = () => {
218
- let timeoutId = null;
219
- return {
220
- cancel() {
221
- if (timeoutId !== null) {
222
- clearTimeout(timeoutId);
223
- timeoutId = null;
224
- }
225
- },
226
- isActive() {
227
- return timeoutId !== null;
228
- },
229
- schedule(callback, delayMs) {
230
- if (timeoutId !== null) {
231
- clearTimeout(timeoutId);
232
- }
233
- timeoutId = setTimeout(() => {
234
- timeoutId = null;
235
- callback();
236
- }, sanitizeTimerDelay(delayMs));
237
- }
238
- };
239
- };
240
- var createManagedInterval = () => {
241
- let intervalId = null;
242
- return {
243
- cancel() {
244
- if (intervalId !== null) {
245
- clearInterval(intervalId);
246
- intervalId = null;
247
- }
248
- },
249
- isActive() {
250
- return intervalId !== null;
251
- },
252
- start(callback, intervalMs) {
253
- if (intervalId !== null) {
254
- clearInterval(intervalId);
255
- }
256
- intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
257
- }
258
- };
259
- };
260
-
261
446
  // src/hooks/useReconnect.ts
262
447
  var createInitialState = (enabled) => ({
263
448
  attempt: 0,
@@ -1393,6 +1578,6 @@ var useEventSource = (options) => {
1393
1578
  };
1394
1579
  };
1395
1580
 
1396
- export { useEventSource, useHeartbeat, useOnlineStatus, useReconnect, useWebSocket };
1581
+ export { useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
1397
1582
  //# sourceMappingURL=index.js.map
1398
1583
  //# sourceMappingURL=index.js.map