@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.
@@ -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({ source: 'subscription' })).current;
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
- unsubscribeRef.current();
44
- unsubscribeRef.current = null;
45
- setIsSubscribed(false);
46
- logger.info(`Unsubscribed from channel: ${channel}`);
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
- setData(receivedData);
61
- setError(null);
62
- onPublication?.(receivedData);
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
- onError?.(subscriptionError);
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, onPublication, onError, unsubscribe, logger]);
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
- // Debug UI (Embedded in Provider, not for direct use)
103
+ // Components (Universal, composable monitoring components)
104
104
  // ─────────────────────────────────────────────────────────────────────────
105
105
 
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
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, lazy, Suspense, type ReactNode } from 'react';
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 { 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
- );
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, isAdminUser } = useAuth();
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
  }