@djangocfg/centrifugo 1.0.2 → 1.0.4
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 +534 -83
- package/package.json +5 -5
- package/src/components/CentrifugoMonitor/CentrifugoMonitor.tsx +137 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorDialog.tsx +64 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorFAB.tsx +81 -0
- package/src/components/CentrifugoMonitor/CentrifugoMonitorWidget.tsx +74 -0
- package/src/components/CentrifugoMonitor/index.ts +14 -0
- package/src/components/ConnectionStatus/ConnectionStatus.tsx +192 -0
- package/src/components/ConnectionStatus/ConnectionStatusCard.tsx +56 -0
- package/src/components/ConnectionStatus/index.ts +9 -0
- package/src/components/MessagesFeed/MessageFilters.tsx +163 -0
- package/src/components/MessagesFeed/MessagesFeed.tsx +383 -0
- package/src/components/MessagesFeed/index.ts +9 -0
- package/src/components/MessagesFeed/types.ts +31 -0
- package/src/components/SubscriptionsList/SubscriptionsList.tsx +179 -0
- package/src/components/SubscriptionsList/index.ts +7 -0
- package/src/components/index.ts +18 -0
- package/src/core/client/CentrifugoRPCClient.ts +212 -15
- package/src/core/logger/createLogger.ts +26 -3
- package/src/debug/DebugPanel/DebugPanel.tsx +1 -15
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useRPC.ts +149 -0
- package/src/hooks/useSubscription.ts +44 -10
- package/src/index.ts +3 -4
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +3 -21
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRPC Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for making RPC calls via Centrifugo using correlation ID pattern.
|
|
5
|
+
* Provides type-safe request-response communication over WebSocket.
|
|
6
|
+
*
|
|
7
|
+
* Pattern:
|
|
8
|
+
* 1. Client sends request with correlation_id to rpc#{method}
|
|
9
|
+
* 2. Backend processes and sends response to user#{userId} with same correlation_id
|
|
10
|
+
* 3. Client matches response by correlation_id and resolves Promise
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { call, isLoading, error } = useRPC();
|
|
14
|
+
*
|
|
15
|
+
* const handleGetStats = async () => {
|
|
16
|
+
* const result = await call('tasks.get_stats', { bot_id: '123' });
|
|
17
|
+
* console.log('Stats:', result);
|
|
18
|
+
* };
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use client';
|
|
22
|
+
|
|
23
|
+
import { useState, useCallback, useRef } from 'react';
|
|
24
|
+
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
25
|
+
import { createLogger } from '../core/logger';
|
|
26
|
+
|
|
27
|
+
export interface UseRPCOptions {
|
|
28
|
+
timeout?: number;
|
|
29
|
+
replyChannel?: string;
|
|
30
|
+
onError?: (error: Error) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseRPCResult {
|
|
34
|
+
call: <TRequest = any, TResponse = any>(
|
|
35
|
+
method: string,
|
|
36
|
+
params: TRequest,
|
|
37
|
+
options?: UseRPCOptions
|
|
38
|
+
) => Promise<TResponse>;
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
reset: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useRPC(defaultOptions: UseRPCOptions = {}): UseRPCResult {
|
|
45
|
+
const { client, isConnected } = useCentrifugo();
|
|
46
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
47
|
+
const [error, setError] = useState<Error | null>(null);
|
|
48
|
+
|
|
49
|
+
const logger = useRef(createLogger('useRPC')).current;
|
|
50
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
51
|
+
|
|
52
|
+
const reset = useCallback(() => {
|
|
53
|
+
setIsLoading(false);
|
|
54
|
+
setError(null);
|
|
55
|
+
if (abortControllerRef.current) {
|
|
56
|
+
abortControllerRef.current.abort();
|
|
57
|
+
abortControllerRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const call = useCallback(
|
|
62
|
+
async <TRequest = any, TResponse = any>(
|
|
63
|
+
method: string,
|
|
64
|
+
params: TRequest,
|
|
65
|
+
options: UseRPCOptions = {}
|
|
66
|
+
): Promise<TResponse> => {
|
|
67
|
+
if (!client) {
|
|
68
|
+
const error = new Error('Centrifugo client not available');
|
|
69
|
+
setError(error);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isConnected) {
|
|
74
|
+
const error = new Error('Not connected to Centrifugo');
|
|
75
|
+
setError(error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Reset previous state
|
|
80
|
+
setError(null);
|
|
81
|
+
setIsLoading(true);
|
|
82
|
+
|
|
83
|
+
// Create abort controller for this call
|
|
84
|
+
const abortController = new AbortController();
|
|
85
|
+
abortControllerRef.current = abortController;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const mergedOptions = {
|
|
89
|
+
...defaultOptions,
|
|
90
|
+
...options,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
logger.info(`RPC call: ${method}`, { params });
|
|
94
|
+
|
|
95
|
+
const result = await client.rpc<TRequest, TResponse>(
|
|
96
|
+
method,
|
|
97
|
+
params,
|
|
98
|
+
{
|
|
99
|
+
timeout: mergedOptions.timeout,
|
|
100
|
+
replyChannel: mergedOptions.replyChannel,
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Check if aborted
|
|
105
|
+
if (abortController.signal.aborted) {
|
|
106
|
+
throw new Error('RPC call aborted');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.success(`RPC success: ${method}`);
|
|
110
|
+
setIsLoading(false);
|
|
111
|
+
return result;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const rpcError = err instanceof Error ? err : new Error('RPC call failed');
|
|
114
|
+
|
|
115
|
+
// Don't set error if aborted
|
|
116
|
+
if (!abortController.signal.aborted) {
|
|
117
|
+
setError(rpcError);
|
|
118
|
+
logger.error(`RPC failed: ${method}`, rpcError);
|
|
119
|
+
|
|
120
|
+
// Call error callback if provided
|
|
121
|
+
const onError = options.onError || defaultOptions.onError;
|
|
122
|
+
if (onError) {
|
|
123
|
+
try {
|
|
124
|
+
onError(rpcError);
|
|
125
|
+
} catch (callbackError) {
|
|
126
|
+
logger.error('Error in onError callback', callbackError);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setIsLoading(false);
|
|
132
|
+
throw rpcError;
|
|
133
|
+
} finally {
|
|
134
|
+
if (abortControllerRef.current === abortController) {
|
|
135
|
+
abortControllerRef.current = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[client, isConnected, defaultOptions, logger]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
call,
|
|
144
|
+
isLoading,
|
|
145
|
+
error,
|
|
146
|
+
reset,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* useSubscription Hook
|
|
3
3
|
*
|
|
4
4
|
* Subscribe to Centrifugo channel with auto-cleanup.
|
|
5
|
+
*
|
|
6
|
+
* Best practices:
|
|
7
|
+
* - Callbacks are stored in refs to avoid re-subscriptions
|
|
8
|
+
* - Proper error handling in callbacks (as recommended by centrifuge-js)
|
|
9
|
+
* - Cleanup on unmount
|
|
10
|
+
* - Stable unsubscribe function
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
'use client';
|
|
@@ -35,21 +41,38 @@ export function useSubscription<T = any>(
|
|
|
35
41
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
36
42
|
|
|
37
43
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
38
|
-
const logger = useRef(createLogger(
|
|
44
|
+
const logger = useRef(createLogger('useSubscription')).current;
|
|
45
|
+
|
|
46
|
+
// Store callbacks in refs to avoid re-subscriptions when they change
|
|
47
|
+
const onPublicationRef = useRef(onPublication);
|
|
48
|
+
const onErrorRef = useRef(onError);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
onPublicationRef.current = onPublication;
|
|
52
|
+
onErrorRef.current = onError;
|
|
53
|
+
}, [onPublication, onError]);
|
|
39
54
|
|
|
40
55
|
// Unsubscribe function
|
|
41
56
|
const unsubscribe = useCallback(() => {
|
|
42
57
|
if (unsubscribeRef.current) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
try {
|
|
59
|
+
unsubscribeRef.current();
|
|
60
|
+
unsubscribeRef.current = null;
|
|
61
|
+
setIsSubscribed(false);
|
|
62
|
+
logger.info(`Unsubscribed from channel: ${channel}`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error(`Error during unsubscribe from ${channel}`, err);
|
|
65
|
+
}
|
|
47
66
|
}
|
|
48
67
|
}, [channel, logger]);
|
|
49
68
|
|
|
50
69
|
// Subscribe effect
|
|
51
70
|
useEffect(() => {
|
|
52
71
|
if (!client || !isConnected || !enabled) {
|
|
72
|
+
// Reset state when not connected
|
|
73
|
+
if (!isConnected && isSubscribed) {
|
|
74
|
+
setIsSubscribed(false);
|
|
75
|
+
}
|
|
53
76
|
return;
|
|
54
77
|
}
|
|
55
78
|
|
|
@@ -57,9 +80,14 @@ export function useSubscription<T = any>(
|
|
|
57
80
|
|
|
58
81
|
try {
|
|
59
82
|
const unsub = client.subscribe(channel, (receivedData: T) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
try {
|
|
84
|
+
// Error handling in callback as recommended by centrifuge-js docs
|
|
85
|
+
setData(receivedData);
|
|
86
|
+
setError(null);
|
|
87
|
+
onPublicationRef.current?.(receivedData);
|
|
88
|
+
} catch (callbackError) {
|
|
89
|
+
logger.error(`Error in onPublication callback for ${channel}`, callbackError);
|
|
90
|
+
}
|
|
63
91
|
});
|
|
64
92
|
|
|
65
93
|
unsubscribeRef.current = unsub;
|
|
@@ -68,7 +96,13 @@ export function useSubscription<T = any>(
|
|
|
68
96
|
} catch (err) {
|
|
69
97
|
const subscriptionError = err instanceof Error ? err : new Error('Subscription failed');
|
|
70
98
|
setError(subscriptionError);
|
|
71
|
-
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
onErrorRef.current?.(subscriptionError);
|
|
102
|
+
} catch (callbackError) {
|
|
103
|
+
logger.error(`Error in onError callback for ${channel}`, callbackError);
|
|
104
|
+
}
|
|
105
|
+
|
|
72
106
|
logger.error(`Subscription failed: ${channel}`, subscriptionError);
|
|
73
107
|
}
|
|
74
108
|
|
|
@@ -76,7 +110,7 @@ export function useSubscription<T = any>(
|
|
|
76
110
|
return () => {
|
|
77
111
|
unsubscribe();
|
|
78
112
|
};
|
|
79
|
-
}, [client, isConnected, enabled, channel,
|
|
113
|
+
}, [client, isConnected, enabled, channel, unsubscribe, logger, isSubscribed]);
|
|
80
114
|
|
|
81
115
|
return {
|
|
82
116
|
data,
|
package/src/index.ts
CHANGED
|
@@ -100,9 +100,8 @@ export { centrifugoConfig, isDevelopment, isProduction, isStaticBuild } from './
|
|
|
100
100
|
export type { CentrifugoConfig } from './config';
|
|
101
101
|
|
|
102
102
|
// ─────────────────────────────────────────────────────────────────────────
|
|
103
|
-
//
|
|
103
|
+
// Components (Universal, composable monitoring components)
|
|
104
104
|
// ─────────────────────────────────────────────────────────────────────────
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// No need to manually add <DebugPanel /> to your app
|
|
106
|
+
export * from './components';
|
|
107
|
+
export * from './debug';
|
|
@@ -7,18 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo,
|
|
10
|
+
import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react';
|
|
11
11
|
import { useAuth } from '@djangocfg/layouts';
|
|
12
12
|
import { CentrifugoRPCClient } from '../../core/client';
|
|
13
13
|
import { createLogger } from '../../core/logger';
|
|
14
14
|
import type { ConnectionState, CentrifugoToken } from '../../core/types';
|
|
15
15
|
import { LogsProvider } from '../LogsProvider';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// Lazy load DebugPanel to reduce bundle size
|
|
19
|
-
const DebugPanel = lazy(() =>
|
|
20
|
-
import('../../debug/DebugPanel').then((m) => ({ default: m.DebugPanel }))
|
|
21
|
-
);
|
|
16
|
+
import { isStaticBuild } from '../../config';
|
|
22
17
|
|
|
23
18
|
// ─────────────────────────────────────────────────────────────────────────
|
|
24
19
|
// Context
|
|
@@ -72,7 +67,7 @@ function CentrifugoProviderInner({
|
|
|
72
67
|
url,
|
|
73
68
|
autoConnect: autoConnectProp = true,
|
|
74
69
|
}: CentrifugoProviderProps) {
|
|
75
|
-
const { isAuthenticated, isLoading, user
|
|
70
|
+
const { isAuthenticated, isLoading, user } = useAuth();
|
|
76
71
|
|
|
77
72
|
const [client, setClient] = useState<CentrifugoRPCClient | null>(null);
|
|
78
73
|
const [isConnected, setIsConnected] = useState(false);
|
|
@@ -334,22 +329,9 @@ function CentrifugoProviderInner({
|
|
|
334
329
|
enabled,
|
|
335
330
|
};
|
|
336
331
|
|
|
337
|
-
// Smart DebugPanel visibility:
|
|
338
|
-
// - Show in development mode (not static build)
|
|
339
|
-
// - Always show for admin users (staff or superuser)
|
|
340
|
-
const showDebugPanel = useMemo(() => {
|
|
341
|
-
const devMode = isDevelopment && !isStaticBuild;
|
|
342
|
-
return devMode || isAdminUser;
|
|
343
|
-
}, [isAdminUser]);
|
|
344
|
-
|
|
345
332
|
return (
|
|
346
333
|
<CentrifugoContext.Provider value={value}>
|
|
347
334
|
{children}
|
|
348
|
-
{showDebugPanel && (
|
|
349
|
-
<Suspense fallback={null}>
|
|
350
|
-
<DebugPanel />
|
|
351
|
-
</Suspense>
|
|
352
|
-
)}
|
|
353
335
|
</CentrifugoContext.Provider>
|
|
354
336
|
);
|
|
355
337
|
}
|