@djangocfg/centrifugo 2.1.66 → 2.1.68

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
@@ -14,6 +14,7 @@ Professional Centrifugo WebSocket client with React integration, composable UI c
14
14
  ## Features
15
15
 
16
16
  - 🔌 **Robust WebSocket Connection** - Auto-reconnect, error handling, and connection state management
17
+ - 👁️ **Page Visibility Handling** - Auto-reconnect when tab becomes active, pause reconnects when hidden (saves battery)
17
18
  - 🔄 **RPC Pattern Support** - Request-response via correlation ID for synchronous-like communication
18
19
  - 📊 **Advanced Logging System** - Circular buffer with dual output (consola + in-memory accumulation)
19
20
  - 🧩 **Composable UI Components** - Flexible, reusable components for any use case
@@ -47,7 +48,9 @@ src/
47
48
  │ └── LogsProvider/ # Logs accumulation provider
48
49
  ├── hooks/ # React hooks
49
50
  │ ├── useSubscription.ts # Channel subscription hook
50
- └── useRPC.ts # RPC request-response hook
51
+ ├── useRPC.ts # RPC request-response hook
52
+ │ ├── useNamedRPC.ts # Native Centrifugo RPC hook
53
+ │ └── usePageVisibility.ts # Browser tab visibility tracking
51
54
  └── components/ # Composable UI components
52
55
  ├── ConnectionStatus/ # Connection status display
53
56
  │ ├── ConnectionStatus.tsx # Badge/inline/detailed variants
@@ -451,6 +454,82 @@ interface UseNamedRPCResult {
451
454
 
452
455
  > **Note:** `useNamedRPC` uses native Centrifugo RPC which requires RPC proxy to be configured in Centrifugo server. See the [Setup Guide](https://djangocfg.com/docs/features/integrations/centrifugo/client-generation/) for configuration details.
453
456
 
457
+ ### usePageVisibility()
458
+
459
+ Hook for tracking browser tab visibility. Built into `CentrifugoProvider` for automatic reconnection handling.
460
+
461
+ **Built-in Behavior (CentrifugoProvider):**
462
+
463
+ The provider automatically handles visibility changes:
464
+ - **Tab hidden**: Pauses reconnect attempts (saves battery)
465
+ - **Tab visible**: Triggers reconnect if connection was lost
466
+
467
+ **Standalone Usage:**
468
+
469
+ ```tsx
470
+ import { usePageVisibility } from '@djangocfg/centrifugo';
471
+
472
+ function MyComponent() {
473
+ const { isVisible, wasHidden, hiddenDuration } = usePageVisibility({
474
+ onVisible: () => {
475
+ console.log('Tab is now visible');
476
+ // Refresh data, resume animations, etc.
477
+ },
478
+ onHidden: () => {
479
+ console.log('Tab is now hidden');
480
+ // Pause expensive operations
481
+ },
482
+ onChange: (isVisible) => {
483
+ console.log('Visibility changed:', isVisible);
484
+ },
485
+ });
486
+
487
+ return (
488
+ <div>
489
+ <p>Tab visible: {isVisible ? 'Yes' : 'No'}</p>
490
+ <p>Was hidden: {wasHidden ? 'Yes' : 'No'}</p>
491
+ {hiddenDuration > 0 && (
492
+ <p>Was hidden for: {Math.round(hiddenDuration / 1000)}s</p>
493
+ )}
494
+ </div>
495
+ );
496
+ }
497
+ ```
498
+
499
+ **Return Value:**
500
+ ```typescript
501
+ interface UsePageVisibilityResult {
502
+ /** Whether the page is currently visible */
503
+ isVisible: boolean;
504
+ /** Whether the page was ever hidden during this session */
505
+ wasHidden: boolean;
506
+ /** Timestamp when page became visible */
507
+ visibleSince: number | null;
508
+ /** How long the page was hidden (ms) */
509
+ hiddenDuration: number;
510
+ /** Force check visibility state */
511
+ checkVisibility: () => boolean;
512
+ }
513
+ ```
514
+
515
+ **Options:**
516
+ ```typescript
517
+ interface UsePageVisibilityOptions {
518
+ /** Callback when page becomes visible */
519
+ onVisible?: () => void;
520
+ /** Callback when page becomes hidden */
521
+ onHidden?: () => void;
522
+ /** Callback with visibility state change */
523
+ onChange?: (isVisible: boolean) => void;
524
+ }
525
+ ```
526
+
527
+ **Use Cases:**
528
+ - Pause/resume WebSocket reconnection attempts
529
+ - Refresh stale data when tab becomes active
530
+ - Pause expensive animations or timers
531
+ - Track user engagement metrics
532
+
454
533
  ### namedRPCNoWait() - Fire-and-Forget RPC
455
534
 
456
535
  For latency-sensitive operations (like terminal input), use the fire-and-forget variant that returns immediately without waiting for a response.
@@ -1032,6 +1111,9 @@ import type {
1032
1111
  // Hooks
1033
1112
  UseSubscriptionOptions,
1034
1113
  UseSubscriptionResult,
1114
+ UsePageVisibilityOptions,
1115
+ UsePageVisibilityResult,
1116
+ PageVisibilityState,
1035
1117
 
1036
1118
  // Components
1037
1119
  ConnectionStatusProps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/centrifugo",
3
- "version": "2.1.66",
3
+ "version": "2.1.68",
4
4
  "description": "Production-ready Centrifugo WebSocket client for React with real-time subscriptions, RPC patterns, and connection state management",
5
5
  "keywords": [
6
6
  "centrifugo",
@@ -51,9 +51,9 @@
51
51
  "centrifuge": "^5.2.2"
52
52
  },
53
53
  "peerDependencies": {
54
- "@djangocfg/api": "^2.1.66",
55
- "@djangocfg/ui-nextjs": "^2.1.66",
56
- "@djangocfg/layouts": "^2.1.66",
54
+ "@djangocfg/api": "^2.1.68",
55
+ "@djangocfg/ui-nextjs": "^2.1.68",
56
+ "@djangocfg/layouts": "^2.1.68",
57
57
  "consola": "^3.4.2",
58
58
  "lucide-react": "^0.545.0",
59
59
  "moment": "^2.30.1",
@@ -61,7 +61,7 @@
61
61
  "react-dom": "^19.1.0"
62
62
  },
63
63
  "devDependencies": {
64
- "@djangocfg/typescript-config": "^2.1.66",
64
+ "@djangocfg/typescript-config": "^2.1.68",
65
65
  "@types/react": "^19.1.0",
66
66
  "@types/react-dom": "^19.1.0",
67
67
  "moment": "^2.30.1",
@@ -10,3 +10,10 @@ export type { UseRPCOptions, UseRPCResult } from './useRPC';
10
10
 
11
11
  export { useNamedRPC } from './useNamedRPC';
12
12
  export type { UseNamedRPCOptions, UseNamedRPCResult } from './useNamedRPC';
13
+
14
+ export { usePageVisibility } from './usePageVisibility';
15
+ export type {
16
+ PageVisibilityState,
17
+ UsePageVisibilityOptions,
18
+ UsePageVisibilityResult,
19
+ } from './usePageVisibility';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * usePageVisibility Hook
3
+ *
4
+ * Tracks browser tab visibility state using the Page Visibility API.
5
+ * Returns true when the page is visible, false when hidden.
6
+ *
7
+ * Use cases:
8
+ * - Pause/resume WebSocket connections
9
+ * - Pause expensive operations when tab is inactive
10
+ * - Trigger data sync when tab becomes visible
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import { useEffect, useState, useCallback, useRef } from 'react';
16
+
17
+ // =============================================================================
18
+ // TYPES
19
+ // =============================================================================
20
+
21
+ export interface PageVisibilityState {
22
+ /** Whether the page is currently visible */
23
+ isVisible: boolean;
24
+ /** Whether the page was ever hidden during this session */
25
+ wasHidden: boolean;
26
+ /** Timestamp when page became visible (for uptime tracking) */
27
+ visibleSince: number | null;
28
+ /** How long the page was hidden (ms), reset when visible */
29
+ hiddenDuration: number;
30
+ }
31
+
32
+ export interface UsePageVisibilityOptions {
33
+ /** Callback when page becomes visible */
34
+ onVisible?: () => void;
35
+ /** Callback when page becomes hidden */
36
+ onHidden?: () => void;
37
+ /** Callback with visibility state change */
38
+ onChange?: (isVisible: boolean) => void;
39
+ }
40
+
41
+ export interface UsePageVisibilityResult extends PageVisibilityState {
42
+ /** Force check visibility state */
43
+ checkVisibility: () => boolean;
44
+ }
45
+
46
+ // =============================================================================
47
+ // HELPERS
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Get current visibility state
52
+ * SSR-safe: returns true on server
53
+ */
54
+ function getVisibilityState(): boolean {
55
+ if (typeof document === 'undefined') return true;
56
+ return document.visibilityState === 'visible';
57
+ }
58
+
59
+ // =============================================================================
60
+ // HOOK
61
+ // =============================================================================
62
+
63
+ export function usePageVisibility(
64
+ options: UsePageVisibilityOptions = {}
65
+ ): UsePageVisibilityResult {
66
+ const { onVisible, onHidden, onChange } = options;
67
+
68
+ const [state, setState] = useState<PageVisibilityState>(() => ({
69
+ isVisible: getVisibilityState(),
70
+ wasHidden: false,
71
+ visibleSince: getVisibilityState() ? Date.now() : null,
72
+ hiddenDuration: 0,
73
+ }));
74
+
75
+ // Refs to avoid stale closures in callbacks
76
+ const onVisibleRef = useRef(onVisible);
77
+ const onHiddenRef = useRef(onHidden);
78
+ const onChangeRef = useRef(onChange);
79
+ const hiddenAtRef = useRef<number | null>(null);
80
+
81
+ // Keep refs updated
82
+ onVisibleRef.current = onVisible;
83
+ onHiddenRef.current = onHidden;
84
+ onChangeRef.current = onChange;
85
+
86
+ const checkVisibility = useCallback((): boolean => {
87
+ return getVisibilityState();
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ if (typeof document === 'undefined') return;
92
+
93
+ const handleVisibilityChange = () => {
94
+ const isNowVisible = getVisibilityState();
95
+
96
+ setState((prev) => {
97
+ // Calculate hidden duration if becoming visible
98
+ let hiddenDuration = prev.hiddenDuration;
99
+ if (isNowVisible && hiddenAtRef.current !== null) {
100
+ hiddenDuration = Date.now() - hiddenAtRef.current;
101
+ hiddenAtRef.current = null;
102
+ }
103
+
104
+ // Track when we became hidden
105
+ if (!isNowVisible) {
106
+ hiddenAtRef.current = Date.now();
107
+ }
108
+
109
+ return {
110
+ isVisible: isNowVisible,
111
+ wasHidden: prev.wasHidden || !isNowVisible,
112
+ visibleSince: isNowVisible ? (prev.visibleSince ?? Date.now()) : null,
113
+ hiddenDuration,
114
+ };
115
+ });
116
+
117
+ // Trigger callbacks
118
+ onChangeRef.current?.(isNowVisible);
119
+
120
+ if (isNowVisible) {
121
+ onVisibleRef.current?.();
122
+ } else {
123
+ onHiddenRef.current?.();
124
+ }
125
+ };
126
+
127
+ document.addEventListener('visibilitychange', handleVisibilityChange);
128
+
129
+ return () => {
130
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
131
+ };
132
+ }, []);
133
+
134
+ return {
135
+ ...state,
136
+ checkVisibility,
137
+ };
138
+ }
139
+
140
+ export default usePageVisibility;
package/src/index.ts CHANGED
@@ -92,6 +92,19 @@ export type {
92
92
  UseSubscriptionResult,
93
93
  } from './hooks/useSubscription';
94
94
 
95
+ export { useRPC } from './hooks/useRPC';
96
+ export type { UseRPCOptions, UseRPCResult } from './hooks/useRPC';
97
+
98
+ export { useNamedRPC } from './hooks/useNamedRPC';
99
+ export type { UseNamedRPCOptions, UseNamedRPCResult } from './hooks/useNamedRPC';
100
+
101
+ export { usePageVisibility } from './hooks/usePageVisibility';
102
+ export type {
103
+ PageVisibilityState,
104
+ UsePageVisibilityOptions,
105
+ UsePageVisibilityResult,
106
+ } from './hooks/usePageVisibility';
107
+
95
108
  export { useCodegenTip } from './hooks/useCodegenTip';
96
109
 
97
110
  // ─────────────────────────────────────────────────────────────────────────
@@ -20,6 +20,7 @@ import { isDevelopment, isStaticBuild, reconnectConfig } from '../../config';
20
20
  import { CentrifugoRPCClient } from '../../core/client';
21
21
  import { getConsolaLogger } from '../../core/logger/consolaLogger';
22
22
  import { useCodegenTip } from '../../hooks/useCodegenTip';
23
+ import { usePageVisibility } from '../../hooks/usePageVisibility';
23
24
  import { LogsProvider } from '../LogsProvider';
24
25
 
25
26
  import type { ConnectionState, CentrifugoToken, ActiveSubscription } from '../../core/types';
@@ -113,6 +114,7 @@ function CentrifugoProviderInner({
113
114
  const devWarningShownRef = useRef(false); // Track if server unavailable warning was shown
114
115
  const connectRef = useRef<(() => Promise<void>) | null>(null);
115
116
  const disconnectRef = useRef<(() => void) | null>(null);
117
+ const wasConnectedBeforeHiddenRef = useRef(false); // Track connection state before page hidden
116
118
 
117
119
  const centrifugoToken: CentrifugoToken | undefined = user?.centrifugo;
118
120
  const hasCentrifugoToken = !!centrifugoToken?.token;
@@ -413,6 +415,50 @@ function CentrifugoProviderInner({
413
415
  };
414
416
  }, [autoConnect]); // Only depend on autoConnect, not on connect/disconnect
415
417
 
418
+ // ==========================================================================
419
+ // PAGE VISIBILITY HANDLING
420
+ // ==========================================================================
421
+ // When tab becomes hidden: pause reconnect attempts (save battery)
422
+ // When tab becomes visible: trigger reconnect if needed
423
+
424
+ usePageVisibility({
425
+ onHidden: () => {
426
+ // Save connection state before hiding
427
+ wasConnectedBeforeHiddenRef.current = isConnected;
428
+
429
+ // Pause reconnect attempts while tab is hidden (save battery)
430
+ if (reconnectTimeoutRef.current) {
431
+ clearTimeout(reconnectTimeoutRef.current);
432
+ reconnectTimeoutRef.current = null;
433
+ logger.debug('Paused reconnect attempts (tab hidden)');
434
+ }
435
+ },
436
+ onVisible: () => {
437
+ // Only attempt reconnect if:
438
+ // 1. We should auto-connect
439
+ // 2. We're not already connected or connecting
440
+ // 3. We haven't stopped reconnecting
441
+ // 4. We were previously connected (or never connected yet)
442
+ const shouldReconnect =
443
+ autoConnect &&
444
+ !isConnected &&
445
+ !isConnectingRef.current &&
446
+ !reconnectStoppedRef.current &&
447
+ (wasConnectedBeforeHiddenRef.current || !hasConnectedRef.current);
448
+
449
+ if (shouldReconnect) {
450
+ // Reset reconnect attempts for fresh start
451
+ reconnectAttemptRef.current = 0;
452
+ devWarningShownRef.current = false;
453
+
454
+ logger.info('Tab visible - attempting reconnect...');
455
+ connectRef.current?.();
456
+ } else if (isConnected) {
457
+ logger.debug('Tab visible - connection still active');
458
+ }
459
+ },
460
+ });
461
+
416
462
  const connectionState: ConnectionState = isConnected
417
463
  ? 'connected'
418
464
  : isConnecting