@djangocfg/centrifugo 1.4.36 → 2.1.1
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 +1 -1
- package/package.json +10 -10
- package/src/components/CentrifugoMonitor/CentrifugoMonitor.tsx +1 -1
- package/src/components/CentrifugoMonitor/CentrifugoMonitorDialog.tsx +1 -1
- package/src/components/CentrifugoMonitor/CentrifugoMonitorWidget.tsx +1 -1
- package/src/components/ConnectionStatus/ConnectionStatus.tsx +1 -1
- package/src/components/ConnectionStatus/ConnectionStatusCard.tsx +1 -1
- package/src/components/MessagesFeed/MessageFilters.tsx +1 -1
- package/src/components/MessagesFeed/MessagesFeed.tsx +2 -2
- package/src/components/SubscriptionsList/SubscriptionsList.tsx +1 -1
- package/src/config.ts +21 -0
- package/src/debug/ConnectionTab/ConnectionTab.tsx +1 -1
- package/src/debug/DebugPanel/DebugPanel.tsx +1 -1
- package/src/debug/LogsTab/LogsTab.tsx +2 -2
- package/src/debug/SubscriptionsTab/SubscriptionsTab.tsx +1 -1
- package/src/events.ts +1 -1
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +89 -12
package/README.md
CHANGED
|
@@ -844,7 +844,7 @@ pnpm dev
|
|
|
844
844
|
## Requirements
|
|
845
845
|
|
|
846
846
|
- React 18+
|
|
847
|
-
- `@djangocfg/ui` - UI components (shadcn/ui based)
|
|
847
|
+
- `@djangocfg/ui-nextjs` - UI components (shadcn/ui based)
|
|
848
848
|
- `@djangocfg/layouts` - Layout components
|
|
849
849
|
- `centrifuge` - WebSocket client library
|
|
850
850
|
- `moment` - Date manipulation library
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/centrifugo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Production-ready Centrifugo WebSocket client for React with real-time subscriptions, RPC patterns, and connection state management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"centrifugo",
|
|
@@ -51,20 +51,20 @@
|
|
|
51
51
|
"centrifuge": "^5.2.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@djangocfg/ui": "^1.
|
|
55
|
-
"@djangocfg/layouts": "^2.
|
|
54
|
+
"@djangocfg/ui-nextjs": "^2.1.1",
|
|
55
|
+
"@djangocfg/layouts": "^2.1.1",
|
|
56
56
|
"consola": "^3.4.2",
|
|
57
|
-
"lucide-react": "^0.
|
|
57
|
+
"lucide-react": "^0.545.0",
|
|
58
58
|
"moment": "^2.30.1",
|
|
59
|
-
"react": "^19.
|
|
60
|
-
"react-dom": "^19.
|
|
59
|
+
"react": "^19.1.0",
|
|
60
|
+
"react-dom": "^19.1.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@djangocfg/typescript-config": "^1.
|
|
64
|
-
"@types/react": "^19.0
|
|
65
|
-
"@types/react-dom": "^19.0
|
|
63
|
+
"@djangocfg/typescript-config": "^2.1.1",
|
|
64
|
+
"@types/react": "^19.1.0",
|
|
65
|
+
"@types/react-dom": "^19.1.0",
|
|
66
66
|
"moment": "^2.30.1",
|
|
67
|
-
"typescript": "^5.
|
|
67
|
+
"typescript": "^5.9.3"
|
|
68
68
|
},
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@djangocfg/ui';
|
|
11
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@djangocfg/ui-nextjs';
|
|
12
12
|
import { ConnectionStatus } from '../ConnectionStatus';
|
|
13
13
|
import { MessagesFeed } from '../MessagesFeed';
|
|
14
14
|
import { SubscriptionsList } from '../SubscriptionsList';
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SheetTitle,
|
|
16
16
|
SheetDescription,
|
|
17
17
|
useEventListener,
|
|
18
|
-
} from '@djangocfg/ui';
|
|
18
|
+
} from '@djangocfg/ui-nextjs';
|
|
19
19
|
import { Activity } from 'lucide-react';
|
|
20
20
|
import { CentrifugoMonitor } from './CentrifugoMonitor';
|
|
21
21
|
import { CENTRIFUGO_MONITOR_EVENTS, type OpenMonitorDialogPayload } from '../../events';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { Card, CardContent, CardHeader, CardTitle, Button } from '@djangocfg/ui';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, Button } from '@djangocfg/ui-nextjs';
|
|
12
12
|
import { Activity, Maximize2 } from 'lucide-react';
|
|
13
13
|
import { ConnectionStatus } from '../ConnectionStatus';
|
|
14
14
|
import { emitOpenMonitorDialog } from '../../events';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import React, { useState, useEffect } from 'react';
|
|
11
|
-
import { Badge } from '@djangocfg/ui';
|
|
11
|
+
import { Badge } from '@djangocfg/ui-nextjs';
|
|
12
12
|
import { Wifi, WifiOff, Radio, Clock } from 'lucide-react';
|
|
13
13
|
import moment from 'moment';
|
|
14
14
|
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { Card, CardContent, CardHeader, CardTitle } from '@djangocfg/ui';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@djangocfg/ui-nextjs';
|
|
12
12
|
import { Wifi, WifiOff } from 'lucide-react';
|
|
13
13
|
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
14
14
|
import { ConnectionStatus } from './ConnectionStatus';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
|
-
import { Input, Badge, Button } from '@djangocfg/ui';
|
|
10
|
+
import { Input, Badge, Button } from '@djangocfg/ui-nextjs';
|
|
11
11
|
import { Search, Filter, X } from 'lucide-react';
|
|
12
12
|
import type { MessageFilters as MessageFiltersType } from './types';
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
Badge,
|
|
17
17
|
Button,
|
|
18
18
|
ScrollArea,
|
|
19
|
-
} from '@djangocfg/ui';
|
|
19
|
+
} from '@djangocfg/ui-nextjs';
|
|
20
20
|
import {
|
|
21
21
|
Trash2,
|
|
22
22
|
Pause,
|
|
@@ -324,7 +324,7 @@ export function MessagesFeed({
|
|
|
324
324
|
/>
|
|
325
325
|
)}
|
|
326
326
|
|
|
327
|
-
<ScrollArea className="h-[400px]"
|
|
327
|
+
<ScrollArea className="h-[400px]" viewportRef={scrollRef}>
|
|
328
328
|
{filteredMessages.length === 0 ? (
|
|
329
329
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
330
330
|
<Activity className="h-12 w-12 text-muted-foreground mb-4" />
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Badge,
|
|
16
16
|
Button,
|
|
17
17
|
ScrollArea,
|
|
18
|
-
} from '@djangocfg/ui';
|
|
18
|
+
} from '@djangocfg/ui-nextjs';
|
|
19
19
|
import { Radio, RefreshCw, Trash2 } from 'lucide-react';
|
|
20
20
|
import { Subscription, SubscriptionState } from 'centrifuge';
|
|
21
21
|
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
package/src/config.ts
CHANGED
|
@@ -8,9 +8,30 @@ export const isStaticBuild = process.env.NEXT_PHASE === 'phase-production-build'
|
|
|
8
8
|
|
|
9
9
|
const showDebugPanel = isDevelopment && !isStaticBuild;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Reconnect configuration with exponential backoff
|
|
13
|
+
*/
|
|
14
|
+
export const reconnectConfig = {
|
|
15
|
+
// Initial delay before first reconnect attempt (ms)
|
|
16
|
+
initialDelay: isDevelopment ? 2000 : 1000,
|
|
17
|
+
// Maximum delay between reconnect attempts (ms)
|
|
18
|
+
maxDelay: isDevelopment ? 30000 : 60000,
|
|
19
|
+
// Multiplier for exponential backoff
|
|
20
|
+
multiplier: 1.5,
|
|
21
|
+
// Maximum number of reconnect attempts
|
|
22
|
+
// Dev: 3 attempts then stop (server probably not running)
|
|
23
|
+
// Prod: 10 attempts then stop (avoid infinite reconnection spam)
|
|
24
|
+
maxAttempts: isDevelopment ? 3 : 10,
|
|
25
|
+
// Jitter factor to randomize delays (0-1)
|
|
26
|
+
jitter: 0.1,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
11
29
|
export const centrifugoConfig = {
|
|
12
30
|
// Show debug panel only in development and not in static builds
|
|
13
31
|
showDebugPanel,
|
|
32
|
+
// Reconnect settings
|
|
33
|
+
reconnect: reconnectConfig,
|
|
14
34
|
} as const;
|
|
15
35
|
|
|
16
36
|
export type CentrifugoConfig = typeof centrifugoConfig;
|
|
37
|
+
export type ReconnectConfig = typeof reconnectConfig;
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
TabsList,
|
|
20
20
|
TabsTrigger,
|
|
21
21
|
TabsContent,
|
|
22
|
-
} from '@djangocfg/ui';
|
|
22
|
+
} from '@djangocfg/ui-nextjs';
|
|
23
23
|
import { ConnectionTab } from '../ConnectionTab';
|
|
24
24
|
import { LogsTab } from '../LogsTab';
|
|
25
25
|
import { SubscriptionsTab } from '../SubscriptionsTab';
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
ScrollArea,
|
|
26
26
|
Badge,
|
|
27
27
|
PrettyCode,
|
|
28
|
-
} from '@djangocfg/ui';
|
|
28
|
+
} from '@djangocfg/ui-nextjs';
|
|
29
29
|
import { useLogs } from '../../providers/LogsProvider';
|
|
30
30
|
import type { LogLevel, LogEntry } from '../../core/types';
|
|
31
31
|
|
|
@@ -181,7 +181,7 @@ export function LogsTab() {
|
|
|
181
181
|
{/* Logs Viewer (Bash-like) */}
|
|
182
182
|
<Card>
|
|
183
183
|
<CardContent className="p-0">
|
|
184
|
-
<ScrollArea
|
|
184
|
+
<ScrollArea viewportRef={scrollRef} className="h-[400px] w-full">
|
|
185
185
|
<div className="p-4 font-mono text-xs space-y-1 bg-slate-950 text-slate-50">
|
|
186
186
|
{filteredLogs.length === 0 ? (
|
|
187
187
|
<div className="text-slate-500 text-center py-8">
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
Button,
|
|
18
18
|
ScrollArea,
|
|
19
19
|
Separator,
|
|
20
|
-
} from '@djangocfg/ui';
|
|
20
|
+
} from '@djangocfg/ui-nextjs';
|
|
21
21
|
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
22
22
|
|
|
23
23
|
// ─────────────────────────────────────────────────────────────────────────
|
package/src/events.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { CentrifugoRPCClient } from '../../core/client';
|
|
|
13
13
|
import { createLogger } from '../../core/logger';
|
|
14
14
|
import type { ConnectionState, CentrifugoToken, ActiveSubscription } from '../../core/types';
|
|
15
15
|
import { LogsProvider } from '../LogsProvider';
|
|
16
|
-
import { isStaticBuild } from '../../config';
|
|
16
|
+
import { isStaticBuild, isDevelopment, reconnectConfig } from '../../config';
|
|
17
17
|
import { CentrifugoMonitorDialog } from '../../components/CentrifugoMonitor/CentrifugoMonitorDialog';
|
|
18
18
|
|
|
19
19
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -85,10 +85,32 @@ function CentrifugoProviderInner({
|
|
|
85
85
|
const hasConnectedRef = useRef(false);
|
|
86
86
|
const isConnectingRef = useRef(false);
|
|
87
87
|
const isMountedRef = useRef(true);
|
|
88
|
+
const reconnectAttemptRef = useRef(0);
|
|
89
|
+
const devWarningShownRef = useRef(false);
|
|
90
|
+
const reconnectStoppedRef = useRef(false); // Track if we should stop reconnecting
|
|
91
|
+
const connectRef = useRef<(() => Promise<void>) | null>(null);
|
|
92
|
+
const disconnectRef = useRef<(() => void) | null>(null);
|
|
88
93
|
|
|
89
94
|
const centrifugoToken: CentrifugoToken | undefined = user?.centrifugo;
|
|
90
95
|
const hasCentrifugoToken = !!centrifugoToken?.token;
|
|
91
96
|
|
|
97
|
+
// Calculate reconnect delay with exponential backoff
|
|
98
|
+
const getReconnectDelay = useCallback((attempt: number): number => {
|
|
99
|
+
const { initialDelay, maxDelay, multiplier, jitter } = reconnectConfig;
|
|
100
|
+
|
|
101
|
+
// Exponential backoff: initialDelay * multiplier^attempt
|
|
102
|
+
let delay = initialDelay * Math.pow(multiplier, attempt);
|
|
103
|
+
|
|
104
|
+
// Cap at maxDelay
|
|
105
|
+
delay = Math.min(delay, maxDelay);
|
|
106
|
+
|
|
107
|
+
// Add jitter to prevent thundering herd
|
|
108
|
+
const jitterAmount = delay * jitter * (Math.random() * 2 - 1);
|
|
109
|
+
delay = Math.round(delay + jitterAmount);
|
|
110
|
+
|
|
111
|
+
return delay;
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
92
114
|
const wsUrl = useMemo(() => {
|
|
93
115
|
if (url) return url;
|
|
94
116
|
if (centrifugoToken?.centrifugo_url) return centrifugoToken.centrifugo_url;
|
|
@@ -165,6 +187,8 @@ function CentrifugoProviderInner({
|
|
|
165
187
|
|
|
166
188
|
// Connect function
|
|
167
189
|
const connect = useCallback(async () => {
|
|
190
|
+
// Don't reconnect if we've decided to stop (dev mode hit max attempts)
|
|
191
|
+
if (reconnectStoppedRef.current) return;
|
|
168
192
|
if (hasConnectedRef.current || isConnectingRef.current) return;
|
|
169
193
|
if (isConnecting || isConnected) return;
|
|
170
194
|
|
|
@@ -203,12 +227,23 @@ function CentrifugoProviderInner({
|
|
|
203
227
|
hasConnectedRef.current = true;
|
|
204
228
|
isConnectingRef.current = false;
|
|
205
229
|
|
|
230
|
+
// Clear any pending reconnect timeout
|
|
231
|
+
if (reconnectTimeoutRef.current) {
|
|
232
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
233
|
+
reconnectTimeoutRef.current = null;
|
|
234
|
+
}
|
|
235
|
+
|
|
206
236
|
setClient(rpcClient);
|
|
207
237
|
setIsConnected(true);
|
|
208
238
|
setConnectionTime(new Date());
|
|
209
239
|
setError(null);
|
|
210
240
|
|
|
211
241
|
logger.success('WebSocket connected');
|
|
242
|
+
|
|
243
|
+
// Reset reconnect state on successful connection
|
|
244
|
+
reconnectAttemptRef.current = 0;
|
|
245
|
+
devWarningShownRef.current = false;
|
|
246
|
+
reconnectStoppedRef.current = false;
|
|
212
247
|
} catch (err) {
|
|
213
248
|
const error = err instanceof Error ? err : new Error('Connection failed');
|
|
214
249
|
setError(error);
|
|
@@ -225,16 +260,51 @@ function CentrifugoProviderInner({
|
|
|
225
260
|
if (isAuthError) {
|
|
226
261
|
logger.error('Authentication failed', error);
|
|
227
262
|
} else {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
263
|
+
// Check if we should attempt reconnect
|
|
264
|
+
const { maxAttempts } = reconnectConfig;
|
|
265
|
+
const currentAttempt = reconnectAttemptRef.current;
|
|
266
|
+
|
|
267
|
+
// In dev mode: show warning once and stop after maxAttempts
|
|
268
|
+
if (isDevelopment) {
|
|
269
|
+
if (!devWarningShownRef.current) {
|
|
270
|
+
devWarningShownRef.current = true;
|
|
271
|
+
logger.warning(
|
|
272
|
+
'🔌 Centrifugo server is not running. ' +
|
|
273
|
+
'Start it with: docker compose -f docker-compose-local-services.yml up centrifugo'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Stop reconnecting after maxAttempts in dev mode
|
|
278
|
+
if (maxAttempts > 0 && currentAttempt >= maxAttempts) {
|
|
279
|
+
reconnectStoppedRef.current = true; // Mark as stopped permanently
|
|
280
|
+
logger.info(`Stopped reconnecting after ${maxAttempts} attempts (dev mode)`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Try to reconnect with exponential backoff (respects maxAttempts)
|
|
286
|
+
if (currentAttempt < maxAttempts) {
|
|
287
|
+
const delay = getReconnectDelay(currentAttempt);
|
|
288
|
+
reconnectAttemptRef.current = currentAttempt + 1;
|
|
289
|
+
|
|
290
|
+
if (!isDevelopment || currentAttempt < 2) {
|
|
291
|
+
// Only log in prod, or first 2 attempts in dev
|
|
292
|
+
logger.info(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${currentAttempt + 1}/${maxAttempts})...`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
296
|
+
connectRef.current?.();
|
|
297
|
+
}, delay);
|
|
298
|
+
} else {
|
|
299
|
+
// Max attempts reached - stop reconnecting
|
|
300
|
+
reconnectStoppedRef.current = true;
|
|
301
|
+
logger.warning(`Stopped reconnecting after ${maxAttempts} attempts. WebSocket server may be unavailable.`);
|
|
302
|
+
}
|
|
233
303
|
}
|
|
234
304
|
} finally {
|
|
235
305
|
setIsConnecting(false);
|
|
236
306
|
}
|
|
237
|
-
}, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected]);
|
|
307
|
+
}, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected, getReconnectDelay]);
|
|
238
308
|
|
|
239
309
|
// Disconnect function
|
|
240
310
|
const disconnect = useCallback(() => {
|
|
@@ -257,6 +327,9 @@ function CentrifugoProviderInner({
|
|
|
257
327
|
|
|
258
328
|
hasConnectedRef.current = false;
|
|
259
329
|
isConnectingRef.current = false;
|
|
330
|
+
reconnectAttemptRef.current = 0;
|
|
331
|
+
devWarningShownRef.current = false;
|
|
332
|
+
reconnectStoppedRef.current = false; // Reset so manual reconnect works
|
|
260
333
|
}, [client, logger]);
|
|
261
334
|
|
|
262
335
|
// Reconnect function
|
|
@@ -284,12 +357,16 @@ function CentrifugoProviderInner({
|
|
|
284
357
|
}
|
|
285
358
|
}, [client, logger]);
|
|
286
359
|
|
|
287
|
-
//
|
|
360
|
+
// Keep refs up-to-date
|
|
361
|
+
connectRef.current = connect;
|
|
362
|
+
disconnectRef.current = disconnect;
|
|
363
|
+
|
|
364
|
+
// Auto-connect on mount - uses refs to avoid recreation issues
|
|
288
365
|
useEffect(() => {
|
|
289
366
|
isMountedRef.current = true;
|
|
290
367
|
|
|
291
|
-
if (autoConnect && !hasConnectedRef.current) {
|
|
292
|
-
|
|
368
|
+
if (autoConnect && !hasConnectedRef.current && !reconnectStoppedRef.current) {
|
|
369
|
+
connectRef.current?.();
|
|
293
370
|
}
|
|
294
371
|
|
|
295
372
|
return () => {
|
|
@@ -302,9 +379,9 @@ function CentrifugoProviderInner({
|
|
|
302
379
|
}
|
|
303
380
|
|
|
304
381
|
isMountedRef.current = false;
|
|
305
|
-
|
|
382
|
+
disconnectRef.current?.();
|
|
306
383
|
};
|
|
307
|
-
}, [autoConnect, connect
|
|
384
|
+
}, [autoConnect]); // Only depend on autoConnect, not on connect/disconnect
|
|
308
385
|
|
|
309
386
|
const connectionState: ConnectionState = isConnected
|
|
310
387
|
? 'connected'
|