@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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscriptions List Component
|
|
3
|
+
*
|
|
4
|
+
* Displays active Centrifugo subscriptions with status and controls
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useState, useEffect } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Card,
|
|
12
|
+
CardContent,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
Badge,
|
|
16
|
+
Button,
|
|
17
|
+
ScrollArea,
|
|
18
|
+
} from '@djangocfg/ui';
|
|
19
|
+
import { Radio, RefreshCw, Trash2 } from 'lucide-react';
|
|
20
|
+
import { Subscription, SubscriptionState } from 'centrifuge';
|
|
21
|
+
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
22
|
+
import { createLogger } from '../../core/logger/createLogger';
|
|
23
|
+
|
|
24
|
+
const logger = createLogger('SubscriptionsList');
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Types
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface SubscriptionItem {
|
|
31
|
+
channel: string;
|
|
32
|
+
state: SubscriptionState;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SubscriptionsListProps {
|
|
36
|
+
showControls?: boolean;
|
|
37
|
+
onSubscriptionClick?: (channel: string) => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Component
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function SubscriptionsList({
|
|
46
|
+
showControls = true,
|
|
47
|
+
onSubscriptionClick,
|
|
48
|
+
className = '',
|
|
49
|
+
}: SubscriptionsListProps) {
|
|
50
|
+
const { isConnected, client } = useCentrifugo();
|
|
51
|
+
const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);
|
|
52
|
+
|
|
53
|
+
// Update subscriptions list
|
|
54
|
+
const updateSubscriptions = () => {
|
|
55
|
+
if (!client || !isConnected) {
|
|
56
|
+
setSubscriptions([]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const centrifuge = client.getCentrifuge();
|
|
62
|
+
const subs = centrifuge.subscriptions();
|
|
63
|
+
const subscriptionsList: SubscriptionItem[] = [];
|
|
64
|
+
|
|
65
|
+
for (const [channel, sub] of Object.entries(subs)) {
|
|
66
|
+
subscriptionsList.push({
|
|
67
|
+
channel,
|
|
68
|
+
state: sub.state,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setSubscriptions(subscriptionsList);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error('Failed to get subscriptions', error);
|
|
75
|
+
setSubscriptions([]);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Auto-update subscriptions
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
updateSubscriptions();
|
|
82
|
+
|
|
83
|
+
if (!client) return;
|
|
84
|
+
|
|
85
|
+
const centrifuge = client.getCentrifuge();
|
|
86
|
+
|
|
87
|
+
const handleSubscribed = () => updateSubscriptions();
|
|
88
|
+
const handleUnsubscribed = () => updateSubscriptions();
|
|
89
|
+
|
|
90
|
+
centrifuge.on('subscribed', handleSubscribed);
|
|
91
|
+
centrifuge.on('unsubscribed', handleUnsubscribed);
|
|
92
|
+
|
|
93
|
+
const interval = setInterval(updateSubscriptions, 3000);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
centrifuge.off('subscribed', handleSubscribed);
|
|
97
|
+
centrifuge.off('unsubscribed', handleUnsubscribed);
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
};
|
|
100
|
+
}, [client, isConnected]);
|
|
101
|
+
|
|
102
|
+
// Unsubscribe from channel
|
|
103
|
+
const handleUnsubscribe = async (channel: string) => {
|
|
104
|
+
if (!client) return;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await client.unsubscribe(channel);
|
|
108
|
+
updateSubscriptions();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error('Failed to unsubscribe', error);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Card className={className}>
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<div className="flex items-center justify-between">
|
|
118
|
+
<CardTitle className="flex items-center gap-2">
|
|
119
|
+
<Radio className="h-5 w-5" />
|
|
120
|
+
Active Subscriptions
|
|
121
|
+
<Badge variant="outline">{subscriptions.length}</Badge>
|
|
122
|
+
</CardTitle>
|
|
123
|
+
|
|
124
|
+
{showControls && (
|
|
125
|
+
<Button size="sm" variant="outline" onClick={updateSubscriptions}>
|
|
126
|
+
<RefreshCw className="h-4 w-4" />
|
|
127
|
+
</Button>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
|
|
132
|
+
<CardContent>
|
|
133
|
+
{!isConnected ? (
|
|
134
|
+
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
135
|
+
Not connected to Centrifugo
|
|
136
|
+
</div>
|
|
137
|
+
) : subscriptions.length === 0 ? (
|
|
138
|
+
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
139
|
+
No active subscriptions
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<ScrollArea className="h-[300px]">
|
|
143
|
+
<div className="space-y-2">
|
|
144
|
+
{subscriptions.map((sub) => (
|
|
145
|
+
<div
|
|
146
|
+
key={sub.channel}
|
|
147
|
+
className="flex items-center justify-between p-3 rounded border hover:bg-muted/50 transition-colors"
|
|
148
|
+
>
|
|
149
|
+
<div
|
|
150
|
+
className="flex-1 min-w-0 cursor-pointer"
|
|
151
|
+
onClick={() => onSubscriptionClick?.(sub.channel)}
|
|
152
|
+
>
|
|
153
|
+
<div className="flex items-center gap-2">
|
|
154
|
+
<Badge variant="outline" className="text-xs">
|
|
155
|
+
{sub.state}
|
|
156
|
+
</Badge>
|
|
157
|
+
<span className="text-sm font-mono truncate">{sub.channel}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{showControls && (
|
|
162
|
+
<Button
|
|
163
|
+
size="sm"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
onClick={() => handleUnsubscribe(sub.channel)}
|
|
166
|
+
>
|
|
167
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
168
|
+
</Button>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</ScrollArea>
|
|
174
|
+
)}
|
|
175
|
+
</CardContent>
|
|
176
|
+
</Card>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrifugo Components
|
|
3
|
+
*
|
|
4
|
+
* Universal, composable components for Centrifugo monitoring and debugging
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Connection Status
|
|
8
|
+
export * from './ConnectionStatus';
|
|
9
|
+
|
|
10
|
+
// Messages Feed
|
|
11
|
+
export * from './MessagesFeed';
|
|
12
|
+
|
|
13
|
+
// Subscriptions List
|
|
14
|
+
export * from './SubscriptionsList';
|
|
15
|
+
|
|
16
|
+
// Main Monitor (all-in-one)
|
|
17
|
+
export * from './CentrifugoMonitor';
|
|
18
|
+
|
|
@@ -14,6 +14,7 @@ export class CentrifugoRPCClient {
|
|
|
14
14
|
private subscription: any;
|
|
15
15
|
private channelSubscriptions: Map<string, any> = new Map();
|
|
16
16
|
private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
|
|
17
|
+
private readonly userId: string;
|
|
17
18
|
private readonly replyChannel: string;
|
|
18
19
|
private readonly timeout: number;
|
|
19
20
|
private readonly logger: Logger;
|
|
@@ -25,6 +26,7 @@ export class CentrifugoRPCClient {
|
|
|
25
26
|
timeout: number = 30000,
|
|
26
27
|
logger?: Logger
|
|
27
28
|
) {
|
|
29
|
+
this.userId = userId;
|
|
28
30
|
this.replyChannel = `user#${userId}`;
|
|
29
31
|
this.timeout = timeout;
|
|
30
32
|
this.logger = logger || createLogger({ source: 'client' });
|
|
@@ -160,7 +162,145 @@ export class CentrifugoRPCClient {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
private generateCorrelationId(): string {
|
|
163
|
-
return
|
|
165
|
+
return `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
169
|
+
// RPC Pattern API (Request-Response via Correlation ID)
|
|
170
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Send RPC request and wait for response.
|
|
174
|
+
*
|
|
175
|
+
* Uses correlation ID pattern to match request with response:
|
|
176
|
+
* 1. Generate unique correlation_id
|
|
177
|
+
* 2. Subscribe to personal reply channel (user#{userId})
|
|
178
|
+
* 3. Publish request with correlation_id
|
|
179
|
+
* 4. Wait for response with matching correlation_id
|
|
180
|
+
* 5. Return response or timeout
|
|
181
|
+
*
|
|
182
|
+
* @param method - RPC method name (e.g., 'tasks.get_stats')
|
|
183
|
+
* @param params - Request parameters
|
|
184
|
+
* @param options - RPC options
|
|
185
|
+
* @returns Promise that resolves with response data
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* const result = await client.rpc('tasks.get_stats', { bot_id: '123' });
|
|
189
|
+
* console.log('Stats:', result);
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* // With custom timeout and reply channel
|
|
193
|
+
* const result = await client.rpc('bot.command',
|
|
194
|
+
* { command: 'start' },
|
|
195
|
+
* { timeout: 30000, replyChannel: 'bot#123#replies' }
|
|
196
|
+
* );
|
|
197
|
+
*/
|
|
198
|
+
async rpc<TRequest = any, TResponse = any>(
|
|
199
|
+
method: string,
|
|
200
|
+
params: TRequest,
|
|
201
|
+
options: {
|
|
202
|
+
timeout?: number;
|
|
203
|
+
replyChannel?: string;
|
|
204
|
+
} = {}
|
|
205
|
+
): Promise<TResponse> {
|
|
206
|
+
const {
|
|
207
|
+
timeout = 10000,
|
|
208
|
+
replyChannel = `user#${this.userId}`,
|
|
209
|
+
} = options;
|
|
210
|
+
|
|
211
|
+
const correlationId = this.generateCorrelationId();
|
|
212
|
+
|
|
213
|
+
this.logger.info(`RPC request: ${method}`, { correlationId, params });
|
|
214
|
+
|
|
215
|
+
return new Promise<TResponse>((resolve, reject) => {
|
|
216
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
217
|
+
let unsubscribe: (() => void) | null = null;
|
|
218
|
+
|
|
219
|
+
// Cleanup function
|
|
220
|
+
const cleanup = () => {
|
|
221
|
+
if (timeoutId) {
|
|
222
|
+
clearTimeout(timeoutId);
|
|
223
|
+
timeoutId = null;
|
|
224
|
+
}
|
|
225
|
+
if (unsubscribe) {
|
|
226
|
+
unsubscribe();
|
|
227
|
+
unsubscribe = null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Set timeout
|
|
232
|
+
timeoutId = setTimeout(() => {
|
|
233
|
+
cleanup();
|
|
234
|
+
const error = new Error(`RPC timeout: ${method} (${timeout}ms)`);
|
|
235
|
+
this.logger.error(`RPC timeout: ${method}`, { correlationId, timeout });
|
|
236
|
+
reject(error);
|
|
237
|
+
}, timeout);
|
|
238
|
+
|
|
239
|
+
// Subscribe to reply channel
|
|
240
|
+
try {
|
|
241
|
+
unsubscribe = this.subscribe(replyChannel, (data: any) => {
|
|
242
|
+
try {
|
|
243
|
+
// Check if this is our response
|
|
244
|
+
if (data.correlation_id === correlationId) {
|
|
245
|
+
cleanup();
|
|
246
|
+
|
|
247
|
+
// Check for error in response
|
|
248
|
+
if (data.error) {
|
|
249
|
+
this.logger.error(`RPC error: ${method}`, {
|
|
250
|
+
correlationId,
|
|
251
|
+
error: data.error
|
|
252
|
+
});
|
|
253
|
+
reject(new Error(data.error.message || 'RPC error'));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger.success(`RPC response: ${method}`, {
|
|
258
|
+
correlationId,
|
|
259
|
+
hasResult: !!data.result
|
|
260
|
+
});
|
|
261
|
+
resolve(data.result as TResponse);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
cleanup();
|
|
265
|
+
this.logger.error(`Error processing RPC response: ${method}`, error);
|
|
266
|
+
reject(error);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Publish request to RPC channel
|
|
271
|
+
// Note: This uses Centrifuge's publish, not our HTTP publish
|
|
272
|
+
// The backend should be subscribed to this channel
|
|
273
|
+
const rpcChannel = `rpc#${method}`;
|
|
274
|
+
const sub = this.centrifuge.getSubscription(rpcChannel);
|
|
275
|
+
|
|
276
|
+
if (sub) {
|
|
277
|
+
// If we have a subscription, use it to publish
|
|
278
|
+
sub.publish({
|
|
279
|
+
correlation_id: correlationId,
|
|
280
|
+
method,
|
|
281
|
+
params,
|
|
282
|
+
reply_to: replyChannel,
|
|
283
|
+
timestamp: Date.now(),
|
|
284
|
+
}).catch((error: any) => {
|
|
285
|
+
cleanup();
|
|
286
|
+
this.logger.error(`Failed to publish RPC request: ${method}`, error);
|
|
287
|
+
reject(error);
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
// Otherwise, we need to publish via HTTP API
|
|
291
|
+
// This requires the backend to expose a publish endpoint
|
|
292
|
+
cleanup();
|
|
293
|
+
reject(new Error(
|
|
294
|
+
`Cannot publish RPC request: no subscription to ${rpcChannel}. ` +
|
|
295
|
+
`Backend should provide a publish endpoint or use server-side subscriptions.`
|
|
296
|
+
));
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
cleanup();
|
|
300
|
+
this.logger.error(`Failed to setup RPC: ${method}`, error);
|
|
301
|
+
reject(error);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
164
304
|
}
|
|
165
305
|
|
|
166
306
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -183,26 +323,52 @@ export class CentrifugoRPCClient {
|
|
|
183
323
|
* unsubscribe();
|
|
184
324
|
*/
|
|
185
325
|
subscribe(channel: string, callback: (data: any) => void): () => void {
|
|
186
|
-
// Check if already subscribed
|
|
187
|
-
|
|
188
|
-
|
|
326
|
+
// Check if already subscribed - reuse existing subscription
|
|
327
|
+
const existingSub = this.channelSubscriptions.get(channel);
|
|
328
|
+
if (existingSub) {
|
|
329
|
+
this.logger.warning(`Already subscribed to ${channel}, reusing existing subscription`);
|
|
330
|
+
|
|
331
|
+
// Add new callback handler to existing subscription
|
|
332
|
+
const handler = (ctx: any) => {
|
|
333
|
+
try {
|
|
334
|
+
callback(ctx.data);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
this.logger.error(`Error in subscription callback for ${channel}`, error);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
existingSub.on('publication', handler);
|
|
341
|
+
|
|
342
|
+
// Return unsubscribe that only removes this specific handler
|
|
343
|
+
return () => {
|
|
344
|
+
existingSub.off('publication', handler);
|
|
345
|
+
};
|
|
189
346
|
}
|
|
190
347
|
|
|
191
|
-
// Create new subscription
|
|
192
|
-
|
|
348
|
+
// Create new subscription using Centrifuge's getSubscription or newSubscription
|
|
349
|
+
let sub = this.centrifuge.getSubscription(channel);
|
|
350
|
+
if (!sub) {
|
|
351
|
+
sub = this.centrifuge.newSubscription(channel);
|
|
352
|
+
}
|
|
193
353
|
|
|
194
|
-
// Handle publications
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
354
|
+
// Handle publications with error handling
|
|
355
|
+
const publicationHandler = (ctx: any) => {
|
|
356
|
+
try {
|
|
357
|
+
callback(ctx.data);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.logger.error(`Error in subscription callback for ${channel}`, error);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
sub.on('publication', publicationHandler);
|
|
198
364
|
|
|
199
365
|
// Handle subscription lifecycle
|
|
200
366
|
sub.on('subscribed', () => {
|
|
201
|
-
|
|
367
|
+
this.logger.success(`Subscribed to ${channel}`);
|
|
202
368
|
});
|
|
203
369
|
|
|
204
370
|
sub.on('error', (ctx: any) => {
|
|
205
|
-
this.logger.error(`Subscription error for ${channel}
|
|
371
|
+
this.logger.error(`Subscription error for ${channel}`, ctx.error);
|
|
206
372
|
});
|
|
207
373
|
|
|
208
374
|
// Start subscription
|
|
@@ -217,6 +383,7 @@ export class CentrifugoRPCClient {
|
|
|
217
383
|
|
|
218
384
|
/**
|
|
219
385
|
* Unsubscribe from a channel.
|
|
386
|
+
* Properly removes subscription from both internal map and Centrifuge client.
|
|
220
387
|
*
|
|
221
388
|
* @param channel - Channel name
|
|
222
389
|
*/
|
|
@@ -226,12 +393,27 @@ export class CentrifugoRPCClient {
|
|
|
226
393
|
return;
|
|
227
394
|
}
|
|
228
395
|
|
|
229
|
-
|
|
230
|
-
|
|
396
|
+
try {
|
|
397
|
+
// Unsubscribe from channel
|
|
398
|
+
sub.unsubscribe();
|
|
399
|
+
|
|
400
|
+
// Remove subscription from Centrifuge client registry
|
|
401
|
+
this.centrifuge.removeSubscription(sub);
|
|
402
|
+
|
|
403
|
+
// Remove from our internal map
|
|
404
|
+
this.channelSubscriptions.delete(channel);
|
|
405
|
+
|
|
406
|
+
this.logger.info(`Unsubscribed from ${channel}`);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
this.logger.error(`Error unsubscribing from ${channel}`, error);
|
|
409
|
+
// Still remove from our map even if there was an error
|
|
410
|
+
this.channelSubscriptions.delete(channel);
|
|
411
|
+
}
|
|
231
412
|
}
|
|
232
413
|
|
|
233
414
|
/**
|
|
234
415
|
* Unsubscribe from all channels.
|
|
416
|
+
* Properly removes all subscriptions from both internal map and Centrifuge client.
|
|
235
417
|
*/
|
|
236
418
|
unsubscribeAll(): void {
|
|
237
419
|
if (this.channelSubscriptions.size === 0) {
|
|
@@ -239,9 +421,16 @@ export class CentrifugoRPCClient {
|
|
|
239
421
|
}
|
|
240
422
|
|
|
241
423
|
this.channelSubscriptions.forEach((sub, channel) => {
|
|
242
|
-
|
|
424
|
+
try {
|
|
425
|
+
sub.unsubscribe();
|
|
426
|
+
this.centrifuge.removeSubscription(sub);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this.logger.error(`Error unsubscribing from ${channel}`, error);
|
|
429
|
+
}
|
|
243
430
|
});
|
|
431
|
+
|
|
244
432
|
this.channelSubscriptions.clear();
|
|
433
|
+
this.logger.info('Unsubscribed from all channels');
|
|
245
434
|
}
|
|
246
435
|
|
|
247
436
|
/**
|
|
@@ -278,4 +467,12 @@ export class CentrifugoRPCClient {
|
|
|
278
467
|
// Combine and deduplicate
|
|
279
468
|
return Array.from(new Set([...clientSubs, ...serverSubs]));
|
|
280
469
|
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Get native Centrifuge client for direct access to low-level APIs.
|
|
473
|
+
* Use with caution - prefer using the high-level methods when possible.
|
|
474
|
+
*/
|
|
475
|
+
getCentrifuge(): Centrifuge {
|
|
476
|
+
return this.centrifuge;
|
|
477
|
+
}
|
|
281
478
|
}
|
|
@@ -23,12 +23,35 @@ export interface LoggerConfig {
|
|
|
23
23
|
tag?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Create a logger instance
|
|
28
|
+
*
|
|
29
|
+
* @param configOrPrefix - Either a full LoggerConfig object or a simple string prefix
|
|
30
|
+
* @returns Logger instance
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Simple usage with prefix
|
|
34
|
+
* const logger = createLogger('MyComponent');
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Advanced usage with full config
|
|
38
|
+
* const logger = createLogger({
|
|
39
|
+
* source: 'client',
|
|
40
|
+
* tag: 'MyComponent',
|
|
41
|
+
* isDevelopment: true
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
export function createLogger(configOrPrefix: LoggerConfig | string): Logger {
|
|
45
|
+
// Normalize input to LoggerConfig
|
|
46
|
+
const config: LoggerConfig = typeof configOrPrefix === 'string'
|
|
47
|
+
? { source: 'client', tag: configOrPrefix }
|
|
48
|
+
: configOrPrefix;
|
|
49
|
+
|
|
27
50
|
const { source, isDevelopment = process.env.NODE_ENV === 'development', tag = 'Centrifugo' } = config;
|
|
28
51
|
|
|
29
52
|
const logsStore = getGlobalLogsStore();
|
|
30
53
|
|
|
31
|
-
// Console logger (consola)
|
|
54
|
+
// Console logger (consola) with prefix tag
|
|
32
55
|
const consola = createConsola({
|
|
33
56
|
level: isDevelopment ? 4 : 3,
|
|
34
57
|
formatOptions: {
|
|
@@ -36,7 +59,7 @@ export function createLogger(config: LoggerConfig): Logger {
|
|
|
36
59
|
date: false,
|
|
37
60
|
compact: !isDevelopment,
|
|
38
61
|
},
|
|
39
|
-
}).withTag(tag);
|
|
62
|
+
}).withTag(`[${tag}]`);
|
|
40
63
|
|
|
41
64
|
const log = (level: LogLevel, message: string, data?: unknown) => {
|
|
42
65
|
// Add to LogsStore (always)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Debug Panel
|
|
3
3
|
*
|
|
4
4
|
* Main debug UI with FAB button + Sheet modal + Tabs.
|
|
5
|
-
*
|
|
5
|
+
* Visibility controlled by CentrifugoProvider (dev mode OR admin users).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
@@ -24,15 +24,6 @@ import { ConnectionTab } from '../ConnectionTab';
|
|
|
24
24
|
import { LogsTab } from '../LogsTab';
|
|
25
25
|
import { SubscriptionsTab } from '../SubscriptionsTab';
|
|
26
26
|
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
-
// Config
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
32
|
-
const isStaticBuild = process.env.NEXT_PHASE === 'phase-production-build';
|
|
33
|
-
|
|
34
|
-
const showDebugPanel = isDevelopment && !isStaticBuild;
|
|
35
|
-
|
|
36
27
|
// ─────────────────────────────────────────────────────────────────────────
|
|
37
28
|
// Component
|
|
38
29
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -41,11 +32,6 @@ export function DebugPanel() {
|
|
|
41
32
|
const [isOpen, setIsOpen] = useState(false);
|
|
42
33
|
const [activeTab, setActiveTab] = useState('connection');
|
|
43
34
|
|
|
44
|
-
// Don't render in production
|
|
45
|
-
if (!showDebugPanel) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
35
|
return (
|
|
50
36
|
<>
|
|
51
37
|
{/* FAB Button (fixed bottom-left) */}
|
package/src/hooks/index.ts
CHANGED