react-realtime-hooks 1.1.0 → 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
@@ -29,6 +29,34 @@ type UsePageVisibilityHook = (options?: UsePageVisibilityOptions) => UsePageVisi
29
29
 
30
30
  declare const usePageVisibility: UsePageVisibilityHook;
31
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
+
32
60
  type ReconnectStatus = "idle" | "scheduled" | "running" | "stopped";
33
61
  type ReconnectTrigger = "mount" | "manual" | "close" | "error" | "heartbeat-timeout" | "offline" | "online" | "visibility";
34
62
  interface ReconnectAttempt {
@@ -235,4 +263,4 @@ type UseEventSourceHook = <TMessage = unknown>(options: UseEventSourceOptions<TM
235
263
 
236
264
  declare const useEventSource: UseEventSourceHook;
237
265
 
238
- 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 UsePageVisibilityHook, type UsePageVisibilityOptions, type UsePageVisibilityResult, type UseReconnectHook, type UseReconnectOptions, type UseReconnectResult, type UseWebSocketHeartbeatOptions, type UseWebSocketHook, type UseWebSocketOptions, type UseWebSocketResult, type WebSocketHeartbeatAction, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, 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
@@ -136,6 +136,177 @@ var usePageVisibility = (options = {}) => {
136
136
  };
137
137
  };
138
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
+ };
309
+
139
310
  // src/core/reconnect.ts
140
311
  var DEFAULT_RECONNECT_OPTIONS = {
141
312
  backoffFactor: 2,
@@ -272,57 +443,6 @@ var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {
272
443
  };
273
444
  };
274
445
 
275
- // src/core/timers.ts
276
- var sanitizeTimerDelay = (delayMs) => {
277
- if (!Number.isFinite(delayMs)) {
278
- return 0;
279
- }
280
- return Math.max(0, Math.round(delayMs));
281
- };
282
- var createManagedTimeout = () => {
283
- let timeoutId = null;
284
- return {
285
- cancel() {
286
- if (timeoutId !== null) {
287
- clearTimeout(timeoutId);
288
- timeoutId = null;
289
- }
290
- },
291
- isActive() {
292
- return timeoutId !== null;
293
- },
294
- schedule(callback, delayMs) {
295
- if (timeoutId !== null) {
296
- clearTimeout(timeoutId);
297
- }
298
- timeoutId = setTimeout(() => {
299
- timeoutId = null;
300
- callback();
301
- }, sanitizeTimerDelay(delayMs));
302
- }
303
- };
304
- };
305
- var createManagedInterval = () => {
306
- let intervalId = null;
307
- return {
308
- cancel() {
309
- if (intervalId !== null) {
310
- clearInterval(intervalId);
311
- intervalId = null;
312
- }
313
- },
314
- isActive() {
315
- return intervalId !== null;
316
- },
317
- start(callback, intervalMs) {
318
- if (intervalId !== null) {
319
- clearInterval(intervalId);
320
- }
321
- intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
322
- }
323
- };
324
- };
325
-
326
446
  // src/hooks/useReconnect.ts
327
447
  var createInitialState = (enabled) => ({
328
448
  attempt: 0,
@@ -1458,6 +1578,6 @@ var useEventSource = (options) => {
1458
1578
  };
1459
1579
  };
1460
1580
 
1461
- export { useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
1581
+ export { useConnectionGate, useEventSource, useHeartbeat, useOnlineStatus, usePageVisibility, useReconnect, useWebSocket };
1462
1582
  //# sourceMappingURL=index.js.map
1463
1583
  //# sourceMappingURL=index.js.map