@djangocfg/centrifugo 2.1.224 → 2.1.226

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/centrifugo",
3
- "version": "2.1.224",
3
+ "version": "2.1.226",
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",
@@ -63,10 +63,10 @@
63
63
  "centrifuge": "^5.2.2"
64
64
  },
65
65
  "peerDependencies": {
66
- "@djangocfg/api": "^2.1.224",
67
- "@djangocfg/i18n": "^2.1.224",
68
- "@djangocfg/ui-core": "^2.1.224",
69
- "@djangocfg/ui-tools": "^2.1.224",
66
+ "@djangocfg/api": "^2.1.226",
67
+ "@djangocfg/i18n": "^2.1.226",
68
+ "@djangocfg/ui-core": "^2.1.226",
69
+ "@djangocfg/ui-tools": "^2.1.226",
70
70
  "consola": "^3.4.2",
71
71
  "lucide-react": "^0.545.0",
72
72
  "moment": "^2.30.1",
@@ -74,11 +74,11 @@
74
74
  "react-dom": "^19.1.0"
75
75
  },
76
76
  "devDependencies": {
77
- "@djangocfg/api": "^2.1.224",
78
- "@djangocfg/i18n": "^2.1.224",
79
- "@djangocfg/typescript-config": "^2.1.224",
80
- "@djangocfg/ui-core": "^2.1.224",
81
- "@djangocfg/ui-tools": "^2.1.224",
77
+ "@djangocfg/api": "^2.1.226",
78
+ "@djangocfg/i18n": "^2.1.226",
79
+ "@djangocfg/typescript-config": "^2.1.226",
80
+ "@djangocfg/ui-core": "^2.1.226",
81
+ "@djangocfg/ui-tools": "^2.1.226",
82
82
  "@types/node": "^24.7.2",
83
83
  "@types/react": "^19.1.0",
84
84
  "@types/react-dom": "^19.1.0",
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Resolve Centrifugo WebSocket URL.
3
+ *
4
+ * Priority:
5
+ * 1. Explicit `url` prop passed to CentrifugoProvider
6
+ * 2. `centrifugo_url` from JWT token (set by Django backend — correct for prod)
7
+ * 3. `NEXT_PUBLIC_CENTRIFUGO_URL` env var
8
+ * 4. Auto-derive from `NEXT_PUBLIC_API_URL` in dev (http://host → ws://host:8120)
9
+ * 5. '' — disabled, no connection attempted
10
+ *
11
+ * Prod safety: if resolved URL contains `localhost` or `127.0.0.1`, treat as
12
+ * disabled in production builds — Centrifugo is never on localhost in prod.
13
+ */
14
+
15
+ const CENTRIFUGO_WS_PATH = '/connection/websocket';
16
+ const DEV_CENTRIFUGO_PORT = '8120';
17
+
18
+ /** Convert http(s):// API URL to ws(s):// Centrifugo URL on the dev port. */
19
+ function deriveWsUrlFromApiUrl(apiUrl: string): string {
20
+ try {
21
+ const u = new URL(apiUrl);
22
+ const wsProto = u.protocol === 'https:' ? 'wss:' : 'ws:';
23
+ u.protocol = wsProto;
24
+ u.port = DEV_CENTRIFUGO_PORT;
25
+ u.pathname = CENTRIFUGO_WS_PATH;
26
+ return u.toString();
27
+ } catch {
28
+ return '';
29
+ }
30
+ }
31
+
32
+ /** True if URL points to localhost — not valid in production. */
33
+ function isLocalhost(url: string): boolean {
34
+ try {
35
+ const { hostname } = new URL(url);
36
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export interface ResolveWsUrlOptions {
43
+ /** Explicit URL from CentrifugoProvider props. Highest priority. */
44
+ urlProp?: string;
45
+ /** centrifugo_url from JWT token payload (set by Django). */
46
+ tokenUrl?: string;
47
+ /** NODE_ENV — pass `process.env.NODE_ENV`. */
48
+ nodeEnv?: string;
49
+ /** NEXT_PUBLIC_CENTRIFUGO_URL — pass `process.env.NEXT_PUBLIC_CENTRIFUGO_URL`. */
50
+ envCentrifugoUrl?: string;
51
+ /** NEXT_PUBLIC_API_URL — used for dev auto-derive. */
52
+ envApiUrl?: string;
53
+ }
54
+
55
+ export interface ResolvedWsUrl {
56
+ /** Final WebSocket URL to connect to, or '' if disabled. */
57
+ url: string;
58
+ /** Whether to attempt connection at all. False = disabled cleanly, no retries. */
59
+ enabled: boolean;
60
+ /** How the URL was resolved — useful for debug logging. */
61
+ source: 'prop' | 'token' | 'env' | 'derived' | 'disabled';
62
+ }
63
+
64
+ export function resolveWsUrl({
65
+ urlProp,
66
+ tokenUrl,
67
+ nodeEnv,
68
+ envCentrifugoUrl,
69
+ envApiUrl,
70
+ }: ResolveWsUrlOptions): ResolvedWsUrl {
71
+ const isDev = nodeEnv === 'development';
72
+ const isProd = !isDev;
73
+
74
+ // 1. Explicit prop
75
+ if (urlProp) {
76
+ if (isProd && isLocalhost(urlProp)) {
77
+ return { url: '', enabled: false, source: 'disabled' };
78
+ }
79
+ return { url: urlProp, enabled: true, source: 'prop' };
80
+ }
81
+
82
+ // 2. JWT token url (Django sends this for authenticated users)
83
+ if (tokenUrl) {
84
+ if (isProd && isLocalhost(tokenUrl)) {
85
+ return { url: '', enabled: false, source: 'disabled' };
86
+ }
87
+ return { url: tokenUrl, enabled: true, source: 'token' };
88
+ }
89
+
90
+ // 3. Env var NEXT_PUBLIC_CENTRIFUGO_URL
91
+ if (envCentrifugoUrl) {
92
+ if (isProd && isLocalhost(envCentrifugoUrl)) {
93
+ return { url: '', enabled: false, source: 'disabled' };
94
+ }
95
+ return { url: envCentrifugoUrl, enabled: true, source: 'env' };
96
+ }
97
+
98
+ // 4. Dev only: auto-derive from API URL
99
+ if (isDev && envApiUrl) {
100
+ const derived = deriveWsUrlFromApiUrl(envApiUrl);
101
+ if (derived) {
102
+ return { url: derived, enabled: true, source: 'derived' };
103
+ }
104
+ }
105
+
106
+ // 5. Nothing resolved — disable cleanly
107
+ return { url: '', enabled: false, source: 'disabled' };
108
+ }
package/src/index.ts CHANGED
@@ -116,6 +116,9 @@ export { useCodegenTip } from './hooks/useCodegenTip';
116
116
  export { centrifugoConfig, isDevelopment, isProduction, isStaticBuild } from './config';
117
117
  export type { CentrifugoConfig } from './config';
118
118
 
119
+ export { resolveWsUrl } from './core/utils/resolveWsUrl';
120
+ export type { ResolveWsUrlOptions, ResolvedWsUrl } from './core/utils/resolveWsUrl';
121
+
119
122
  // ─────────────────────────────────────────────────────────────────────────
120
123
  // Errors
121
124
  // ─────────────────────────────────────────────────────────────────────────
@@ -17,6 +17,7 @@ import {
17
17
  CentrifugoMonitorDialog
18
18
  } from '../../components/CentrifugoMonitor/CentrifugoMonitorDialog';
19
19
  import { isDevelopment, reconnectConfig } from '../../config';
20
+ import { resolveWsUrl } from '../../core/utils/resolveWsUrl';
20
21
  import { CentrifugoRPCClient } from '../../core/client';
21
22
  import { getConsolaLogger } from '../../core/logger/consolaLogger';
22
23
  import { useCodegenTip } from '../../hooks/useCodegenTip';
@@ -138,16 +139,19 @@ function CentrifugoProviderInner({
138
139
  return delay;
139
140
  }, []);
140
141
 
141
- const wsUrl = useMemo(() => {
142
- if (url) return url;
143
- if (centrifugoToken?.centrifugo_url) return centrifugoToken.centrifugo_url;
144
- return '';
145
- }, [url, centrifugoToken?.centrifugo_url]);
142
+ const { url: wsUrl, enabled: wsEnabled, source: wsSource } = useMemo(() => resolveWsUrl({
143
+ urlProp: url,
144
+ tokenUrl: centrifugoToken?.centrifugo_url,
145
+ nodeEnv: process.env.NODE_ENV,
146
+ envCentrifugoUrl: process.env.NEXT_PUBLIC_CENTRIFUGO_URL,
147
+ envApiUrl: process.env.NEXT_PUBLIC_API_URL,
148
+ }), [url, centrifugoToken?.centrifugo_url]);
146
149
 
147
150
  const autoConnect = autoConnectProp &&
148
151
  (isAuthenticated && !isLoading) &&
149
152
  enabled &&
150
- hasCentrifugoToken;
153
+ hasCentrifugoToken &&
154
+ wsEnabled;
151
155
 
152
156
  // Log connection decision
153
157
  useEffect(() => {
@@ -158,6 +162,8 @@ function CentrifugoProviderInner({
158
162
  enabled,
159
163
  hasToken: hasCentrifugoToken,
160
164
  url: wsUrl,
165
+ urlSource: wsSource,
166
+ wsEnabled,
161
167
  });
162
168
  }
163
169
  }, [autoConnect, isAuthenticated, isLoading, enabled, hasCentrifugoToken, logger, wsUrl]);
@@ -316,12 +322,13 @@ function CentrifugoProviderInner({
316
322
  reconnectStoppedRef.current = false;
317
323
  } catch (err) {
318
324
  const error = err instanceof Error ? err : new Error('Connection failed');
325
+ hasConnectedRef.current = false;
326
+ isConnectingRef.current = false;
327
+ if (!isMountedRef.current) return;
319
328
  setError(error);
320
329
  setClient(null);
321
330
  setIsConnected(false);
322
331
  setConnectionTime(null);
323
- hasConnectedRef.current = false;
324
- isConnectingRef.current = false;
325
332
 
326
333
  const isAuthError = error.message.includes('token') ||
327
334
  error.message.includes('auth') ||
@@ -363,7 +370,9 @@ function CentrifugoProviderInner({
363
370
  }
364
371
 
365
372
  reconnectTimeoutRef.current = setTimeout(() => {
366
- connectRef.current?.();
373
+ if (isMountedRef.current) {
374
+ connectRef.current?.();
375
+ }
367
376
  }, delay);
368
377
  } else {
369
378
  // Max attempts reached - stop reconnecting
@@ -372,7 +381,10 @@ function CentrifugoProviderInner({
372
381
  }
373
382
  }
374
383
  } finally {
375
- setIsConnecting(false);
384
+ isConnectingRef.current = false;
385
+ if (isMountedRef.current) {
386
+ setIsConnecting(false);
387
+ }
376
388
  }
377
389
  }, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected, getReconnectDelay, onTokenRefresh]);
378
390