@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 +83 -1
- package/package.json +5 -5
- package/src/hooks/index.ts +7 -0
- package/src/hooks/usePageVisibility.ts +140 -0
- package/src/index.ts +13 -0
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +46 -0
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
|
-
│
|
|
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.
|
|
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.
|
|
55
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
56
|
-
"@djangocfg/layouts": "^2.1.
|
|
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.
|
|
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",
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|