@djangocfg/centrifugo 1.0.1 → 1.0.3
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 +345 -34
- package/package.json +6 -4
- package/src/config.ts +1 -1
- package/src/core/client/CentrifugoRPCClient.ts +281 -0
- package/src/core/client/index.ts +5 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger/LogsStore.ts +101 -0
- package/src/core/logger/createLogger.ts +79 -0
- package/src/core/logger/index.ts +9 -0
- package/src/core/types/index.ts +68 -0
- package/src/debug/ConnectionTab/ConnectionTab.tsx +160 -0
- package/src/debug/ConnectionTab/index.ts +5 -0
- package/src/debug/DebugPanel/DebugPanel.tsx +88 -0
- package/src/debug/DebugPanel/index.ts +5 -0
- package/src/debug/LogsTab/LogsTab.tsx +236 -0
- package/src/debug/LogsTab/index.ts +5 -0
- package/src/debug/SubscriptionsTab/SubscriptionsTab.tsx +135 -0
- package/src/debug/SubscriptionsTab/index.ts +5 -0
- package/src/debug/index.ts +11 -0
- package/src/hooks/index.ts +2 -5
- package/src/hooks/useSubscription.ts +66 -65
- package/src/index.ts +94 -13
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +380 -0
- package/src/providers/CentrifugoProvider/index.ts +6 -0
- package/src/providers/LogsProvider/LogsProvider.tsx +107 -0
- package/src/providers/LogsProvider/index.ts +6 -0
- package/src/providers/index.ts +9 -0
- package/API_GENERATOR.md +0 -253
- package/src/components/CentrifugoDebug.tsx +0 -182
- package/src/components/index.ts +0 -5
- package/src/context/CentrifugoProvider.tsx +0 -228
- package/src/context/index.ts +0 -5
- package/src/hooks/useLogger.ts +0 -69
- package/src/types/index.ts +0 -45
|
@@ -1,86 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* useSubscription Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* // Subscribe to bot heartbeat
|
|
8
|
-
* useSubscription('bot#bot-123#heartbeat', (data) => {
|
|
9
|
-
* console.log('Heartbeat:', data);
|
|
10
|
-
* updateMetrics(data);
|
|
11
|
-
* });
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* // Conditional subscription
|
|
15
|
-
* useSubscription(
|
|
16
|
-
* 'bot#bot-123#status',
|
|
17
|
-
* (data) => console.log('Status:', data),
|
|
18
|
-
* { enabled: isMonitoring }
|
|
19
|
-
* );
|
|
4
|
+
* Subscribe to Centrifugo channel with auto-cleanup.
|
|
20
5
|
*/
|
|
21
6
|
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useEffect, useCallback, useRef, useState } from 'react';
|
|
10
|
+
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
11
|
+
import { createLogger } from '../core/logger';
|
|
24
12
|
|
|
25
|
-
export interface
|
|
26
|
-
|
|
27
|
-
* Enable/disable subscription.
|
|
28
|
-
* Defaults to true.
|
|
29
|
-
*/
|
|
13
|
+
export interface UseSubscriptionOptions<T = any> {
|
|
14
|
+
channel: string;
|
|
30
15
|
enabled?: boolean;
|
|
16
|
+
onPublication?: (data: T) => void;
|
|
17
|
+
onError?: (error: Error) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseSubscriptionResult<T = any> {
|
|
21
|
+
data: T | null;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
isSubscribed: boolean;
|
|
24
|
+
unsubscribe: () => void;
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
/**
|
|
34
|
-
* Hook for subscribing to a Centrifugo channel.
|
|
35
|
-
*
|
|
36
|
-
* @param channel - Channel name (e.g., 'bot#bot-123#heartbeat'). Set to null to disable.
|
|
37
|
-
* @param callback - Callback function for received messages
|
|
38
|
-
* @param options - Subscription options
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```tsx
|
|
42
|
-
* function BotMonitor({ botId }: { botId: string }) {
|
|
43
|
-
* const [heartbeat, setHeartbeat] = useState(null);
|
|
44
|
-
*
|
|
45
|
-
* useSubscription(`bot#${botId}#heartbeat`, (data) => {
|
|
46
|
-
* setHeartbeat(data);
|
|
47
|
-
* });
|
|
48
|
-
*
|
|
49
|
-
* return <div>CPU: {heartbeat?.cpu_usage}%</div>;
|
|
50
|
-
* }
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
27
|
export function useSubscription<T = any>(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const { baseClient, isConnected } = useCentrifugo();
|
|
59
|
-
const callbackRef = useRef(callback);
|
|
60
|
-
const { enabled = true } = options;
|
|
28
|
+
options: UseSubscriptionOptions<T>
|
|
29
|
+
): UseSubscriptionResult<T> {
|
|
30
|
+
const { client, isConnected } = useCentrifugo();
|
|
31
|
+
const { channel, enabled = true, onPublication, onError } = options;
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}, [callback]);
|
|
33
|
+
const [data, setData] = useState<T | null>(null);
|
|
34
|
+
const [error, setError] = useState<Error | null>(null);
|
|
35
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
66
36
|
|
|
37
|
+
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
38
|
+
const logger = useRef(createLogger({ source: 'subscription' })).current;
|
|
39
|
+
|
|
40
|
+
// Unsubscribe function
|
|
41
|
+
const unsubscribe = useCallback(() => {
|
|
42
|
+
if (unsubscribeRef.current) {
|
|
43
|
+
unsubscribeRef.current();
|
|
44
|
+
unsubscribeRef.current = null;
|
|
45
|
+
setIsSubscribed(false);
|
|
46
|
+
logger.info(`Unsubscribed from channel: ${channel}`);
|
|
47
|
+
}
|
|
48
|
+
}, [channel, logger]);
|
|
49
|
+
|
|
50
|
+
// Subscribe effect
|
|
67
51
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
if (!enabled || !isConnected || !baseClient || !channel) {
|
|
52
|
+
if (!client || !isConnected || !enabled) {
|
|
70
53
|
return;
|
|
71
54
|
}
|
|
72
55
|
|
|
73
|
-
|
|
56
|
+
logger.info(`Subscribing to channel: ${channel}`);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const unsub = client.subscribe(channel, (receivedData: T) => {
|
|
60
|
+
setData(receivedData);
|
|
61
|
+
setError(null);
|
|
62
|
+
onPublication?.(receivedData);
|
|
63
|
+
});
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
65
|
+
unsubscribeRef.current = unsub;
|
|
66
|
+
setIsSubscribed(true);
|
|
67
|
+
logger.success(`Subscribed to channel: ${channel}`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const subscriptionError = err instanceof Error ? err : new Error('Subscription failed');
|
|
70
|
+
setError(subscriptionError);
|
|
71
|
+
onError?.(subscriptionError);
|
|
72
|
+
logger.error(`Subscription failed: ${channel}`, subscriptionError);
|
|
73
|
+
}
|
|
79
74
|
|
|
80
|
-
// Cleanup on unmount or
|
|
75
|
+
// Cleanup on unmount or deps change
|
|
81
76
|
return () => {
|
|
82
|
-
console.log(`🎣 useSubscription: Unsubscribing from ${channel}`);
|
|
83
77
|
unsubscribe();
|
|
84
78
|
};
|
|
85
|
-
}, [
|
|
79
|
+
}, [client, isConnected, enabled, channel, onPublication, onError, unsubscribe, logger]);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
data,
|
|
83
|
+
error,
|
|
84
|
+
isSubscribed,
|
|
85
|
+
unsubscribe,
|
|
86
|
+
};
|
|
86
87
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @djangocfg/centrifugo
|
|
3
3
|
*
|
|
4
|
-
* WebSocket client
|
|
4
|
+
* Professional Centrifugo WebSocket client with React integration.
|
|
5
|
+
*
|
|
6
|
+
* ## Architecture
|
|
7
|
+
*
|
|
8
|
+
* - **Core**: Platform-agnostic client, logger, and types
|
|
9
|
+
* - **Providers**: React Context for connection and logs management
|
|
10
|
+
* - **Hooks**: React hooks for subscriptions
|
|
11
|
+
* - **Debug**: Development-only debug UI (lazy loaded)
|
|
12
|
+
*
|
|
13
|
+
* ## Usage
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { CentrifugoProvider, useCentrifugo, useSubscription, DebugPanel } from '@djangocfg/centrifugo';
|
|
17
|
+
*
|
|
18
|
+
* function App() {
|
|
19
|
+
* return (
|
|
20
|
+
* <CentrifugoProvider
|
|
21
|
+
* url="ws://localhost:8000/ws"
|
|
22
|
+
* getToken={async () => ({ token: 'xxx', user: 'user123' })}
|
|
23
|
+
* >
|
|
24
|
+
* <YourApp />
|
|
25
|
+
* <DebugPanel />
|
|
26
|
+
* </CentrifugoProvider>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* function YourComponent() {
|
|
31
|
+
* const { isConnected, client } = useCentrifugo();
|
|
32
|
+
*
|
|
33
|
+
* useSubscription({
|
|
34
|
+
* channel: 'notifications',
|
|
35
|
+
* onPublication: (data) => console.log(data),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* return <div>Connected: {isConnected ? 'Yes' : 'No'}</div>;
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
5
41
|
*/
|
|
6
42
|
|
|
7
|
-
//
|
|
8
|
-
export
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Re-export Centrifuge for advanced usage
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
9
46
|
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
export type { CentrifugoLogger, RPCLogger } from './hooks';
|
|
13
|
-
export type { useSubscriptionOptions } from './hooks';
|
|
47
|
+
export { Centrifuge } from 'centrifuge';
|
|
48
|
+
export type { Subscription } from 'centrifuge';
|
|
14
49
|
|
|
15
|
-
//
|
|
16
|
-
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Core (Platform-agnostic)
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
17
53
|
|
|
18
|
-
|
|
54
|
+
export { CentrifugoRPCClient } from './core/client/CentrifugoRPCClient';
|
|
55
|
+
export { createLogger, getGlobalLogsStore } from './core/logger';
|
|
19
56
|
export type {
|
|
57
|
+
// Connection
|
|
58
|
+
ConnectionState,
|
|
20
59
|
CentrifugoToken,
|
|
21
60
|
User,
|
|
61
|
+
|
|
62
|
+
// Logs
|
|
63
|
+
LogLevel,
|
|
64
|
+
LogEntry,
|
|
65
|
+
|
|
66
|
+
// Subscriptions
|
|
67
|
+
ActiveSubscription,
|
|
68
|
+
|
|
69
|
+
// Client
|
|
70
|
+
CentrifugoClientConfig,
|
|
71
|
+
CentrifugoClientState,
|
|
72
|
+
} from './core/types';
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Providers (React Context)
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export { CentrifugoProvider, useCentrifugo } from './providers/CentrifugoProvider';
|
|
79
|
+
export { LogsProvider, useLogs } from './providers/LogsProvider';
|
|
80
|
+
export type {
|
|
22
81
|
CentrifugoProviderProps,
|
|
23
82
|
CentrifugoContextValue,
|
|
24
|
-
} from './
|
|
83
|
+
} from './providers/CentrifugoProvider';
|
|
84
|
+
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// Hooks
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export { useSubscription } from './hooks/useSubscription';
|
|
90
|
+
export type {
|
|
91
|
+
UseSubscriptionOptions,
|
|
92
|
+
UseSubscriptionResult,
|
|
93
|
+
} from './hooks/useSubscription';
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Configuration
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export { centrifugoConfig, isDevelopment, isProduction, isStaticBuild } from './config';
|
|
100
|
+
export type { CentrifugoConfig } from './config';
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Debug UI (Embedded in Provider, not for direct use)
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
25
105
|
|
|
26
|
-
//
|
|
27
|
-
|
|
106
|
+
// Note: DebugPanel is now automatically embedded in CentrifugoProvider
|
|
107
|
+
// It shows automatically in development or for admin users
|
|
108
|
+
// No need to manually add <DebugPanel /> to your app
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Provider
|
|
3
|
+
*
|
|
4
|
+
* Main provider that manages WebSocket connection.
|
|
5
|
+
* Wraps LogsProvider to provide logs accumulation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense, type ReactNode } from 'react';
|
|
11
|
+
import { useAuth } from '@djangocfg/layouts';
|
|
12
|
+
import { CentrifugoRPCClient } from '../../core/client';
|
|
13
|
+
import { createLogger } from '../../core/logger';
|
|
14
|
+
import type { ConnectionState, CentrifugoToken } from '../../core/types';
|
|
15
|
+
import { LogsProvider } from '../LogsProvider';
|
|
16
|
+
import { isDevelopment, isStaticBuild } from '../../config';
|
|
17
|
+
|
|
18
|
+
// Lazy load DebugPanel to reduce bundle size
|
|
19
|
+
const DebugPanel = lazy(() =>
|
|
20
|
+
import('../../debug/DebugPanel').then((m) => ({ default: m.DebugPanel }))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Context
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface CentrifugoContextValue {
|
|
28
|
+
// Client
|
|
29
|
+
client: CentrifugoRPCClient | null;
|
|
30
|
+
|
|
31
|
+
// Connection State
|
|
32
|
+
isConnected: boolean;
|
|
33
|
+
isConnecting: boolean;
|
|
34
|
+
error: Error | null;
|
|
35
|
+
connectionState: ConnectionState;
|
|
36
|
+
|
|
37
|
+
// Connection Info
|
|
38
|
+
uptime: number; // seconds
|
|
39
|
+
subscriptions: string[];
|
|
40
|
+
activeSubscriptions: import('../../core/types').ActiveSubscription[];
|
|
41
|
+
|
|
42
|
+
// Controls
|
|
43
|
+
connect: () => Promise<void>;
|
|
44
|
+
disconnect: () => void;
|
|
45
|
+
reconnect: () => Promise<void>;
|
|
46
|
+
unsubscribe: (channel: string) => void;
|
|
47
|
+
|
|
48
|
+
// Config
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CentrifugoContext = createContext<CentrifugoContextValue | undefined>(undefined);
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Provider Props
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export interface CentrifugoProviderProps {
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
url?: string;
|
|
62
|
+
autoConnect?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Inner Provider (has access to LogsProvider)
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function CentrifugoProviderInner({
|
|
70
|
+
children,
|
|
71
|
+
enabled = false,
|
|
72
|
+
url,
|
|
73
|
+
autoConnect: autoConnectProp = true,
|
|
74
|
+
}: CentrifugoProviderProps) {
|
|
75
|
+
const { isAuthenticated, isLoading, user, isAdminUser } = useAuth();
|
|
76
|
+
|
|
77
|
+
const [client, setClient] = useState<CentrifugoRPCClient | null>(null);
|
|
78
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
79
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
80
|
+
const [error, setError] = useState<Error | null>(null);
|
|
81
|
+
const [connectionTime, setConnectionTime] = useState<Date | null>(null);
|
|
82
|
+
const [uptime, setUptime] = useState<number>(0);
|
|
83
|
+
const [subscriptions, setSubscriptions] = useState<string[]>([]);
|
|
84
|
+
const [activeSubscriptions, setActiveSubscriptions] = useState<import('../../core/types').ActiveSubscription[]>([]);
|
|
85
|
+
|
|
86
|
+
const logger = useMemo(() => createLogger({ source: 'provider' }), []);
|
|
87
|
+
|
|
88
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
89
|
+
const hasConnectedRef = useRef(false);
|
|
90
|
+
const isConnectingRef = useRef(false);
|
|
91
|
+
const isMountedRef = useRef(true);
|
|
92
|
+
|
|
93
|
+
const centrifugoToken: CentrifugoToken | undefined = user?.centrifugo;
|
|
94
|
+
const hasCentrifugoToken = !!centrifugoToken?.token;
|
|
95
|
+
|
|
96
|
+
const wsUrl = useMemo(() => {
|
|
97
|
+
if (url) return url;
|
|
98
|
+
if (centrifugoToken?.centrifugo_url) return centrifugoToken.centrifugo_url;
|
|
99
|
+
return '';
|
|
100
|
+
}, [url, centrifugoToken?.centrifugo_url]);
|
|
101
|
+
|
|
102
|
+
const autoConnect = autoConnectProp &&
|
|
103
|
+
(isAuthenticated && !isLoading) &&
|
|
104
|
+
enabled &&
|
|
105
|
+
hasCentrifugoToken;
|
|
106
|
+
|
|
107
|
+
// Log connection decision
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!isLoading) {
|
|
110
|
+
logger.info(`Auto-connect: ${autoConnect ? 'YES' : 'NO'}`, {
|
|
111
|
+
authenticated: isAuthenticated,
|
|
112
|
+
loading: isLoading,
|
|
113
|
+
enabled,
|
|
114
|
+
hasToken: hasCentrifugoToken,
|
|
115
|
+
url: wsUrl,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}, [autoConnect, isAuthenticated, isLoading, enabled, hasCentrifugoToken, logger, wsUrl]);
|
|
119
|
+
|
|
120
|
+
// Update uptime every second
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isConnected || !connectionTime) {
|
|
123
|
+
setUptime(0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const updateUptime = () => {
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const diff = Math.floor((now.getTime() - connectionTime.getTime()) / 1000);
|
|
130
|
+
setUptime(diff);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
updateUptime();
|
|
134
|
+
const interval = setInterval(updateUptime, 1000);
|
|
135
|
+
|
|
136
|
+
return () => clearInterval(interval);
|
|
137
|
+
}, [isConnected, connectionTime]);
|
|
138
|
+
|
|
139
|
+
// Update subscriptions periodically
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!client || !isConnected) {
|
|
142
|
+
setSubscriptions([]);
|
|
143
|
+
setActiveSubscriptions([]);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const updateSubs = () => {
|
|
148
|
+
try {
|
|
149
|
+
const subs = client.getAllSubscriptions?.() || [];
|
|
150
|
+
setSubscriptions(subs);
|
|
151
|
+
|
|
152
|
+
// Convert to ActiveSubscription format
|
|
153
|
+
const activeSubs: import('../../core/types').ActiveSubscription[] = subs.map((channel) => ({
|
|
154
|
+
channel,
|
|
155
|
+
type: 'client' as const,
|
|
156
|
+
subscribedAt: Date.now(),
|
|
157
|
+
}));
|
|
158
|
+
setActiveSubscriptions(activeSubs);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error('Failed to get subscriptions', error);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
updateSubs();
|
|
165
|
+
const interval = setInterval(updateSubs, 2000);
|
|
166
|
+
|
|
167
|
+
return () => clearInterval(interval);
|
|
168
|
+
}, [client, isConnected, logger]);
|
|
169
|
+
|
|
170
|
+
// Connect function
|
|
171
|
+
const connect = useCallback(async () => {
|
|
172
|
+
if (hasConnectedRef.current || isConnectingRef.current) return;
|
|
173
|
+
if (isConnecting || isConnected) return;
|
|
174
|
+
|
|
175
|
+
isConnectingRef.current = true;
|
|
176
|
+
setIsConnecting(true);
|
|
177
|
+
setError(null);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
logger.info('Connecting to WebSocket server...');
|
|
181
|
+
|
|
182
|
+
if (!centrifugoToken?.token) {
|
|
183
|
+
throw new Error('No Centrifugo token available');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const token = centrifugoToken.token;
|
|
187
|
+
let userId = user?.id?.toString() || '1';
|
|
188
|
+
|
|
189
|
+
if (!user?.id) {
|
|
190
|
+
try {
|
|
191
|
+
const tokenPayload = JSON.parse(atob(token.split('.')[1]));
|
|
192
|
+
userId = tokenPayload.user_id?.toString() || tokenPayload.sub?.toString() || '1';
|
|
193
|
+
} catch (err) {
|
|
194
|
+
// Fallback
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rpcClient = new CentrifugoRPCClient(wsUrl, token, userId, 30000, logger);
|
|
199
|
+
await rpcClient.connect();
|
|
200
|
+
|
|
201
|
+
if (!isMountedRef.current) {
|
|
202
|
+
rpcClient.disconnect();
|
|
203
|
+
isConnectingRef.current = false;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hasConnectedRef.current = true;
|
|
208
|
+
isConnectingRef.current = false;
|
|
209
|
+
|
|
210
|
+
setClient(rpcClient);
|
|
211
|
+
setIsConnected(true);
|
|
212
|
+
setConnectionTime(new Date());
|
|
213
|
+
setError(null);
|
|
214
|
+
|
|
215
|
+
logger.success('WebSocket connected');
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const error = err instanceof Error ? err : new Error('Connection failed');
|
|
218
|
+
setError(error);
|
|
219
|
+
setClient(null);
|
|
220
|
+
setIsConnected(false);
|
|
221
|
+
setConnectionTime(null);
|
|
222
|
+
hasConnectedRef.current = false;
|
|
223
|
+
isConnectingRef.current = false;
|
|
224
|
+
|
|
225
|
+
const isAuthError = error.message.includes('token') ||
|
|
226
|
+
error.message.includes('auth') ||
|
|
227
|
+
error.message.includes('expired');
|
|
228
|
+
|
|
229
|
+
if (isAuthError) {
|
|
230
|
+
logger.error('Authentication failed', error);
|
|
231
|
+
} else {
|
|
232
|
+
logger.error('Connection failed', error);
|
|
233
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
234
|
+
logger.info('Attempting to reconnect...');
|
|
235
|
+
connect();
|
|
236
|
+
}, 5000);
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
setIsConnecting(false);
|
|
240
|
+
}
|
|
241
|
+
}, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected]);
|
|
242
|
+
|
|
243
|
+
// Disconnect function
|
|
244
|
+
const disconnect = useCallback(() => {
|
|
245
|
+
if (isConnectingRef.current) return;
|
|
246
|
+
|
|
247
|
+
if (reconnectTimeoutRef.current) {
|
|
248
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
249
|
+
reconnectTimeoutRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (client) {
|
|
253
|
+
logger.info('Disconnecting from WebSocket server...');
|
|
254
|
+
client.disconnect();
|
|
255
|
+
setClient(null);
|
|
256
|
+
setIsConnected(false);
|
|
257
|
+
setConnectionTime(null);
|
|
258
|
+
setError(null);
|
|
259
|
+
setSubscriptions([]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
hasConnectedRef.current = false;
|
|
263
|
+
isConnectingRef.current = false;
|
|
264
|
+
}, [client, logger]);
|
|
265
|
+
|
|
266
|
+
// Reconnect function
|
|
267
|
+
const reconnect = useCallback(async () => {
|
|
268
|
+
disconnect();
|
|
269
|
+
await connect();
|
|
270
|
+
}, [connect, disconnect]);
|
|
271
|
+
|
|
272
|
+
// Unsubscribe function
|
|
273
|
+
const unsubscribe = useCallback((channel: string) => {
|
|
274
|
+
if (!client) {
|
|
275
|
+
logger.warning('Cannot unsubscribe: client not connected');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
client.unsubscribe?.(channel);
|
|
281
|
+
logger.info(`Unsubscribed from channel: ${channel}`);
|
|
282
|
+
|
|
283
|
+
// Update state immediately
|
|
284
|
+
setSubscriptions((prev) => prev.filter((ch) => ch !== channel));
|
|
285
|
+
setActiveSubscriptions((prev) => prev.filter((sub) => sub.channel !== channel));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logger.error(`Failed to unsubscribe from ${channel}`, error);
|
|
288
|
+
}
|
|
289
|
+
}, [client, logger]);
|
|
290
|
+
|
|
291
|
+
// Auto-connect on mount
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
isMountedRef.current = true;
|
|
294
|
+
|
|
295
|
+
if (autoConnect && !hasConnectedRef.current) {
|
|
296
|
+
connect();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return () => {
|
|
300
|
+
if (isConnectingRef.current && !hasConnectedRef.current) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!hasConnectedRef.current) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
isMountedRef.current = false;
|
|
309
|
+
disconnect();
|
|
310
|
+
};
|
|
311
|
+
}, [autoConnect, connect, disconnect]);
|
|
312
|
+
|
|
313
|
+
const connectionState: ConnectionState = isConnected
|
|
314
|
+
? 'connected'
|
|
315
|
+
: isConnecting
|
|
316
|
+
? 'connecting'
|
|
317
|
+
: error
|
|
318
|
+
? 'error'
|
|
319
|
+
: 'disconnected';
|
|
320
|
+
|
|
321
|
+
const value: CentrifugoContextValue = {
|
|
322
|
+
client,
|
|
323
|
+
isConnected,
|
|
324
|
+
isConnecting,
|
|
325
|
+
error,
|
|
326
|
+
connectionState,
|
|
327
|
+
uptime,
|
|
328
|
+
subscriptions,
|
|
329
|
+
activeSubscriptions,
|
|
330
|
+
connect,
|
|
331
|
+
disconnect,
|
|
332
|
+
reconnect,
|
|
333
|
+
unsubscribe,
|
|
334
|
+
enabled,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Smart DebugPanel visibility:
|
|
338
|
+
// - Show in development mode
|
|
339
|
+
// - Show for admin users
|
|
340
|
+
const showDebugPanel = useMemo(() => {
|
|
341
|
+
return isDevelopment || isAdminUser;
|
|
342
|
+
}, [isAdminUser]);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<CentrifugoContext.Provider value={value}>
|
|
346
|
+
{children}
|
|
347
|
+
{showDebugPanel && (
|
|
348
|
+
<Suspense fallback={null}>
|
|
349
|
+
<DebugPanel />
|
|
350
|
+
</Suspense>
|
|
351
|
+
)}
|
|
352
|
+
</CentrifugoContext.Provider>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
357
|
+
// Main Provider (wraps LogsProvider)
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
export function CentrifugoProvider(props: CentrifugoProviderProps) {
|
|
361
|
+
return (
|
|
362
|
+
<LogsProvider>
|
|
363
|
+
<CentrifugoProviderInner {...props} />
|
|
364
|
+
</LogsProvider>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
369
|
+
// Hook
|
|
370
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
export function useCentrifugo(): CentrifugoContextValue {
|
|
373
|
+
const context = useContext(CentrifugoContext);
|
|
374
|
+
|
|
375
|
+
if (context === undefined) {
|
|
376
|
+
throw new Error('useCentrifugo must be used within a CentrifugoProvider');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return context;
|
|
380
|
+
}
|