@djangocfg/centrifugo 2.1.26 → 2.1.28
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 +135 -1
- package/package.json +5 -5
- package/src/components/ConnectionStatus/ConnectionStatus.tsx +2 -2
- package/src/components/SubscriptionsList/SubscriptionsList.tsx +2 -2
- package/src/core/client/CentrifugoRPCClient.ts +73 -8
- package/src/core/client/index.ts +1 -0
- package/src/core/logger/consolaLogger.ts +37 -0
- package/src/core/utils/devTips.ts +74 -0
- package/src/hooks/useCodegenTip.ts +39 -0
- package/src/hooks/useNamedRPC.ts +2 -2
- package/src/hooks/useRPC.ts +3 -3
- package/src/hooks/useSubscription.ts +2 -2
- package/src/index.ts +3 -0
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +29 -5
package/README.md
CHANGED
|
@@ -441,7 +441,141 @@ interface UseNamedRPCResult {
|
|
|
441
441
|
}
|
|
442
442
|
```
|
|
443
443
|
|
|
444
|
-
> **Note:** `useNamedRPC` uses native Centrifugo RPC which requires RPC proxy to be configured in Centrifugo server. See the [Setup Guide](https://djangocfg.com/features/integrations/centrifugo/
|
|
444
|
+
> **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.
|
|
445
|
+
|
|
446
|
+
## Auto-Generated Type-Safe Clients
|
|
447
|
+
|
|
448
|
+
Django-CFG can auto-generate TypeScript clients from your `@websocket_rpc` handlers, providing full type safety and eliminating manual RPC calls.
|
|
449
|
+
|
|
450
|
+
### Generate Clients
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# Generate TypeScript client from @websocket_rpc handlers
|
|
454
|
+
python manage.py generate_centrifugo_clients --typescript --output ./clients
|
|
455
|
+
|
|
456
|
+
# Generate and auto-copy to Next.js app (recommended)
|
|
457
|
+
python manage.py generate_centrifugo_clients --typescript
|
|
458
|
+
# Outputs to: openapi/centrifuge/typescript/
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
This generates type-safe clients with:
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
typescript/
|
|
465
|
+
├── client.ts # Type-safe API methods
|
|
466
|
+
├── types.ts # Pydantic models → TypeScript interfaces
|
|
467
|
+
├── index.ts # Exports
|
|
468
|
+
└── rpc-client.ts # Low-level RPC client (optional)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Copy to your app:**
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Manual copy
|
|
475
|
+
cp -r openapi/centrifuge/typescript/* apps/web/app/_ws/
|
|
476
|
+
|
|
477
|
+
# Or create a custom command for your project
|
|
478
|
+
# See: core/management/commands/generate_centrifuge_web.py
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Using Generated Clients
|
|
482
|
+
|
|
483
|
+
**Before (manual RPC):**
|
|
484
|
+
```tsx
|
|
485
|
+
// ❌ No type safety, easy to make mistakes
|
|
486
|
+
const result = await client.namedRPC('ai_chat.get_messages', {
|
|
487
|
+
session_id: sessionId,
|
|
488
|
+
limit: 50,
|
|
489
|
+
before_id: beforeId, // Could typo: beforeID
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**After (generated client):**
|
|
494
|
+
```tsx
|
|
495
|
+
import { APIClient, type MessageListResult } from '@/_ws';
|
|
496
|
+
|
|
497
|
+
const apiClient = useMemo(() =>
|
|
498
|
+
client ? new APIClient(client) : null,
|
|
499
|
+
[client]
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// ✅ Full type safety with autocomplete
|
|
503
|
+
const result: MessageListResult = await apiClient.aiChatGetMessages({
|
|
504
|
+
session_id: sessionId, // ✓ Autocomplete
|
|
505
|
+
limit: 50, // ✓ Type checking
|
|
506
|
+
before_id: beforeId, // ✓ Typo protection
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### How It Works
|
|
511
|
+
|
|
512
|
+
1. **Backend** - Define RPC handlers with Pydantic models:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from pydantic import BaseModel
|
|
516
|
+
from django_cfg.apps.integrations.centrifugo.decorators import websocket_rpc
|
|
517
|
+
|
|
518
|
+
class GetMessagesParams(BaseModel):
|
|
519
|
+
session_id: str
|
|
520
|
+
limit: int = 50
|
|
521
|
+
before_id: str | None = None
|
|
522
|
+
|
|
523
|
+
class MessageListResult(BaseModel):
|
|
524
|
+
messages: list[MessageData]
|
|
525
|
+
total: int
|
|
526
|
+
has_more: bool
|
|
527
|
+
|
|
528
|
+
@websocket_rpc("ai_chat.get_messages")
|
|
529
|
+
async def ai_chat_get_messages(conn, params: GetMessagesParams) -> MessageListResult:
|
|
530
|
+
# Implementation
|
|
531
|
+
return MessageListResult(...)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
2. **Generate** - Run generation command:
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
python manage.py generate_centrifuge_web
|
|
538
|
+
# Discovers all @websocket_rpc handlers
|
|
539
|
+
# Converts Pydantic models to TypeScript
|
|
540
|
+
# Generates type-safe APIClient
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
3. **Frontend** - Use generated client:
|
|
544
|
+
|
|
545
|
+
```tsx
|
|
546
|
+
const apiClient = new APIClient(client);
|
|
547
|
+
const result = await apiClient.aiChatGetMessages({
|
|
548
|
+
session_id: "uuid",
|
|
549
|
+
limit: 50,
|
|
550
|
+
});
|
|
551
|
+
// TypeScript knows result is MessageListResult!
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Benefits
|
|
555
|
+
|
|
556
|
+
- ✅ **Type Safety** - Compile-time errors for invalid parameters
|
|
557
|
+
- ✅ **Autocomplete** - IDE suggests available methods and parameters
|
|
558
|
+
- ✅ **Sync with Backend** - Regenerate when backend changes
|
|
559
|
+
- ✅ **No Manual Updates** - Types automatically match Pydantic models
|
|
560
|
+
- ✅ **Documentation** - Docstrings from backend appear in IDE
|
|
561
|
+
|
|
562
|
+
### Available Methods
|
|
563
|
+
|
|
564
|
+
All `@websocket_rpc` handlers are auto-generated:
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
// Terminal RPC
|
|
568
|
+
await apiClient.terminalInput({ session_id, data });
|
|
569
|
+
await apiClient.terminalResize({ session_id, cols, rows });
|
|
570
|
+
await apiClient.terminalClose({ session_id });
|
|
571
|
+
|
|
572
|
+
// AI Chat RPC
|
|
573
|
+
await apiClient.aiChatCreateSession({ workspace_id, title });
|
|
574
|
+
await apiClient.aiChatSendMessage({ session_id, message, stream });
|
|
575
|
+
await apiClient.aiChatGetMessages({ session_id, limit, before_id });
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
> **Tip:** Run `python manage.py generate_centrifugo_clients --typescript` after adding new RPC handlers to update types.
|
|
445
579
|
|
|
446
580
|
## UI Components
|
|
447
581
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/centrifugo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.28",
|
|
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,9 +51,9 @@
|
|
|
51
51
|
"centrifuge": "^5.2.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@djangocfg/api": "^2.1.
|
|
55
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
56
|
-
"@djangocfg/layouts": "^2.1.
|
|
54
|
+
"@djangocfg/api": "^2.1.28",
|
|
55
|
+
"@djangocfg/ui-nextjs": "^2.1.28",
|
|
56
|
+
"@djangocfg/layouts": "^2.1.28",
|
|
57
57
|
"consola": "^3.4.2",
|
|
58
58
|
"lucide-react": "^0.545.0",
|
|
59
59
|
"moment": "^2.30.1",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"react-dom": "^19.1.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
64
|
+
"@djangocfg/typescript-config": "^2.1.28",
|
|
65
65
|
"@types/react": "^19.1.0",
|
|
66
66
|
"@types/react-dom": "^19.1.0",
|
|
67
67
|
"moment": "^2.30.1",
|
|
@@ -12,9 +12,9 @@ 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';
|
|
15
|
-
import {
|
|
15
|
+
import { getConsolaLogger } from '../../core/logger/consolaLogger';
|
|
16
16
|
|
|
17
|
-
const logger =
|
|
17
|
+
const logger = getConsolaLogger('ConnectionStatus');
|
|
18
18
|
|
|
19
19
|
// ─────────────────────────────────────────────────────────────────────────
|
|
20
20
|
// Types
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
import { Radio, RefreshCw, Trash2 } from 'lucide-react';
|
|
20
20
|
import { Subscription, SubscriptionState } from 'centrifuge';
|
|
21
21
|
import { useCentrifugo } from '../../providers/CentrifugoProvider';
|
|
22
|
-
import {
|
|
22
|
+
import { getConsolaLogger } from '../../core/logger/consolaLogger';
|
|
23
23
|
|
|
24
|
-
const logger =
|
|
24
|
+
const logger = getConsolaLogger('SubscriptionsList');
|
|
25
25
|
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Types
|
|
@@ -7,7 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
import { Centrifuge } from 'centrifuge';
|
|
9
9
|
import type { Logger } from '../logger';
|
|
10
|
-
import {
|
|
10
|
+
import { getConsolaLogger } from '../logger/consolaLogger';
|
|
11
|
+
|
|
12
|
+
export interface CentrifugoClientOptions {
|
|
13
|
+
url: string;
|
|
14
|
+
token: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
/**
|
|
19
|
+
* Callback to get fresh token when current token expires.
|
|
20
|
+
* Centrifuge-js calls this automatically on token expiration.
|
|
21
|
+
*/
|
|
22
|
+
getToken?: () => Promise<string>;
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export class CentrifugoRPCClient {
|
|
13
26
|
private centrifuge: Centrifuge;
|
|
@@ -19,21 +32,73 @@ export class CentrifugoRPCClient {
|
|
|
19
32
|
private readonly timeout: number;
|
|
20
33
|
private readonly logger: Logger;
|
|
21
34
|
|
|
35
|
+
constructor(options: CentrifugoClientOptions);
|
|
36
|
+
/** @deprecated Use options object instead */
|
|
22
37
|
constructor(
|
|
23
38
|
url: string,
|
|
24
39
|
token: string,
|
|
25
40
|
userId: string,
|
|
41
|
+
timeout?: number,
|
|
42
|
+
logger?: Logger
|
|
43
|
+
);
|
|
44
|
+
constructor(
|
|
45
|
+
urlOrOptions: string | CentrifugoClientOptions,
|
|
46
|
+
token?: string,
|
|
47
|
+
userId?: string,
|
|
26
48
|
timeout: number = 30000,
|
|
27
49
|
logger?: Logger
|
|
28
50
|
) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
// Handle both old and new API
|
|
52
|
+
let url: string;
|
|
53
|
+
let actualToken: string;
|
|
54
|
+
let actualUserId: string;
|
|
55
|
+
let actualTimeout: number;
|
|
56
|
+
let actualLogger: Logger | undefined;
|
|
57
|
+
let getToken: (() => Promise<string>) | undefined;
|
|
58
|
+
|
|
59
|
+
if (typeof urlOrOptions === 'object') {
|
|
60
|
+
// New options-based API
|
|
61
|
+
url = urlOrOptions.url;
|
|
62
|
+
actualToken = urlOrOptions.token;
|
|
63
|
+
actualUserId = urlOrOptions.userId;
|
|
64
|
+
actualTimeout = urlOrOptions.timeout ?? 30000;
|
|
65
|
+
actualLogger = urlOrOptions.logger;
|
|
66
|
+
getToken = urlOrOptions.getToken;
|
|
67
|
+
} else {
|
|
68
|
+
// Legacy positional arguments API
|
|
69
|
+
url = urlOrOptions;
|
|
70
|
+
actualToken = token!;
|
|
71
|
+
actualUserId = userId!;
|
|
72
|
+
actualTimeout = timeout;
|
|
73
|
+
actualLogger = logger;
|
|
74
|
+
}
|
|
33
75
|
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
76
|
+
this.userId = actualUserId;
|
|
77
|
+
this.replyChannel = `user#${actualUserId}`;
|
|
78
|
+
this.timeout = actualTimeout;
|
|
79
|
+
this.logger = actualLogger || getConsolaLogger('client');
|
|
80
|
+
|
|
81
|
+
// Build Centrifuge options
|
|
82
|
+
const centrifugeOptions: any = {
|
|
83
|
+
token: actualToken,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Add getToken callback for automatic token refresh
|
|
87
|
+
if (getToken) {
|
|
88
|
+
centrifugeOptions.getToken = async () => {
|
|
89
|
+
this.logger.info('Token expired, refreshing...');
|
|
90
|
+
try {
|
|
91
|
+
const newToken = await getToken();
|
|
92
|
+
this.logger.success('Token refreshed successfully');
|
|
93
|
+
return newToken;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.logger.error('Failed to refresh token', error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.centrifuge = new Centrifuge(url, centrifugeOptions);
|
|
37
102
|
|
|
38
103
|
this.centrifuge.on('disconnected', (ctx) => {
|
|
39
104
|
// Reject all pending requests
|
package/src/core/client/index.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Consola Logger
|
|
3
|
+
*
|
|
4
|
+
* Single consola instance used across the package for development logging.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createConsola } from 'consola';
|
|
8
|
+
import type { Logger } from './createLogger';
|
|
9
|
+
|
|
10
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared consola logger for Centrifugo package
|
|
14
|
+
*/
|
|
15
|
+
export const consolaLogger = createConsola({
|
|
16
|
+
level: isDevelopment ? 4 : 3,
|
|
17
|
+
formatOptions: {
|
|
18
|
+
colors: true,
|
|
19
|
+
date: false,
|
|
20
|
+
compact: !isDevelopment,
|
|
21
|
+
},
|
|
22
|
+
}).withTag('[Centrifugo]');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get a consola logger with custom tag wrapped to match Logger interface
|
|
26
|
+
*/
|
|
27
|
+
export function getConsolaLogger(tag: string): Logger {
|
|
28
|
+
const consola = consolaLogger.withTag(`[${tag}]`);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
debug: (message: string, data?: unknown) => consola.debug(message, data || ''),
|
|
32
|
+
info: (message: string, data?: unknown) => consola.info(message, data || ''),
|
|
33
|
+
success: (message: string, data?: unknown) => consola.success(message, data || ''),
|
|
34
|
+
warning: (message: string, data?: unknown) => consola.warn(message, data || ''),
|
|
35
|
+
error: (message: string, error?: Error | unknown) => consola.error(message, error || ''),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development Tips
|
|
3
|
+
*
|
|
4
|
+
* Shows helpful development tips once per session.
|
|
5
|
+
* Used across the package to educate developers about features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getConsolaLogger } from '../logger/consolaLogger';
|
|
9
|
+
|
|
10
|
+
const logger = getConsolaLogger('DevTips');
|
|
11
|
+
const shownTips = new Set<string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Show a development tip once per session
|
|
15
|
+
*
|
|
16
|
+
* @param id - Unique identifier for the tip
|
|
17
|
+
* @param message - Tip message
|
|
18
|
+
* @param details - Optional details (shown as second parameter)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* showDevTip('codegen', 'Generate type-safe clients:', 'python manage.py ...');
|
|
22
|
+
*/
|
|
23
|
+
export function showDevTip(id: string, message: string, details?: string): void {
|
|
24
|
+
if (process.env.NODE_ENV !== 'development') return;
|
|
25
|
+
if (shownTips.has(id)) return;
|
|
26
|
+
|
|
27
|
+
shownTips.add(id);
|
|
28
|
+
logger.info(message, details);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Predefined tips
|
|
33
|
+
*/
|
|
34
|
+
export const DevTips = {
|
|
35
|
+
/**
|
|
36
|
+
* Show tip about generating type-safe clients
|
|
37
|
+
*/
|
|
38
|
+
codegenClients: () => {
|
|
39
|
+
showDevTip(
|
|
40
|
+
'codegen-clients',
|
|
41
|
+
'💡 Generate type-safe clients from @websocket_rpc handlers:',
|
|
42
|
+
'\n\npython manage.py generate_centrifugo_clients --typescript\n\nLearn more: https://djangocfg.com/centrifugo/codegen'
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Show tip about using generated client instead of manual RPC
|
|
48
|
+
*/
|
|
49
|
+
useGeneratedClient: () => {
|
|
50
|
+
showDevTip(
|
|
51
|
+
'use-generated-client',
|
|
52
|
+
'💡 Consider using auto-generated APIClient for type safety:',
|
|
53
|
+
'\n\nimport { APIClient } from \'@/_ws\';\nconst apiClient = new APIClient(client);\nawait apiClient.aiChatGetMessages({ ... });\n\nGenerate: python manage.py generate_centrifugo_clients --typescript'
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Show tip about RPC proxy configuration
|
|
59
|
+
*/
|
|
60
|
+
rpcProxyConfig: () => {
|
|
61
|
+
showDevTip(
|
|
62
|
+
'rpc-proxy-config',
|
|
63
|
+
'💡 Using namedRPC requires Centrifugo RPC proxy configuration:',
|
|
64
|
+
'\n\nSee: https://djangocfg.com/features/integrations/centrifugo/setup'
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reset shown tips (useful for testing)
|
|
71
|
+
*/
|
|
72
|
+
export function resetDevTips(): void {
|
|
73
|
+
shownTips.clear();
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCodegenTip Hook
|
|
3
|
+
*
|
|
4
|
+
* Shows development tip about client generation once per session.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useRef } from 'react';
|
|
8
|
+
import { consolaLogger } from '../core/logger/consolaLogger';
|
|
9
|
+
|
|
10
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
11
|
+
let tipShown = false; // Global flag to show tip once across all components
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Show dev tip about generating type-safe clients from @websocket_rpc handlers
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* function MyComponent() {
|
|
18
|
+
* useCodegenTip();
|
|
19
|
+
* // ... rest of component
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export function useCodegenTip() {
|
|
23
|
+
const mountedRef = useRef(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!isDevelopment) return;
|
|
27
|
+
if (tipShown) return;
|
|
28
|
+
if (mountedRef.current) return;
|
|
29
|
+
|
|
30
|
+
mountedRef.current = true;
|
|
31
|
+
tipShown = true;
|
|
32
|
+
|
|
33
|
+
consolaLogger.info(
|
|
34
|
+
'💡 Generate type-safe clients from @websocket_rpc handlers:\n\n' +
|
|
35
|
+
'python manage.py generate_centrifugo_clients --typescript\n\n' +
|
|
36
|
+
'Learn more: https://djangocfg.com/docs/features/integrations/centrifugo/client-generation/'
|
|
37
|
+
);
|
|
38
|
+
}, []);
|
|
39
|
+
}
|
package/src/hooks/useNamedRPC.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { useState, useCallback, useRef } from 'react';
|
|
29
29
|
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
30
|
-
import {
|
|
30
|
+
import { getConsolaLogger } from '../core/logger/consolaLogger';
|
|
31
31
|
|
|
32
32
|
export interface UseNamedRPCOptions {
|
|
33
33
|
onError?: (error: Error) => void;
|
|
@@ -50,7 +50,7 @@ export function useNamedRPC(
|
|
|
50
50
|
const [isLoading, setIsLoading] = useState(false);
|
|
51
51
|
const [error, setError] = useState<Error | null>(null);
|
|
52
52
|
|
|
53
|
-
const logger = useRef(
|
|
53
|
+
const logger = useRef(getConsolaLogger('useNamedRPC')).current;
|
|
54
54
|
|
|
55
55
|
const reset = useCallback(() => {
|
|
56
56
|
setIsLoading(false);
|
package/src/hooks/useRPC.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { useState, useCallback, useRef } from 'react';
|
|
24
24
|
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
25
|
-
import {
|
|
25
|
+
import { getConsolaLogger } from '../core/logger/consolaLogger';
|
|
26
26
|
|
|
27
27
|
export interface UseRPCOptions {
|
|
28
28
|
timeout?: number;
|
|
@@ -45,8 +45,8 @@ export function useRPC(defaultOptions: UseRPCOptions = {}): UseRPCResult {
|
|
|
45
45
|
const { client, isConnected } = useCentrifugo();
|
|
46
46
|
const [isLoading, setIsLoading] = useState(false);
|
|
47
47
|
const [error, setError] = useState<Error | null>(null);
|
|
48
|
-
|
|
49
|
-
const logger = useRef(
|
|
48
|
+
|
|
49
|
+
const logger = useRef(getConsolaLogger('useRPC')).current;
|
|
50
50
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
51
51
|
|
|
52
52
|
const reset = useCallback(() => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useCallback, useRef, useState } from 'react';
|
|
16
16
|
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
17
|
-
import {
|
|
17
|
+
import { getConsolaLogger } from '../core/logger/consolaLogger';
|
|
18
18
|
import { logChannelWarnings } from '../core/utils/channelValidator';
|
|
19
19
|
|
|
20
20
|
export interface UseSubscriptionOptions<T = any> {
|
|
@@ -42,7 +42,7 @@ export function useSubscription<T = any>(
|
|
|
42
42
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
43
43
|
|
|
44
44
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
45
|
-
const logger = useRef(
|
|
45
|
+
const logger = useRef(getConsolaLogger('useSubscription')).current;
|
|
46
46
|
|
|
47
47
|
// Store callbacks in refs to avoid re-subscriptions when they change
|
|
48
48
|
const onPublicationRef = useRef(onPublication);
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type { Subscription } from 'centrifuge';
|
|
|
53
53
|
|
|
54
54
|
export { CentrifugoRPCClient } from './core/client/CentrifugoRPCClient';
|
|
55
55
|
export { createLogger, getGlobalLogsStore } from './core/logger';
|
|
56
|
+
export { consolaLogger, getConsolaLogger } from './core/logger/consolaLogger';
|
|
56
57
|
export type {
|
|
57
58
|
// Connection
|
|
58
59
|
ConnectionState,
|
|
@@ -92,6 +93,8 @@ export type {
|
|
|
92
93
|
UseSubscriptionResult,
|
|
93
94
|
} from './hooks/useSubscription';
|
|
94
95
|
|
|
96
|
+
export { useCodegenTip } from './hooks/useCodegenTip';
|
|
97
|
+
|
|
95
98
|
// ─────────────────────────────────────────────────────────────────────────
|
|
96
99
|
// Configuration
|
|
97
100
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from 'react';
|
|
11
11
|
import { useAuth } from '@djangocfg/api/auth';
|
|
12
12
|
import { CentrifugoRPCClient } from '../../core/client';
|
|
13
|
-
import {
|
|
13
|
+
import { getConsolaLogger } from '../../core/logger/consolaLogger';
|
|
14
14
|
import type { ConnectionState, CentrifugoToken, ActiveSubscription } from '../../core/types';
|
|
15
15
|
import { LogsProvider } from '../LogsProvider';
|
|
16
16
|
import { isStaticBuild, isDevelopment, reconnectConfig } from '../../config';
|
|
17
17
|
import { CentrifugoMonitorDialog } from '../../components/CentrifugoMonitor/CentrifugoMonitorDialog';
|
|
18
|
+
import { useCodegenTip } from '../../hooks/useCodegenTip';
|
|
18
19
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────
|
|
20
21
|
// Context
|
|
@@ -56,6 +57,18 @@ export interface CentrifugoProviderProps {
|
|
|
56
57
|
enabled?: boolean;
|
|
57
58
|
url?: string;
|
|
58
59
|
autoConnect?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Callback to refresh the Centrifugo token when it expires.
|
|
62
|
+
* If provided, centrifuge-js will automatically call this when token expires.
|
|
63
|
+
* Should return a fresh JWT token string.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* onTokenRefresh={async () => {
|
|
67
|
+
* const response = await getCentrifugoAuthTokenRetrieve();
|
|
68
|
+
* return response.token;
|
|
69
|
+
* }}
|
|
70
|
+
*/
|
|
71
|
+
onTokenRefresh?: () => Promise<string>;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -67,6 +80,7 @@ function CentrifugoProviderInner({
|
|
|
67
80
|
enabled = false,
|
|
68
81
|
url,
|
|
69
82
|
autoConnect: autoConnectProp = true,
|
|
83
|
+
onTokenRefresh,
|
|
70
84
|
}: CentrifugoProviderProps) {
|
|
71
85
|
const { isAuthenticated, isLoading, user } = useAuth();
|
|
72
86
|
|
|
@@ -79,15 +93,18 @@ function CentrifugoProviderInner({
|
|
|
79
93
|
const [subscriptions, setSubscriptions] = useState<string[]>([]);
|
|
80
94
|
const [activeSubscriptions, setActiveSubscriptions] = useState<ActiveSubscription[]>([]);
|
|
81
95
|
|
|
82
|
-
const logger = useMemo(() =>
|
|
96
|
+
const logger = useMemo(() => getConsolaLogger('provider'), []);
|
|
97
|
+
|
|
98
|
+
// Show dev tip about client generation
|
|
99
|
+
useCodegenTip();
|
|
83
100
|
|
|
84
101
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
85
102
|
const hasConnectedRef = useRef(false);
|
|
86
103
|
const isConnectingRef = useRef(false);
|
|
87
104
|
const isMountedRef = useRef(true);
|
|
88
105
|
const reconnectAttemptRef = useRef(0);
|
|
89
|
-
const devWarningShownRef = useRef(false);
|
|
90
106
|
const reconnectStoppedRef = useRef(false); // Track if we should stop reconnecting
|
|
107
|
+
const devWarningShownRef = useRef(false); // Track if server unavailable warning was shown
|
|
91
108
|
const connectRef = useRef<(() => Promise<void>) | null>(null);
|
|
92
109
|
const disconnectRef = useRef<(() => void) | null>(null);
|
|
93
110
|
|
|
@@ -215,7 +232,14 @@ function CentrifugoProviderInner({
|
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
|
|
218
|
-
const rpcClient = new CentrifugoRPCClient(
|
|
235
|
+
const rpcClient = new CentrifugoRPCClient({
|
|
236
|
+
url: wsUrl,
|
|
237
|
+
token,
|
|
238
|
+
userId,
|
|
239
|
+
timeout: 30000,
|
|
240
|
+
logger,
|
|
241
|
+
getToken: onTokenRefresh,
|
|
242
|
+
});
|
|
219
243
|
await rpcClient.connect();
|
|
220
244
|
|
|
221
245
|
if (!isMountedRef.current) {
|
|
@@ -304,7 +328,7 @@ function CentrifugoProviderInner({
|
|
|
304
328
|
} finally {
|
|
305
329
|
setIsConnecting(false);
|
|
306
330
|
}
|
|
307
|
-
}, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected, getReconnectDelay]);
|
|
331
|
+
}, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected, getReconnectDelay, onTokenRefresh]);
|
|
308
332
|
|
|
309
333
|
// Disconnect function
|
|
310
334
|
const disconnect = useCallback(() => {
|