@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 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.4.36",
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.4.36",
55
- "@djangocfg/layouts": "^2.0.6",
54
+ "@djangocfg/ui-nextjs": "^2.1.1",
55
+ "@djangocfg/layouts": "^2.1.1",
56
56
  "consola": "^3.4.2",
57
- "lucide-react": "^0.468.0",
57
+ "lucide-react": "^0.545.0",
58
58
  "moment": "^2.30.1",
59
- "react": "^19.0.0",
60
- "react-dom": "^19.0.0"
59
+ "react": "^19.1.0",
60
+ "react-dom": "^19.1.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@djangocfg/typescript-config": "^1.4.36",
64
- "@types/react": "^19.0.6",
65
- "@types/react-dom": "^19.0.2",
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.7.3"
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]" ref={scrollRef}>
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;
@@ -15,7 +15,7 @@ import {
15
15
  Badge,
16
16
  Button,
17
17
  Separator,
18
- } from '@djangocfg/ui';
18
+ } from '@djangocfg/ui-nextjs';
19
19
  import { useCentrifugo } from '../../providers/CentrifugoProvider';
20
20
 
21
21
  // ─────────────────────────────────────────────────────────────────────────
@@ -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 ref={scrollRef} className="h-[400px] w-full">
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
@@ -4,7 +4,7 @@
4
4
  * Event-driven communication for Centrifugo monitor dialog
5
5
  */
6
6
 
7
- import { events } from '@djangocfg/ui';
7
+ import { events } from '@djangocfg/ui-nextjs';
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────────────────
10
10
  // Event Types
@@ -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
- logger.error('Connection failed', error);
229
- reconnectTimeoutRef.current = setTimeout(() => {
230
- logger.info('Attempting to reconnect...');
231
- connect();
232
- }, 5000);
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
- // Auto-connect on mount
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
- connect();
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
- disconnect();
382
+ disconnectRef.current?.();
306
383
  };
307
- }, [autoConnect, connect, disconnect]);
384
+ }, [autoConnect]); // Only depend on autoConnect, not on connect/disconnect
308
385
 
309
386
  const connectionState: ConnectionState = isConnected
310
387
  ? 'connected'