@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.
Files changed (34) hide show
  1. package/README.md +345 -34
  2. package/package.json +6 -4
  3. package/src/config.ts +1 -1
  4. package/src/core/client/CentrifugoRPCClient.ts +281 -0
  5. package/src/core/client/index.ts +5 -0
  6. package/src/core/index.ts +15 -0
  7. package/src/core/logger/LogsStore.ts +101 -0
  8. package/src/core/logger/createLogger.ts +79 -0
  9. package/src/core/logger/index.ts +9 -0
  10. package/src/core/types/index.ts +68 -0
  11. package/src/debug/ConnectionTab/ConnectionTab.tsx +160 -0
  12. package/src/debug/ConnectionTab/index.ts +5 -0
  13. package/src/debug/DebugPanel/DebugPanel.tsx +88 -0
  14. package/src/debug/DebugPanel/index.ts +5 -0
  15. package/src/debug/LogsTab/LogsTab.tsx +236 -0
  16. package/src/debug/LogsTab/index.ts +5 -0
  17. package/src/debug/SubscriptionsTab/SubscriptionsTab.tsx +135 -0
  18. package/src/debug/SubscriptionsTab/index.ts +5 -0
  19. package/src/debug/index.ts +11 -0
  20. package/src/hooks/index.ts +2 -5
  21. package/src/hooks/useSubscription.ts +66 -65
  22. package/src/index.ts +94 -13
  23. package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +380 -0
  24. package/src/providers/CentrifugoProvider/index.ts +6 -0
  25. package/src/providers/LogsProvider/LogsProvider.tsx +107 -0
  26. package/src/providers/LogsProvider/index.ts +6 -0
  27. package/src/providers/index.ts +9 -0
  28. package/API_GENERATOR.md +0 -253
  29. package/src/components/CentrifugoDebug.tsx +0 -182
  30. package/src/components/index.ts +0 -5
  31. package/src/context/CentrifugoProvider.tsx +0 -228
  32. package/src/context/index.ts +0 -5
  33. package/src/hooks/useLogger.ts +0 -69
  34. package/src/types/index.ts +0 -45
@@ -1,86 +1,87 @@
1
1
  /**
2
- * React hook for subscribing to Centrifugo channels.
2
+ * useSubscription Hook
3
3
  *
4
- * Automatically handles subscription lifecycle (mount/unmount).
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
- import { useEffect, useRef } from 'react';
23
- import { useCentrifugo } from '../context';
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 useSubscriptionOptions {
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
- channel: string | null,
55
- callback: (data: T) => void,
56
- options: useSubscriptionOptions = {}
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
- // Keep callback ref updated (avoid stale closures)
63
- useEffect(() => {
64
- callbackRef.current = callback;
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
- // Don't subscribe if disabled, not connected, or no channel
69
- if (!enabled || !isConnected || !baseClient || !channel) {
52
+ if (!client || !isConnected || !enabled) {
70
53
  return;
71
54
  }
72
55
 
73
- console.log(`🎣 useSubscription: Subscribing to ${channel}`);
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
- // Subscribe with stable callback wrapper
76
- const unsubscribe = baseClient.subscribe(channel, (data) => {
77
- callbackRef.current(data);
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 channel change
75
+ // Cleanup on unmount or deps change
81
76
  return () => {
82
- console.log(`🎣 useSubscription: Unsubscribing from ${channel}`);
83
77
  unsubscribe();
84
78
  };
85
- }, [channel, enabled, isConnected, baseClient]);
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 for Django-CFG + Centrifugo integration
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
- // Context & Provider
8
- export { CentrifugoProvider, useCentrifugo } from './context';
43
+ // ─────────────────────────────────────────────────────────────────────────
44
+ // Re-export Centrifuge for advanced usage
45
+ // ─────────────────────────────────────────────────────────────────────────
9
46
 
10
- // Hooks
11
- export { useCentrifugoLogger, useRPCLogger, useSubscription } from './hooks';
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
- // Components
16
- export { CentrifugoDebug, WSRPCDebug } from './components';
50
+ // ─────────────────────────────────────────────────────────────────────────
51
+ // Core (Platform-agnostic)
52
+ // ─────────────────────────────────────────────────────────────────────────
17
53
 
18
- // Types
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 './types';
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
- // Config
27
- export { isDevelopment, isProduction, isStaticBuild, centrifugoConfig } from './config';
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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Centrifugo Provider
3
+ */
4
+
5
+ export { CentrifugoProvider, useCentrifugo } from './CentrifugoProvider';
6
+ export type { CentrifugoContextValue, CentrifugoProviderProps } from './CentrifugoProvider';