@djangocfg/centrifugo 2.1.230 → 2.1.232
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 +40 -1387
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -5,73 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@djangocfg/centrifugo)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
**Part of [DjangoCFG](https://djangocfg.com)** — a modern Django framework for building production-ready SaaS applications.
|
|
8
|
+
**Part of [DjangoCFG](https://djangocfg.com)** — a modern Django framework for building production-ready SaaS applications.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Professional Centrifugo WebSocket client with React integration, composable UI components, and comprehensive monitoring tools.
|
|
13
|
-
|
|
14
|
-
## Features
|
|
15
|
-
|
|
16
|
-
- 🔌 **Robust WebSocket Connection** - Auto-reconnect, error handling, and connection state management
|
|
17
|
-
- 👁️ **Page Visibility Handling** - Auto-reconnect when tab becomes active, pause reconnects when hidden (saves battery)
|
|
18
|
-
- 🔄 **RPC Pattern Support** - Request-response via correlation ID for synchronous-like communication
|
|
19
|
-
- 📊 **Advanced Logging System** - Circular buffer with dual output (consola + in-memory accumulation)
|
|
20
|
-
- 🧩 **Composable UI Components** - Flexible, reusable components for any use case
|
|
21
|
-
- 🐛 **Monitoring Tools** - Real-time connection status, message feed, and subscriptions list
|
|
22
|
-
- ⚛️ **React Integration** - Context providers and hooks for seamless integration
|
|
23
|
-
- 🎯 **Type-Safe** - Full TypeScript support with proper Centrifuge types
|
|
24
|
-
- 🏗️ **Platform-Agnostic Core** - Core modules can be used without React
|
|
25
|
-
- 🎨 **Beautiful UI** - Pre-built components using shadcn/ui
|
|
26
|
-
- 📅 **Moment.js Integration** - Consistent UTC time handling throughout
|
|
27
|
-
|
|
28
|
-
## Architecture
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
src/
|
|
32
|
-
├── core/ # Platform-agnostic (no React dependencies)
|
|
33
|
-
│ ├── client/ # CentrifugoRPCClient - WebSocket client
|
|
34
|
-
│ │ ├── CentrifugoRPCClient.ts # Main facade (~165 lines)
|
|
35
|
-
│ │ ├── connection.ts # Connection lifecycle
|
|
36
|
-
│ │ ├── subscriptions.ts # Channel subscriptions
|
|
37
|
-
│ │ ├── rpc.ts # RPC methods (namedRPC, namedRPCWithRetry, namedRPCNoWait)
|
|
38
|
-
│ │ ├── version.ts # API version checking
|
|
39
|
-
│ │ ├── types.ts # Type definitions
|
|
40
|
-
│ │ └── index.ts # Exports
|
|
41
|
-
│ ├── errors/ # Error handling with retry logic
|
|
42
|
-
│ │ ├── RPCError.ts # Typed RPC errors (isRetryable, userMessage)
|
|
43
|
-
│ │ └── RPCRetryHandler.ts # Exponential backoff retry
|
|
44
|
-
│ ├── logger/ # Logging system with circular buffer
|
|
45
|
-
│ │ ├── createLogger.ts # Logger factory (supports string prefix)
|
|
46
|
-
│ │ └── LogsStore.ts # In-memory logs accumulation
|
|
47
|
-
│ └── types/ # TypeScript type definitions
|
|
48
|
-
├── events.ts # Unified event system (single 'centrifugo' event)
|
|
49
|
-
├── providers/ # React Context providers
|
|
50
|
-
│ ├── CentrifugoProvider/ # Main connection provider (no auto-FAB)
|
|
51
|
-
│ └── LogsProvider/ # Logs accumulation provider
|
|
52
|
-
├── hooks/ # React hooks
|
|
53
|
-
│ ├── useSubscription.ts # Channel subscription hook
|
|
54
|
-
│ ├── useRPC.ts # RPC request-response hook
|
|
55
|
-
│ ├── useNamedRPC.ts # Native Centrifugo RPC hook
|
|
56
|
-
│ └── usePageVisibility.ts # Browser tab visibility tracking
|
|
57
|
-
└── components/ # Composable UI components
|
|
58
|
-
├── ConnectionStatus/ # Connection status display
|
|
59
|
-
│ ├── ConnectionStatus.tsx # Badge/inline/detailed variants
|
|
60
|
-
│ └── ConnectionStatusCard.tsx # Card wrapper for dashboards
|
|
61
|
-
├── MessagesFeed/ # Real-time message feed
|
|
62
|
-
│ ├── MessagesFeed.tsx # Main feed component
|
|
63
|
-
│ ├── MessageFilters.tsx # Filtering controls
|
|
64
|
-
│ └── types.ts # Message types
|
|
65
|
-
├── SubscriptionsList/ # Active subscriptions list
|
|
66
|
-
│ └── SubscriptionsList.tsx # List with controls
|
|
67
|
-
└── CentrifugoMonitor/ # Main monitoring component
|
|
68
|
-
├── CentrifugoMonitor.tsx # Composable monitor
|
|
69
|
-
├── CentrifugoMonitorDialog.tsx # Modal variant
|
|
70
|
-
├── CentrifugoMonitorFAB.tsx # FAB variant (manual)
|
|
71
|
-
└── CentrifugoMonitorWidget.tsx # Dashboard widget
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Installation
|
|
10
|
+
## Install
|
|
75
11
|
|
|
76
12
|
```bash
|
|
77
13
|
pnpm add @djangocfg/centrifugo moment
|
|
@@ -84,49 +20,16 @@ pnpm add @djangocfg/centrifugo moment
|
|
|
84
20
|
```tsx
|
|
85
21
|
import { CentrifugoProvider } from '@djangocfg/centrifugo';
|
|
86
22
|
|
|
87
|
-
function App() {
|
|
88
|
-
return (
|
|
89
|
-
<CentrifugoProvider
|
|
90
|
-
enabled={true}
|
|
91
|
-
autoConnect={true}
|
|
92
|
-
>
|
|
93
|
-
<YourApp />
|
|
94
|
-
</CentrifugoProvider>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 2. Add Monitoring FAB (Optional)
|
|
100
|
-
|
|
101
|
-
The FAB is **not** automatically included. You control when and where to show it:
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
import { CentrifugoProvider, CentrifugoMonitorFAB } from '@djangocfg/centrifugo';
|
|
105
|
-
import { useAuth } from '@djangocfg/layouts';
|
|
106
|
-
|
|
107
|
-
function CentrifugoMonitor() {
|
|
108
|
-
const { isAdminUser } = useAuth();
|
|
109
|
-
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
110
|
-
|
|
111
|
-
// Show FAB only for admins or in development
|
|
112
|
-
if (!isDevelopment && !isAdminUser) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return <CentrifugoMonitorFAB variant="full" />;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
23
|
function App() {
|
|
120
24
|
return (
|
|
121
25
|
<CentrifugoProvider enabled={true} autoConnect={true}>
|
|
122
26
|
<YourApp />
|
|
123
|
-
<CentrifugoMonitor />
|
|
124
27
|
</CentrifugoProvider>
|
|
125
28
|
);
|
|
126
29
|
}
|
|
127
30
|
```
|
|
128
31
|
|
|
129
|
-
###
|
|
32
|
+
### 2. Use the connection
|
|
130
33
|
|
|
131
34
|
```tsx
|
|
132
35
|
import { useCentrifugo } from '@djangocfg/centrifugo';
|
|
@@ -134,1333 +37,83 @@ import { useCentrifugo } from '@djangocfg/centrifugo';
|
|
|
134
37
|
function YourComponent() {
|
|
135
38
|
const { isConnected, connectionState, uptime } = useCentrifugo();
|
|
136
39
|
|
|
137
|
-
return
|
|
138
|
-
<div>
|
|
139
|
-
<p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
140
|
-
<p>State: {connectionState}</p>
|
|
141
|
-
<p>Uptime: {uptime}s</p>
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
40
|
+
return <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>;
|
|
144
41
|
}
|
|
145
42
|
```
|
|
146
43
|
|
|
147
|
-
###
|
|
44
|
+
### 3. Subscribe to channels
|
|
148
45
|
|
|
149
46
|
```tsx
|
|
150
47
|
import { useSubscription } from '@djangocfg/centrifugo';
|
|
151
48
|
|
|
152
|
-
function
|
|
49
|
+
function Notifications() {
|
|
153
50
|
const { data, isSubscribed } = useSubscription({
|
|
154
51
|
channel: 'notifications',
|
|
155
52
|
enabled: true,
|
|
156
|
-
onPublication: (data) =>
|
|
157
|
-
console.log('New notification:', data);
|
|
158
|
-
},
|
|
159
|
-
onError: (error) => {
|
|
160
|
-
console.error('Subscription error:', error);
|
|
161
|
-
},
|
|
53
|
+
onPublication: (data) => console.log('New:', data),
|
|
162
54
|
});
|
|
163
55
|
|
|
164
|
-
return
|
|
165
|
-
<div>
|
|
166
|
-
<p>Subscribed: {isSubscribed ? 'Yes' : 'No'}</p>
|
|
167
|
-
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
|
168
|
-
</div>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Core APIs
|
|
174
|
-
|
|
175
|
-
### CentrifugoProvider
|
|
176
|
-
|
|
177
|
-
Main provider that manages the WebSocket connection.
|
|
178
|
-
|
|
179
|
-
**Props:**
|
|
180
|
-
- `enabled?: boolean` - Enable/disable the connection (default: `false`)
|
|
181
|
-
- `url?: string` - WebSocket URL (falls back to user.centrifugo.centrifugo_url)
|
|
182
|
-
- `autoConnect?: boolean` - Auto-connect when authenticated (default: `true`)
|
|
183
|
-
|
|
184
|
-
**Context Value:**
|
|
185
|
-
```typescript
|
|
186
|
-
interface CentrifugoContextValue {
|
|
187
|
-
// Client
|
|
188
|
-
client: CentrifugoRPCClient | null;
|
|
189
|
-
|
|
190
|
-
// Connection State
|
|
191
|
-
isConnected: boolean;
|
|
192
|
-
isConnecting: boolean;
|
|
193
|
-
error: Error | null;
|
|
194
|
-
connectionState: 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
195
|
-
|
|
196
|
-
// Connection Info
|
|
197
|
-
uptime: number; // seconds
|
|
198
|
-
subscriptions: string[];
|
|
199
|
-
activeSubscriptions: ActiveSubscription[];
|
|
200
|
-
|
|
201
|
-
// Controls
|
|
202
|
-
connect: () => Promise<void>;
|
|
203
|
-
disconnect: () => void;
|
|
204
|
-
reconnect: () => Promise<void>;
|
|
205
|
-
unsubscribe: (channel: string) => void;
|
|
206
|
-
|
|
207
|
-
// Config
|
|
208
|
-
enabled: boolean;
|
|
56
|
+
return <p>Subscribed: {isSubscribed ? 'Yes' : 'No'}</p>;
|
|
209
57
|
}
|
|
210
58
|
```
|
|
211
59
|
|
|
212
|
-
###
|
|
213
|
-
|
|
214
|
-
Hook to access the Centrifugo connection context.
|
|
215
|
-
|
|
216
|
-
```tsx
|
|
217
|
-
const {
|
|
218
|
-
client,
|
|
219
|
-
isConnected,
|
|
220
|
-
connectionState,
|
|
221
|
-
connect,
|
|
222
|
-
disconnect,
|
|
223
|
-
reconnect,
|
|
224
|
-
} = useCentrifugo();
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### useSubscription()
|
|
228
|
-
|
|
229
|
-
Hook to subscribe to a channel with auto-cleanup.
|
|
230
|
-
|
|
231
|
-
```tsx
|
|
232
|
-
const { data, isSubscribed, error } = useSubscription({
|
|
233
|
-
channel: 'my-channel',
|
|
234
|
-
enabled: true, // optional
|
|
235
|
-
onPublication: (data) => {
|
|
236
|
-
// Handle new data
|
|
237
|
-
},
|
|
238
|
-
onError: (error) => {
|
|
239
|
-
// Handle error
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**Options:**
|
|
245
|
-
```typescript
|
|
246
|
-
interface UseSubscriptionOptions<T> {
|
|
247
|
-
channel: string;
|
|
248
|
-
enabled?: boolean;
|
|
249
|
-
onPublication?: (data: T) => void;
|
|
250
|
-
onError?: (error: Error) => void;
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### useRPC()
|
|
255
|
-
|
|
256
|
-
Hook for making RPC calls using correlation ID pattern.
|
|
257
|
-
|
|
258
|
-
**What is RPC Pattern?**
|
|
259
|
-
|
|
260
|
-
Centrifugo is pub/sub (fire-and-forget) and doesn't support RPC natively. We implement request-response pattern using **correlation ID** to match requests with responses:
|
|
261
|
-
|
|
262
|
-
```
|
|
263
|
-
1. Client generates unique correlation_id
|
|
264
|
-
2. Client subscribes to personal reply channel (user#{userId})
|
|
265
|
-
3. Client publishes request with correlation_id to rpc#{method}
|
|
266
|
-
4. Backend processes and publishes response with same correlation_id
|
|
267
|
-
5. Client matches response by correlation_id and resolves Promise
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Basic Usage:**
|
|
271
|
-
|
|
272
|
-
```tsx
|
|
273
|
-
import { useRPC } from '@djangocfg/centrifugo';
|
|
274
|
-
|
|
275
|
-
function MyComponent() {
|
|
276
|
-
const { call, isLoading, error } = useRPC();
|
|
277
|
-
|
|
278
|
-
const handleGetStats = async () => {
|
|
279
|
-
try {
|
|
280
|
-
const result = await call('tasks.get_stats', { bot_id: '123' });
|
|
281
|
-
console.log('Stats:', result);
|
|
282
|
-
} catch (error) {
|
|
283
|
-
console.error('RPC failed:', error);
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
return (
|
|
288
|
-
<button onClick={handleGetStats} disabled={isLoading}>
|
|
289
|
-
{isLoading ? 'Loading...' : 'Get Stats'}
|
|
290
|
-
</button>
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
**With Type Safety:**
|
|
296
|
-
|
|
297
|
-
```tsx
|
|
298
|
-
interface GetStatsRequest {
|
|
299
|
-
bot_id: string;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
interface GetStatsResponse {
|
|
303
|
-
total_tasks: number;
|
|
304
|
-
completed: number;
|
|
305
|
-
failed: number;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const { call } = useRPC();
|
|
309
|
-
|
|
310
|
-
const result = await call<GetStatsRequest, GetStatsResponse>(
|
|
311
|
-
'tasks.get_stats',
|
|
312
|
-
{ bot_id: '123' }
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
console.log(result.total_tasks); // Type-safe!
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
**With Custom Options:**
|
|
319
|
-
|
|
320
|
-
```tsx
|
|
321
|
-
const { call, isLoading, error, reset } = useRPC({
|
|
322
|
-
timeout: 30000, // 30 seconds
|
|
323
|
-
onError: (error) => {
|
|
324
|
-
toast.error(`RPC failed: ${error.message}`);
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Reset state
|
|
329
|
-
reset();
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Options:**
|
|
333
|
-
```typescript
|
|
334
|
-
interface UseRPCOptions {
|
|
335
|
-
timeout?: number; // Default: 10000ms
|
|
336
|
-
replyChannel?: string; // Default: user#{userId}
|
|
337
|
-
onError?: (error: Error) => void;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
interface UseRPCResult {
|
|
341
|
-
call: <TRequest, TResponse>(
|
|
342
|
-
method: string,
|
|
343
|
-
params: TRequest,
|
|
344
|
-
options?: UseRPCOptions
|
|
345
|
-
) => Promise<TResponse>;
|
|
346
|
-
isLoading: boolean;
|
|
347
|
-
error: Error | null;
|
|
348
|
-
reset: () => void;
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### useNamedRPC() - Native Centrifugo RPC
|
|
353
|
-
|
|
354
|
-
Hook for making **native Centrifugo RPC calls** via RPC proxy.
|
|
355
|
-
|
|
356
|
-
**This is the recommended approach** for request-response patterns. It uses Centrifugo's built-in RPC mechanism which proxies requests to Django.
|
|
357
|
-
|
|
358
|
-
**Flow:**
|
|
359
|
-
```
|
|
360
|
-
1. Client calls namedRPC('terminal.input', data)
|
|
361
|
-
2. Centrifuge.js sends RPC over WebSocket
|
|
362
|
-
3. Centrifugo proxies to Django: POST /centrifugo/rpc/
|
|
363
|
-
4. Django routes to @websocket_rpc handler
|
|
364
|
-
5. Response returned to client
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
**Basic Usage:**
|
|
60
|
+
### 4. Make RPC calls
|
|
368
61
|
|
|
369
62
|
```tsx
|
|
370
63
|
import { useNamedRPC } from '@djangocfg/centrifugo';
|
|
371
64
|
|
|
372
|
-
function
|
|
373
|
-
const { call, isLoading
|
|
374
|
-
|
|
375
|
-
const handleSendInput = async (input: string) => {
|
|
376
|
-
try {
|
|
377
|
-
const result = await call('terminal.input', {
|
|
378
|
-
session_id: 'abc-123',
|
|
379
|
-
data: btoa(input) // Base64 encode
|
|
380
|
-
});
|
|
381
|
-
console.log('Result:', result);
|
|
382
|
-
} catch (error) {
|
|
383
|
-
console.error('RPC failed:', error);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
return (
|
|
388
|
-
<button onClick={() => handleSendInput('ls -la')} disabled={isLoading}>
|
|
389
|
-
{isLoading ? 'Sending...' : 'Send Command'}
|
|
390
|
-
</button>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
**With Type Safety:**
|
|
396
|
-
|
|
397
|
-
```tsx
|
|
398
|
-
interface TerminalInputRequest {
|
|
399
|
-
session_id: string;
|
|
400
|
-
data: string; // Base64 encoded
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
interface TerminalInputResponse {
|
|
404
|
-
success: boolean;
|
|
405
|
-
message?: string;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const { call } = useNamedRPC();
|
|
409
|
-
|
|
410
|
-
const result = await call<TerminalInputRequest, TerminalInputResponse>(
|
|
411
|
-
'terminal.input',
|
|
412
|
-
{ session_id: 'abc-123', data: btoa('ls -la') }
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
console.log(result.success); // Type-safe!
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
**Backend Handler:**
|
|
419
|
-
|
|
420
|
-
```python
|
|
421
|
-
# Django backend with @websocket_rpc decorator
|
|
422
|
-
from pydantic import BaseModel, Field
|
|
423
|
-
from django_cfg.apps.integrations.centrifugo.decorators import websocket_rpc
|
|
424
|
-
|
|
425
|
-
class TerminalInputParams(BaseModel):
|
|
426
|
-
session_id: str = Field(..., description="Session UUID")
|
|
427
|
-
data: str = Field(..., description="Base64 encoded input")
|
|
428
|
-
|
|
429
|
-
class SuccessResult(BaseModel):
|
|
430
|
-
success: bool
|
|
431
|
-
message: str = ""
|
|
65
|
+
function Terminal() {
|
|
66
|
+
const { call, isLoading } = useNamedRPC();
|
|
432
67
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
"""Handle terminal input from browser."""
|
|
436
|
-
# Forward to gRPC/Electron terminal
|
|
437
|
-
await forward_to_terminal(params.session_id, base64.b64decode(params.data))
|
|
438
|
-
return SuccessResult(success=True, message="Input sent")
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
**Options:**
|
|
442
|
-
```typescript
|
|
443
|
-
interface UseNamedRPCOptions {
|
|
444
|
-
onError?: (error: Error) => void;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
interface UseNamedRPCResult {
|
|
448
|
-
call: <TRequest, TResponse>(
|
|
449
|
-
method: string,
|
|
450
|
-
data: TRequest
|
|
451
|
-
) => Promise<TResponse>;
|
|
452
|
-
isLoading: boolean;
|
|
453
|
-
error: Error | null;
|
|
454
|
-
reset: () => void;
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
> **Note:** `useNamedRPC` uses native Centrifugo RPC which requires RPC proxy to be configured in Centrifugo server. See the [Setup Guide](https://djangocfg.com/docs/features/integrations/centrifugo/client-generation/) for configuration details.
|
|
459
|
-
|
|
460
|
-
### usePageVisibility()
|
|
461
|
-
|
|
462
|
-
Hook for tracking browser tab visibility. Built into `CentrifugoProvider` for automatic reconnection handling.
|
|
463
|
-
|
|
464
|
-
**Built-in Behavior (CentrifugoProvider):**
|
|
465
|
-
|
|
466
|
-
The provider automatically handles visibility changes:
|
|
467
|
-
- **Tab hidden**: Pauses reconnect attempts (saves battery)
|
|
468
|
-
- **Tab visible**: Triggers reconnect if connection was lost
|
|
469
|
-
|
|
470
|
-
**Standalone Usage:**
|
|
471
|
-
|
|
472
|
-
```tsx
|
|
473
|
-
import { usePageVisibility } from '@djangocfg/centrifugo';
|
|
474
|
-
|
|
475
|
-
function MyComponent() {
|
|
476
|
-
const { isVisible, wasHidden, hiddenDuration } = usePageVisibility({
|
|
477
|
-
onVisible: () => {
|
|
478
|
-
console.log('Tab is now visible');
|
|
479
|
-
// Refresh data, resume animations, etc.
|
|
480
|
-
},
|
|
481
|
-
onHidden: () => {
|
|
482
|
-
console.log('Tab is now hidden');
|
|
483
|
-
// Pause expensive operations
|
|
484
|
-
},
|
|
485
|
-
onChange: (isVisible) => {
|
|
486
|
-
console.log('Visibility changed:', isVisible);
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
return (
|
|
491
|
-
<div>
|
|
492
|
-
<p>Tab visible: {isVisible ? 'Yes' : 'No'}</p>
|
|
493
|
-
<p>Was hidden: {wasHidden ? 'Yes' : 'No'}</p>
|
|
494
|
-
{hiddenDuration > 0 && (
|
|
495
|
-
<p>Was hidden for: {Math.round(hiddenDuration / 1000)}s</p>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
**Return Value:**
|
|
503
|
-
```typescript
|
|
504
|
-
interface UsePageVisibilityResult {
|
|
505
|
-
/** Whether the page is currently visible */
|
|
506
|
-
isVisible: boolean;
|
|
507
|
-
/** Whether the page was ever hidden during this session */
|
|
508
|
-
wasHidden: boolean;
|
|
509
|
-
/** Timestamp when page became visible */
|
|
510
|
-
visibleSince: number | null;
|
|
511
|
-
/** How long the page was hidden (ms) */
|
|
512
|
-
hiddenDuration: number;
|
|
513
|
-
/** Force check visibility state */
|
|
514
|
-
checkVisibility: () => boolean;
|
|
515
|
-
}
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
**Options:**
|
|
519
|
-
```typescript
|
|
520
|
-
interface UsePageVisibilityOptions {
|
|
521
|
-
/** Callback when page becomes visible */
|
|
522
|
-
onVisible?: () => void;
|
|
523
|
-
/** Callback when page becomes hidden */
|
|
524
|
-
onHidden?: () => void;
|
|
525
|
-
/** Callback with visibility state change */
|
|
526
|
-
onChange?: (isVisible: boolean) => void;
|
|
527
|
-
}
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
**Use Cases:**
|
|
531
|
-
- Pause/resume WebSocket reconnection attempts
|
|
532
|
-
- Refresh stale data when tab becomes active
|
|
533
|
-
- Pause expensive animations or timers
|
|
534
|
-
- Track user engagement metrics
|
|
535
|
-
|
|
536
|
-
### namedRPCNoWait() - Fire-and-Forget RPC
|
|
537
|
-
|
|
538
|
-
For latency-sensitive operations (like terminal input), use the fire-and-forget variant that returns immediately without waiting for a response.
|
|
539
|
-
|
|
540
|
-
**When to use:**
|
|
541
|
-
- Terminal input (user typing)
|
|
542
|
-
- Real-time cursor position updates
|
|
543
|
-
- Any operation where you don't need the response
|
|
544
|
-
|
|
545
|
-
**Direct Client Usage:**
|
|
546
|
-
|
|
547
|
-
```tsx
|
|
548
|
-
import { useCentrifugo } from '@djangocfg/centrifugo';
|
|
549
|
-
|
|
550
|
-
function TerminalInput() {
|
|
551
|
-
const { client } = useCentrifugo();
|
|
552
|
-
|
|
553
|
-
const handleKeyPress = (key: string) => {
|
|
554
|
-
// Fire-and-forget: returns immediately, doesn't wait for response
|
|
555
|
-
client?.namedRPCNoWait('terminal.input', {
|
|
68
|
+
const send = async (input: string) => {
|
|
69
|
+
const result = await call('terminal.input', {
|
|
556
70
|
session_id: 'abc-123',
|
|
557
|
-
data: btoa(
|
|
71
|
+
data: btoa(input),
|
|
558
72
|
});
|
|
559
73
|
};
|
|
560
|
-
|
|
561
|
-
return <input onKeyPress={(e) => handleKeyPress(e.key)} />;
|
|
562
|
-
}
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
**Backend Handler (Django):**
|
|
566
|
-
|
|
567
|
-
```python
|
|
568
|
-
@websocket_rpc("terminal.input", no_wait=True)
|
|
569
|
-
async def terminal_input(conn, params: TerminalInputParams) -> SuccessResult:
|
|
570
|
-
"""Handle terminal input (fire-and-forget)."""
|
|
571
|
-
import asyncio
|
|
572
|
-
|
|
573
|
-
# Spawn background task, return immediately
|
|
574
|
-
asyncio.create_task(_send_input_to_agent(params.session_id, params.data))
|
|
575
|
-
|
|
576
|
-
return SuccessResult(success=True, message="Input queued")
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
**Retry Logic with Exponential Backoff:**
|
|
580
|
-
|
|
581
|
-
`namedRPCNoWait` includes automatic retry with exponential backoff:
|
|
582
|
-
|
|
583
|
-
```tsx
|
|
584
|
-
// Default: 3 retries, 100ms base delay, 2000ms max delay
|
|
585
|
-
client?.namedRPCNoWait('terminal.input', { session_id, data });
|
|
586
|
-
|
|
587
|
-
// Custom retry options
|
|
588
|
-
client?.namedRPCNoWait('terminal.input', { session_id, data }, {
|
|
589
|
-
maxRetries: 5, // Max retry attempts (default: 3)
|
|
590
|
-
baseDelayMs: 100, // Base delay for exponential backoff (default: 100)
|
|
591
|
-
maxDelayMs: 3000, // Max delay cap (default: 2000)
|
|
592
|
-
});
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
**Retry sequence:** 100ms → 200ms → 400ms → 800ms → 1600ms (capped at maxDelayMs)
|
|
596
|
-
|
|
597
|
-
**Performance comparison:**
|
|
598
|
-
|
|
599
|
-
| Method | Latency | Use Case |
|
|
600
|
-
|--------|---------|----------|
|
|
601
|
-
| `namedRPC()` | ~800-1800ms | Commands that need response |
|
|
602
|
-
| `namedRPCNoWait()` | ~10-30ms | Real-time input, fire-and-forget |
|
|
603
|
-
|
|
604
|
-
### namedRPCWithRetry() - RPC with Timeout and Retry
|
|
605
|
-
|
|
606
|
-
For operations that need both timeout protection and automatic retry on transient failures.
|
|
607
|
-
|
|
608
|
-
```tsx
|
|
609
|
-
import { useCentrifugo } from '@djangocfg/centrifugo';
|
|
610
|
-
|
|
611
|
-
function FileList() {
|
|
612
|
-
const { client } = useCentrifugo();
|
|
613
|
-
|
|
614
|
-
const loadFiles = async () => {
|
|
615
|
-
// Automatically retries on timeout/network errors
|
|
616
|
-
const files = await client?.namedRPCWithRetry('files.list',
|
|
617
|
-
{ path: '/home' },
|
|
618
|
-
{
|
|
619
|
-
timeout: 5000, // 5 second timeout per attempt
|
|
620
|
-
maxRetries: 3, // Up to 3 retries
|
|
621
|
-
baseDelayMs: 1000, // Start with 1s delay
|
|
622
|
-
maxDelayMs: 10000, // Cap at 10s
|
|
623
|
-
onRetry: (attempt, error, delay) => {
|
|
624
|
-
console.log(`Retry ${attempt}: ${error.userMessage}, waiting ${delay}ms`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
);
|
|
628
|
-
return files;
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
### RPCError - Typed Error Handling
|
|
634
|
-
|
|
635
|
-
All RPC methods now throw `RPCError` with classification for better error handling:
|
|
636
|
-
|
|
637
|
-
```tsx
|
|
638
|
-
import { RPCError } from '@djangocfg/centrifugo';
|
|
639
|
-
|
|
640
|
-
try {
|
|
641
|
-
await client.namedRPC('files.list', { path: '/' });
|
|
642
|
-
} catch (error) {
|
|
643
|
-
if (error instanceof RPCError) {
|
|
644
|
-
// Error classification
|
|
645
|
-
console.log(error.code); // 'timeout' | 'network_error' | 'server_error' | ...
|
|
646
|
-
console.log(error.isRetryable); // true for transient errors
|
|
647
|
-
console.log(error.userMessage); // User-friendly message
|
|
648
|
-
console.log(error.suggestedRetryDelay); // Recommended delay in ms
|
|
649
|
-
|
|
650
|
-
// Show user-friendly message
|
|
651
|
-
toast.error(error.userMessage);
|
|
652
|
-
|
|
653
|
-
// Decide if retry makes sense
|
|
654
|
-
if (error.isRetryable) {
|
|
655
|
-
// Schedule retry
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
**Error Codes:**
|
|
662
|
-
|
|
663
|
-
| Code | Retryable | Description |
|
|
664
|
-
|------|-----------|-------------|
|
|
665
|
-
| `timeout` | ✅ | Request timed out |
|
|
666
|
-
| `network_error` | ✅ | Network connectivity issue |
|
|
667
|
-
| `connection_failed` | ✅ | WebSocket connection failed |
|
|
668
|
-
| `websocket_error` | ✅ | WebSocket protocol error |
|
|
669
|
-
| `server_error` | ✅ (5xx only) | Server returned error |
|
|
670
|
-
| `not_connected` | ❌ | Client not connected |
|
|
671
|
-
| `encoding_error` | ❌ | Failed to encode request |
|
|
672
|
-
| `decoding_error` | ❌ | Failed to decode response |
|
|
673
|
-
| `cancelled` | ❌ | Request was cancelled |
|
|
674
|
-
|
|
675
|
-
### withRetry() - Generic Retry Utility
|
|
676
|
-
|
|
677
|
-
For custom retry logic outside of RPC:
|
|
678
|
-
|
|
679
|
-
```tsx
|
|
680
|
-
import { withRetry, RPCError } from '@djangocfg/centrifugo';
|
|
681
|
-
|
|
682
|
-
const result = await withRetry(
|
|
683
|
-
() => fetchSomething(),
|
|
684
|
-
{
|
|
685
|
-
maxRetries: 3,
|
|
686
|
-
baseDelayMs: 1000,
|
|
687
|
-
maxDelayMs: 10000,
|
|
688
|
-
jitterFactor: 0.2, // ±20% randomization
|
|
689
|
-
},
|
|
690
|
-
(state, delay) => {
|
|
691
|
-
console.log(`Retry ${state.attempt}, waiting ${delay}ms`);
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
### checkApiVersion() - API Contract Validation
|
|
697
|
-
|
|
698
|
-
Validates that the client API version matches the server. Useful for detecting when the frontend needs to refresh after a backend deployment.
|
|
699
|
-
|
|
700
|
-
```tsx
|
|
701
|
-
import { API_VERSION } from '@/_ws'; // Generated client exports version hash
|
|
702
|
-
|
|
703
|
-
// Check version after connect
|
|
704
|
-
const result = await client.checkApiVersion(API_VERSION);
|
|
705
|
-
|
|
706
|
-
if (!result.compatible) {
|
|
707
|
-
// Versions don't match - show refresh prompt
|
|
708
|
-
toast.warning('New version available. Please refresh the page.');
|
|
709
|
-
}
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
**Unified Event Handling:**
|
|
713
|
-
|
|
714
|
-
Version mismatch automatically dispatches a `'centrifugo'` event:
|
|
715
|
-
|
|
716
|
-
```tsx
|
|
717
|
-
// Listen globally for version mismatch
|
|
718
|
-
window.addEventListener('centrifugo', (e: CustomEvent) => {
|
|
719
|
-
if (e.detail.type === 'version_mismatch') {
|
|
720
|
-
const { clientVersion, serverVersion, message } = e.detail.data;
|
|
721
|
-
toast.warning(message);
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
**How Version Hash Works:**
|
|
727
|
-
|
|
728
|
-
The version hash is computed from:
|
|
729
|
-
- All `@websocket_rpc` method signatures (name, params, return type)
|
|
730
|
-
- All Pydantic model schemas used in handlers
|
|
731
|
-
|
|
732
|
-
When any handler or model changes, the hash changes, triggering a version mismatch.
|
|
733
|
-
|
|
734
|
-
## Auto-Generated Type-Safe Clients
|
|
735
|
-
|
|
736
|
-
Django-CFG can auto-generate TypeScript clients from your `@websocket_rpc` handlers, providing full type safety and eliminating manual RPC calls.
|
|
737
|
-
|
|
738
|
-
### Generate Clients
|
|
739
|
-
|
|
740
|
-
```bash
|
|
741
|
-
# Generate TypeScript client from @websocket_rpc handlers
|
|
742
|
-
python manage.py generate_centrifugo_clients --typescript --output ./clients
|
|
743
|
-
|
|
744
|
-
# Generate and auto-copy to Next.js app (recommended)
|
|
745
|
-
python manage.py generate_centrifugo_clients --typescript
|
|
746
|
-
# Outputs to: openapi/centrifuge/typescript/
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
This generates type-safe clients with:
|
|
750
|
-
|
|
751
|
-
```
|
|
752
|
-
typescript/
|
|
753
|
-
├── client.ts # Type-safe API methods
|
|
754
|
-
├── types.ts # Pydantic models → TypeScript interfaces
|
|
755
|
-
├── index.ts # Exports
|
|
756
|
-
└── rpc-client.ts # Low-level RPC client (optional)
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
**Copy to your app:**
|
|
760
|
-
|
|
761
|
-
```bash
|
|
762
|
-
# Manual copy
|
|
763
|
-
cp -r openapi/centrifuge/typescript/* apps/web/app/_ws/
|
|
764
|
-
|
|
765
|
-
# Or create a custom command for your project
|
|
766
|
-
# See: core/management/commands/generate_centrifuge_web.py
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
### Using Generated Clients
|
|
770
|
-
|
|
771
|
-
**Before (manual RPC):**
|
|
772
|
-
```tsx
|
|
773
|
-
// ❌ No type safety, easy to make mistakes
|
|
774
|
-
const result = await client.namedRPC('ai_chat.get_messages', {
|
|
775
|
-
session_id: sessionId,
|
|
776
|
-
limit: 50,
|
|
777
|
-
before_id: beforeId, // Could typo: beforeID
|
|
778
|
-
});
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
**After (generated client):**
|
|
782
|
-
```tsx
|
|
783
|
-
import { APIClient, type MessageListResult } from '@/_ws';
|
|
784
|
-
|
|
785
|
-
const apiClient = useMemo(() =>
|
|
786
|
-
client ? new APIClient(client) : null,
|
|
787
|
-
[client]
|
|
788
|
-
);
|
|
789
|
-
|
|
790
|
-
// ✅ Full type safety with autocomplete
|
|
791
|
-
const result: MessageListResult = await apiClient.aiChatGetMessages({
|
|
792
|
-
session_id: sessionId, // ✓ Autocomplete
|
|
793
|
-
limit: 50, // ✓ Type checking
|
|
794
|
-
before_id: beforeId, // ✓ Typo protection
|
|
795
|
-
});
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
### How It Works
|
|
799
|
-
|
|
800
|
-
1. **Backend** - Define RPC handlers with Pydantic models:
|
|
801
|
-
|
|
802
|
-
```python
|
|
803
|
-
from pydantic import BaseModel
|
|
804
|
-
from django_cfg.apps.integrations.centrifugo.decorators import websocket_rpc
|
|
805
|
-
|
|
806
|
-
class GetMessagesParams(BaseModel):
|
|
807
|
-
session_id: str
|
|
808
|
-
limit: int = 50
|
|
809
|
-
before_id: str | None = None
|
|
810
|
-
|
|
811
|
-
class MessageListResult(BaseModel):
|
|
812
|
-
messages: list[MessageData]
|
|
813
|
-
total: int
|
|
814
|
-
has_more: bool
|
|
815
|
-
|
|
816
|
-
@websocket_rpc("ai_chat.get_messages")
|
|
817
|
-
async def ai_chat_get_messages(conn, params: GetMessagesParams) -> MessageListResult:
|
|
818
|
-
# Implementation
|
|
819
|
-
return MessageListResult(...)
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
2. **Generate** - Run generation command:
|
|
823
|
-
|
|
824
|
-
```bash
|
|
825
|
-
python manage.py generate_centrifuge_web
|
|
826
|
-
# Discovers all @websocket_rpc handlers
|
|
827
|
-
# Converts Pydantic models to TypeScript
|
|
828
|
-
# Generates type-safe APIClient
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
3. **Frontend** - Use generated client:
|
|
832
|
-
|
|
833
|
-
```tsx
|
|
834
|
-
const apiClient = new APIClient(client);
|
|
835
|
-
const result = await apiClient.aiChatGetMessages({
|
|
836
|
-
session_id: "uuid",
|
|
837
|
-
limit: 50,
|
|
838
|
-
});
|
|
839
|
-
// TypeScript knows result is MessageListResult!
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
### Benefits
|
|
843
|
-
|
|
844
|
-
- ✅ **Type Safety** - Compile-time errors for invalid parameters
|
|
845
|
-
- ✅ **Autocomplete** - IDE suggests available methods and parameters
|
|
846
|
-
- ✅ **Sync with Backend** - Regenerate when backend changes
|
|
847
|
-
- ✅ **No Manual Updates** - Types automatically match Pydantic models
|
|
848
|
-
- ✅ **Documentation** - Docstrings from backend appear in IDE
|
|
849
|
-
|
|
850
|
-
### Available Methods
|
|
851
|
-
|
|
852
|
-
All `@websocket_rpc` handlers are auto-generated:
|
|
853
|
-
|
|
854
|
-
```tsx
|
|
855
|
-
// Terminal RPC
|
|
856
|
-
await apiClient.terminalInput({ session_id, data });
|
|
857
|
-
await apiClient.terminalResize({ session_id, cols, rows });
|
|
858
|
-
await apiClient.terminalClose({ session_id });
|
|
859
|
-
|
|
860
|
-
// AI Chat RPC
|
|
861
|
-
await apiClient.aiChatCreateSession({ workspace_id, title });
|
|
862
|
-
await apiClient.aiChatSendMessage({ session_id, message, stream });
|
|
863
|
-
await apiClient.aiChatGetMessages({ session_id, limit, before_id });
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
> **Tip:** Run `python manage.py generate_centrifugo_clients --typescript` after adding new RPC handlers to update types.
|
|
867
|
-
|
|
868
|
-
## UI Components
|
|
869
|
-
|
|
870
|
-
All components are composable and can be used independently or together.
|
|
871
|
-
|
|
872
|
-
### ConnectionStatus
|
|
873
|
-
|
|
874
|
-
Display connection status in various formats.
|
|
875
|
-
|
|
876
|
-
**Variants:**
|
|
877
|
-
- `badge` - Compact badge with status indicator
|
|
878
|
-
- `inline` - Inline text with icon
|
|
879
|
-
- `detailed` - Detailed view with uptime and subscriptions
|
|
880
|
-
- `dot` - Minimal dot indicator with popover on click (ideal for toolbars)
|
|
881
|
-
|
|
882
|
-
```tsx
|
|
883
|
-
import { ConnectionStatus } from '@djangocfg/centrifugo';
|
|
884
|
-
|
|
885
|
-
// Badge variant
|
|
886
|
-
<ConnectionStatus variant="badge" />
|
|
887
|
-
|
|
888
|
-
// Dot variant - minimal indicator with popover (best for headers/toolbars)
|
|
889
|
-
<ConnectionStatus variant="dot" dotSize="md" />
|
|
890
|
-
|
|
891
|
-
// Detailed variant with uptime and subscriptions
|
|
892
|
-
<ConnectionStatus
|
|
893
|
-
variant="detailed"
|
|
894
|
-
showUptime={true}
|
|
895
|
-
showSubscriptions={true}
|
|
896
|
-
/>
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
**ConnectionStatusCard** - Card wrapper for dashboards:
|
|
900
|
-
|
|
901
|
-
```tsx
|
|
902
|
-
import { ConnectionStatusCard } from '@djangocfg/centrifugo';
|
|
903
|
-
|
|
904
|
-
<ConnectionStatusCard />
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
### MessagesFeed
|
|
908
|
-
|
|
909
|
-
Real-time feed of Centrifugo messages with filtering and export.
|
|
910
|
-
|
|
911
|
-
```tsx
|
|
912
|
-
import { MessagesFeed } from '@djangocfg/centrifugo';
|
|
913
|
-
|
|
914
|
-
<MessagesFeed
|
|
915
|
-
maxMessages={100}
|
|
916
|
-
showFilters={true}
|
|
917
|
-
showControls={true}
|
|
918
|
-
autoScroll={true}
|
|
919
|
-
onMessageClick={(msg) => console.log(msg)}
|
|
920
|
-
/>
|
|
921
|
-
```
|
|
922
|
-
|
|
923
|
-
**Features:**
|
|
924
|
-
- Filter by type, level, channel
|
|
925
|
-
- Search functionality
|
|
926
|
-
- Pause/play auto-scroll
|
|
927
|
-
- Download messages as JSON
|
|
928
|
-
- Clear messages
|
|
929
|
-
|
|
930
|
-
### SubscriptionsList
|
|
931
|
-
|
|
932
|
-
List of active subscriptions with management controls.
|
|
933
|
-
|
|
934
|
-
```tsx
|
|
935
|
-
import { SubscriptionsList } from '@djangocfg/centrifugo';
|
|
936
|
-
|
|
937
|
-
<SubscriptionsList
|
|
938
|
-
showControls={true}
|
|
939
|
-
onSubscriptionClick={(channel) => console.log(channel)}
|
|
940
|
-
/>
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
**Features:**
|
|
944
|
-
- Real-time subscription updates
|
|
945
|
-
- Unsubscribe from channels
|
|
946
|
-
- Click to view channel details
|
|
947
|
-
|
|
948
|
-
### CentrifugoMonitor
|
|
949
|
-
|
|
950
|
-
Main monitoring component with multiple variants.
|
|
951
|
-
|
|
952
|
-
**Basic Usage:**
|
|
953
|
-
|
|
954
|
-
```tsx
|
|
955
|
-
import { CentrifugoMonitor } from '@djangocfg/centrifugo';
|
|
956
|
-
|
|
957
|
-
// Embedded in a page
|
|
958
|
-
<CentrifugoMonitor defaultTab="messages" />
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
**Dialog Variant:**
|
|
962
|
-
|
|
963
|
-
```tsx
|
|
964
|
-
import { CentrifugoMonitorDialog } from '@djangocfg/centrifugo';
|
|
965
|
-
|
|
966
|
-
function MyComponent() {
|
|
967
|
-
const [open, setOpen] = useState(false);
|
|
968
|
-
|
|
969
|
-
return (
|
|
970
|
-
<>
|
|
971
|
-
<button onClick={() => setOpen(true)}>Open Monitor</button>
|
|
972
|
-
<CentrifugoMonitorDialog
|
|
973
|
-
open={open}
|
|
974
|
-
onOpenChange={setOpen}
|
|
975
|
-
defaultTab="status"
|
|
976
|
-
/>
|
|
977
|
-
</>
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
**FAB Variant:**
|
|
983
|
-
|
|
984
|
-
```tsx
|
|
985
|
-
import { CentrifugoMonitorFAB } from '@djangocfg/centrifugo';
|
|
986
|
-
|
|
987
|
-
// Full variant with all tabs
|
|
988
|
-
<CentrifugoMonitorFAB variant="full" />
|
|
989
|
-
|
|
990
|
-
// Compact variant (status only)
|
|
991
|
-
<CentrifugoMonitorFAB variant="compact" />
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
**Widget Variant (for Dashboards):**
|
|
995
|
-
|
|
996
|
-
```tsx
|
|
997
|
-
import { CentrifugoMonitorWidget } from '@djangocfg/centrifugo';
|
|
998
|
-
|
|
999
|
-
<CentrifugoMonitorWidget defaultTab="subscriptions" />
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
**Props:**
|
|
1003
|
-
```typescript
|
|
1004
|
-
interface CentrifugoMonitorProps {
|
|
1005
|
-
defaultTab?: 'status' | 'messages' | 'subscriptions';
|
|
1006
|
-
className?: string;
|
|
1007
|
-
}
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
|
-
## Logging System
|
|
1011
|
-
|
|
1012
|
-
The package includes a sophisticated logging system with:
|
|
1013
|
-
|
|
1014
|
-
- **Circular Buffer** - Stores up to 500 logs (configurable)
|
|
1015
|
-
- **Dual Output** - Consola (dev only) + in-memory store (always)
|
|
1016
|
-
- **Structured Logs** - Timestamp, level, source, message, and optional data
|
|
1017
|
-
- **Real-time Updates** - Subscribe to log changes for React updates
|
|
1018
|
-
- **Prefixed Output** - Logger name appears as `[YourComponent]` in console
|
|
1019
|
-
|
|
1020
|
-
**Creating a Logger:**
|
|
1021
|
-
|
|
1022
|
-
```typescript
|
|
1023
|
-
import { createLogger } from '@djangocfg/centrifugo';
|
|
1024
|
-
|
|
1025
|
-
// Simple usage with string prefix
|
|
1026
|
-
const logger = createLogger('MyComponent');
|
|
1027
|
-
|
|
1028
|
-
// Advanced usage with full config
|
|
1029
|
-
const logger = createLogger({
|
|
1030
|
-
source: 'client', // 'client' | 'provider' | 'subscription' | 'system'
|
|
1031
|
-
tag: 'MyComponent',
|
|
1032
|
-
isDevelopment: true,
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
logger.debug('Debug message', { extra: 'data' });
|
|
1036
|
-
logger.info('Info message');
|
|
1037
|
-
logger.success('Success message');
|
|
1038
|
-
logger.warning('Warning message');
|
|
1039
|
-
logger.error('Error message', error);
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
**Accessing Logs:**
|
|
1043
|
-
|
|
1044
|
-
```typescript
|
|
1045
|
-
import { getGlobalLogsStore } from '@djangocfg/centrifugo';
|
|
1046
|
-
|
|
1047
|
-
const logsStore = getGlobalLogsStore();
|
|
1048
|
-
|
|
1049
|
-
// Get all logs
|
|
1050
|
-
const logs = logsStore.getAll();
|
|
1051
|
-
|
|
1052
|
-
// Subscribe to changes
|
|
1053
|
-
const unsubscribe = logsStore.subscribe((logs) => {
|
|
1054
|
-
console.log('Logs updated:', logs);
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
// Clear logs
|
|
1058
|
-
logsStore.clear();
|
|
1059
|
-
```
|
|
1060
|
-
|
|
1061
|
-
**Using LogsProvider:**
|
|
1062
|
-
|
|
1063
|
-
```tsx
|
|
1064
|
-
import { useLogs } from '@djangocfg/centrifugo';
|
|
1065
|
-
|
|
1066
|
-
function CustomLogsView() {
|
|
1067
|
-
const { logs, setFilter, clearLogs } = useLogs();
|
|
1068
|
-
|
|
1069
|
-
return (
|
|
1070
|
-
<div>
|
|
1071
|
-
<button onClick={() => setFilter({ level: 'error' })}>
|
|
1072
|
-
Show Errors Only
|
|
1073
|
-
</button>
|
|
1074
|
-
<button onClick={() => setFilter({ source: 'client' })}>
|
|
1075
|
-
Show Client Logs
|
|
1076
|
-
</button>
|
|
1077
|
-
<button onClick={() => setFilter({ search: 'WebSocket' })}>
|
|
1078
|
-
Search "WebSocket"
|
|
1079
|
-
</button>
|
|
1080
|
-
<button onClick={clearLogs}>
|
|
1081
|
-
Clear All
|
|
1082
|
-
</button>
|
|
1083
|
-
|
|
1084
|
-
<ul>
|
|
1085
|
-
{logs.map(log => (
|
|
1086
|
-
<li key={log.id}>{log.message}</li>
|
|
1087
|
-
))}
|
|
1088
|
-
</ul>
|
|
1089
|
-
</div>
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
## Advanced Usage
|
|
1095
|
-
|
|
1096
|
-
### Using the Core Client (without React)
|
|
1097
|
-
|
|
1098
|
-
```typescript
|
|
1099
|
-
import { CentrifugoRPCClient, createLogger } from '@djangocfg/centrifugo';
|
|
1100
|
-
|
|
1101
|
-
const logger = createLogger('MyApp');
|
|
1102
|
-
|
|
1103
|
-
// Recommended: Options-based constructor
|
|
1104
|
-
const client = new CentrifugoRPCClient({
|
|
1105
|
-
url: 'ws://localhost:8000/ws',
|
|
1106
|
-
token: 'your-token',
|
|
1107
|
-
userId: 'user-id',
|
|
1108
|
-
timeout: 30000,
|
|
1109
|
-
logger,
|
|
1110
|
-
// Auto-refresh token on expiration
|
|
1111
|
-
getToken: async () => {
|
|
1112
|
-
const response = await fetch('/api/auth/refresh-token');
|
|
1113
|
-
const { token } = await response.json();
|
|
1114
|
-
return token;
|
|
1115
|
-
},
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
await client.connect();
|
|
1119
|
-
|
|
1120
|
-
// Check API version after connect
|
|
1121
|
-
import { API_VERSION } from '@/_ws';
|
|
1122
|
-
await client.checkApiVersion(API_VERSION);
|
|
1123
|
-
|
|
1124
|
-
// Subscribe to channel
|
|
1125
|
-
const unsubscribe = client.subscribe('channel-name', (data) => {
|
|
1126
|
-
console.log('Message:', data);
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
// Get native Centrifuge client for low-level access
|
|
1130
|
-
const centrifuge = client.getCentrifuge();
|
|
1131
|
-
centrifuge.on('connected', () => console.log('Connected'));
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
### Custom Dashboard Integration
|
|
1135
|
-
|
|
1136
|
-
```tsx
|
|
1137
|
-
import {
|
|
1138
|
-
ConnectionStatusCard,
|
|
1139
|
-
MessagesFeed,
|
|
1140
|
-
SubscriptionsList,
|
|
1141
|
-
} from '@djangocfg/centrifugo';
|
|
1142
|
-
|
|
1143
|
-
function Dashboard() {
|
|
1144
|
-
return (
|
|
1145
|
-
<div className="grid grid-cols-3 gap-4">
|
|
1146
|
-
<ConnectionStatusCard />
|
|
1147
|
-
|
|
1148
|
-
<div className="col-span-2">
|
|
1149
|
-
<MessagesFeed
|
|
1150
|
-
maxMessages={50}
|
|
1151
|
-
showFilters={false}
|
|
1152
|
-
/>
|
|
1153
|
-
</div>
|
|
1154
|
-
|
|
1155
|
-
<div className="col-span-3">
|
|
1156
|
-
<SubscriptionsList showControls={true} />
|
|
1157
|
-
</div>
|
|
1158
|
-
</div>
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### Embedded Monitor in Page
|
|
1164
|
-
|
|
1165
|
-
```tsx
|
|
1166
|
-
import { CentrifugoMonitor } from '@djangocfg/centrifugo';
|
|
1167
|
-
|
|
1168
|
-
function MonitoringPage() {
|
|
1169
|
-
return (
|
|
1170
|
-
<div className="container mx-auto p-6">
|
|
1171
|
-
<h1>WebSocket Monitoring</h1>
|
|
1172
|
-
<CentrifugoMonitor defaultTab="messages" />
|
|
1173
|
-
</div>
|
|
1174
|
-
);
|
|
1175
74
|
}
|
|
1176
75
|
```
|
|
1177
76
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
The package is fully typed with comprehensive TypeScript definitions:
|
|
1181
|
-
|
|
1182
|
-
```typescript
|
|
1183
|
-
import type {
|
|
1184
|
-
// Client Options
|
|
1185
|
-
CentrifugoClientOptions,
|
|
1186
|
-
RPCOptions,
|
|
1187
|
-
RetryOptions,
|
|
1188
|
-
VersionCheckResult,
|
|
1189
|
-
NamedRPCWithRetryOptions,
|
|
1190
|
-
|
|
1191
|
-
// Errors
|
|
1192
|
-
RPCErrorCode,
|
|
1193
|
-
RetryConfig,
|
|
1194
|
-
RetryState,
|
|
1195
|
-
|
|
1196
|
-
// Connection
|
|
1197
|
-
ConnectionState,
|
|
1198
|
-
CentrifugoToken,
|
|
1199
|
-
User,
|
|
1200
|
-
|
|
1201
|
-
// Logs
|
|
1202
|
-
LogLevel,
|
|
1203
|
-
LogEntry,
|
|
1204
|
-
|
|
1205
|
-
// Subscriptions
|
|
1206
|
-
ActiveSubscription,
|
|
1207
|
-
|
|
1208
|
-
// Client
|
|
1209
|
-
CentrifugoClientConfig,
|
|
1210
|
-
CentrifugoClientState,
|
|
1211
|
-
|
|
1212
|
-
// Provider
|
|
1213
|
-
CentrifugoProviderProps,
|
|
1214
|
-
CentrifugoContextValue,
|
|
1215
|
-
|
|
1216
|
-
// Hooks
|
|
1217
|
-
UseSubscriptionOptions,
|
|
1218
|
-
UseSubscriptionResult,
|
|
1219
|
-
UsePageVisibilityOptions,
|
|
1220
|
-
UsePageVisibilityResult,
|
|
1221
|
-
PageVisibilityState,
|
|
1222
|
-
|
|
1223
|
-
// Components
|
|
1224
|
-
ConnectionStatusProps,
|
|
1225
|
-
MessagesFeedProps,
|
|
1226
|
-
SubscriptionsListProps,
|
|
1227
|
-
CentrifugoMonitorProps,
|
|
1228
|
-
} from '@djangocfg/centrifugo';
|
|
1229
|
-
|
|
1230
|
-
// Error classes
|
|
1231
|
-
import { RPCError, withRetry, createRetryHandler } from '@djangocfg/centrifugo';
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
## Unified Event System
|
|
1235
|
-
|
|
1236
|
-
All Centrifugo events use a single `'centrifugo'` CustomEvent with a type discriminator. This simplifies event handling and reduces the number of event listeners needed.
|
|
1237
|
-
|
|
1238
|
-
**Event Types:**
|
|
1239
|
-
|
|
1240
|
-
| Type | Description | Data |
|
|
1241
|
-
|------|-------------|------|
|
|
1242
|
-
| `error` | RPC call failed | `{ method, error, code, data }` |
|
|
1243
|
-
| `version_mismatch` | API version mismatch | `{ clientVersion, serverVersion, message }` |
|
|
1244
|
-
| `connected` | Successfully connected | `{ userId }` |
|
|
1245
|
-
| `disconnected` | Connection lost | `{ userId, reason }` |
|
|
1246
|
-
| `reconnecting` | Attempting to reconnect | `{ userId, attempt, reason }` |
|
|
1247
|
-
|
|
1248
|
-
**Listening to Events:**
|
|
77
|
+
### 5. Add monitoring (optional)
|
|
1249
78
|
|
|
1250
|
-
```tsx
|
|
1251
|
-
// Listen to all Centrifugo events
|
|
1252
|
-
window.addEventListener('centrifugo', (e: CustomEvent) => {
|
|
1253
|
-
const { type, data, timestamp } = e.detail;
|
|
1254
|
-
|
|
1255
|
-
switch (type) {
|
|
1256
|
-
case 'error':
|
|
1257
|
-
console.error('RPC error:', data.method, data.error);
|
|
1258
|
-
break;
|
|
1259
|
-
case 'version_mismatch':
|
|
1260
|
-
toast.warning('Please refresh the page');
|
|
1261
|
-
break;
|
|
1262
|
-
case 'connected':
|
|
1263
|
-
console.log('Connected as', data.userId);
|
|
1264
|
-
break;
|
|
1265
|
-
case 'disconnected':
|
|
1266
|
-
console.log('Disconnected:', data.reason);
|
|
1267
|
-
break;
|
|
1268
|
-
case 'reconnecting':
|
|
1269
|
-
console.log('Reconnecting, attempt', data.attempt);
|
|
1270
|
-
break;
|
|
1271
|
-
}
|
|
1272
|
-
});
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
**Dispatching Events (for custom integrations):**
|
|
1276
|
-
|
|
1277
|
-
```tsx
|
|
1278
|
-
import {
|
|
1279
|
-
dispatchCentrifugoError,
|
|
1280
|
-
dispatchVersionMismatch,
|
|
1281
|
-
dispatchConnected,
|
|
1282
|
-
dispatchDisconnected,
|
|
1283
|
-
dispatchReconnecting,
|
|
1284
|
-
} from '@djangocfg/centrifugo';
|
|
1285
|
-
|
|
1286
|
-
// Dispatch error
|
|
1287
|
-
dispatchCentrifugoError({
|
|
1288
|
-
method: 'terminal.input',
|
|
1289
|
-
error: 'Connection timeout',
|
|
1290
|
-
code: 408,
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
// Dispatch version mismatch
|
|
1294
|
-
dispatchVersionMismatch({
|
|
1295
|
-
clientVersion: 'abc123',
|
|
1296
|
-
serverVersion: 'def456',
|
|
1297
|
-
message: 'API version mismatch',
|
|
1298
|
-
});
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
**Integration with ErrorsTracker:**
|
|
1302
|
-
|
|
1303
|
-
The `@djangocfg/layouts` package's `ErrorTrackingProvider` automatically listens to `'centrifugo'` events with `type: 'error'` and displays toast notifications.
|
|
1304
|
-
|
|
1305
|
-
```tsx
|
|
1306
|
-
import { ErrorTrackingProvider } from '@djangocfg/layouts';
|
|
1307
|
-
|
|
1308
|
-
<ErrorTrackingProvider centrifugo={{ enabled: true, showToast: true }}>
|
|
1309
|
-
<App />
|
|
1310
|
-
</ErrorTrackingProvider>
|
|
1311
|
-
```
|
|
1312
|
-
|
|
1313
|
-
**Proper Centrifuge Types:**
|
|
1314
|
-
|
|
1315
|
-
The package now uses proper types from the `centrifuge` library:
|
|
1316
|
-
|
|
1317
|
-
```typescript
|
|
1318
|
-
import type { Subscription, SubscriptionState } from 'centrifuge';
|
|
1319
|
-
|
|
1320
|
-
// No more 'as any' - fully type-safe!
|
|
1321
|
-
const centrifuge = client.getCentrifuge();
|
|
1322
|
-
const subs = centrifuge.subscriptions();
|
|
1323
|
-
|
|
1324
|
-
for (const [channel, sub] of Object.entries(subs)) {
|
|
1325
|
-
console.log(channel, sub.state); // SubscriptionState type
|
|
1326
|
-
}
|
|
1327
|
-
```
|
|
1328
|
-
|
|
1329
|
-
## Best Practices
|
|
1330
|
-
|
|
1331
|
-
### 1. Logger Usage
|
|
1332
|
-
|
|
1333
|
-
Always use the logger instead of `console`:
|
|
1334
|
-
|
|
1335
|
-
```typescript
|
|
1336
|
-
// ❌ Bad
|
|
1337
|
-
console.log('Message');
|
|
1338
|
-
|
|
1339
|
-
// ✅ Good
|
|
1340
|
-
const logger = createLogger('MyComponent');
|
|
1341
|
-
logger.info('Message');
|
|
1342
|
-
```
|
|
1343
|
-
|
|
1344
|
-
### 2. Date Handling
|
|
1345
|
-
|
|
1346
|
-
Use `moment` with UTC for all date operations:
|
|
1347
|
-
|
|
1348
|
-
```typescript
|
|
1349
|
-
import moment from 'moment';
|
|
1350
|
-
|
|
1351
|
-
// ❌ Bad
|
|
1352
|
-
const now = new Date();
|
|
1353
|
-
const timestamp = Date.now();
|
|
1354
|
-
|
|
1355
|
-
// ✅ Good
|
|
1356
|
-
const now = moment.utc();
|
|
1357
|
-
const timestamp = moment.utc().valueOf();
|
|
1358
|
-
const formatted = moment.utc().format('YYYY-MM-DD HH:mm:ss');
|
|
1359
|
-
```
|
|
1360
|
-
|
|
1361
|
-
### 3. FAB Control
|
|
1362
|
-
|
|
1363
|
-
Don't rely on auto-showing FAB. Control it explicitly:
|
|
1364
|
-
|
|
1365
|
-
```typescript
|
|
1366
|
-
// ✅ Good - Developer has full control
|
|
1367
|
-
function App() {
|
|
1368
|
-
return (
|
|
1369
|
-
<CentrifugoProvider enabled={true}>
|
|
1370
|
-
<YourApp />
|
|
1371
|
-
{shouldShowMonitor && <CentrifugoMonitorFAB />}
|
|
1372
|
-
</CentrifugoProvider>
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
```
|
|
1376
|
-
|
|
1377
|
-
### 4. Component Composition
|
|
1378
|
-
|
|
1379
|
-
Use individual components for custom layouts:
|
|
1380
|
-
|
|
1381
|
-
```typescript
|
|
1382
|
-
// ✅ Good - Compose as needed
|
|
1383
|
-
<div className="monitoring-panel">
|
|
1384
|
-
<ConnectionStatus variant="detailed" showUptime />
|
|
1385
|
-
<MessagesFeed maxMessages={50} />
|
|
1386
|
-
<SubscriptionsList />
|
|
1387
|
-
</div>
|
|
1388
|
-
```
|
|
1389
|
-
|
|
1390
|
-
## Migration Guide
|
|
1391
|
-
|
|
1392
|
-
### From Old DebugPanel
|
|
1393
|
-
|
|
1394
|
-
**Before:**
|
|
1395
|
-
```tsx
|
|
1396
|
-
import { DebugPanel } from '@djangocfg/centrifugo';
|
|
1397
|
-
|
|
1398
|
-
// Auto-shown in development
|
|
1399
|
-
<CentrifugoProvider>
|
|
1400
|
-
<App />
|
|
1401
|
-
</CentrifugoProvider>
|
|
1402
|
-
```
|
|
1403
|
-
|
|
1404
|
-
**After:**
|
|
1405
79
|
```tsx
|
|
1406
80
|
import { CentrifugoMonitorFAB } from '@djangocfg/centrifugo';
|
|
1407
81
|
|
|
1408
|
-
//
|
|
1409
|
-
<
|
|
1410
|
-
<App />
|
|
1411
|
-
<CentrifugoMonitorFAB variant="full" />
|
|
1412
|
-
</CentrifugoProvider>
|
|
82
|
+
// Show only for admins or in dev
|
|
83
|
+
{shouldShowMonitor && <CentrifugoMonitorFAB variant="full" />}
|
|
1413
84
|
```
|
|
1414
85
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
**Before:**
|
|
1418
|
-
```typescript
|
|
1419
|
-
import { format } from 'date-fns';
|
|
86
|
+
## Documentation
|
|
1420
87
|
|
|
1421
|
-
|
|
1422
|
-
|
|
88
|
+
| Document | Description |
|
|
89
|
+
|----------|-------------|
|
|
90
|
+
| [@docs/overview.md](@docs/overview.md) | Features, architecture, how it connects |
|
|
91
|
+
| [@docs/api-reference.md](@docs/api-reference.md) | Providers, hooks, core client, TypeScript types |
|
|
92
|
+
| [@docs/rpc.md](@docs/rpc.md) | useNamedRPC, useRPC, namedRPCNoWait, RPCError, retry |
|
|
93
|
+
| [@docs/components.md](@docs/components.md) | ConnectionStatus, MessagesFeed, Monitor, dashboard examples |
|
|
94
|
+
| [@docs/codegen.md](@docs/codegen.md) | Auto-generated type-safe clients from @websocket_rpc |
|
|
95
|
+
| [@docs/events.md](@docs/events.md) | Unified event system, ErrorsTracker integration |
|
|
96
|
+
| [@docs/logging.md](@docs/logging.md) | Logger, LogsProvider, in-memory store |
|
|
97
|
+
| [@docs/migration.md](@docs/migration.md) | Migration from DebugPanel, date-fns to moment |
|
|
1423
98
|
|
|
1424
|
-
|
|
1425
|
-
```typescript
|
|
1426
|
-
import moment from 'moment';
|
|
99
|
+
## Requirements
|
|
1427
100
|
|
|
1428
|
-
|
|
1429
|
-
|
|
101
|
+
- React 18+
|
|
102
|
+
- `@djangocfg/ui-nextjs` — UI components (shadcn/ui)
|
|
103
|
+
- `@djangocfg/layouts` — layout components
|
|
104
|
+
- `centrifuge` — WebSocket client library
|
|
105
|
+
- `moment` — date manipulation
|
|
106
|
+
- `consola` — console logging
|
|
1430
107
|
|
|
1431
108
|
## Development
|
|
1432
109
|
|
|
1433
110
|
```bash
|
|
1434
|
-
# Install dependencies
|
|
1435
|
-
pnpm
|
|
1436
|
-
|
|
1437
|
-
#
|
|
1438
|
-
pnpm build
|
|
1439
|
-
|
|
1440
|
-
# Type check
|
|
1441
|
-
pnpm check
|
|
1442
|
-
|
|
1443
|
-
# Run in development mode
|
|
1444
|
-
pnpm dev
|
|
111
|
+
pnpm install # Install dependencies
|
|
112
|
+
pnpm build # Build
|
|
113
|
+
pnpm check # Type check
|
|
114
|
+
pnpm dev # Watch mode
|
|
1445
115
|
```
|
|
1446
116
|
|
|
1447
|
-
## Requirements
|
|
1448
|
-
|
|
1449
|
-
- React 18+
|
|
1450
|
-
- `@djangocfg/ui-nextjs` - UI components (shadcn/ui based)
|
|
1451
|
-
- `@djangocfg/layouts` - Layout components
|
|
1452
|
-
- `centrifuge` - WebSocket client library
|
|
1453
|
-
- `moment` - Date manipulation library
|
|
1454
|
-
- `consola` - Beautiful console logging
|
|
1455
|
-
|
|
1456
|
-
## Documentation
|
|
1457
|
-
|
|
1458
|
-
Full documentation available at [djangocfg.com](https://djangocfg.com)
|
|
1459
|
-
|
|
1460
|
-
## Contributing
|
|
1461
|
-
|
|
1462
|
-
Issues and pull requests are welcome at [GitHub](https://github.com/markolofsen/django-cfg)
|
|
1463
|
-
|
|
1464
117
|
## License
|
|
1465
118
|
|
|
1466
|
-
MIT
|
|
119
|
+
MIT
|