@djangocfg/centrifugo 2.1.3 → 2.1.4
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 +103 -35
- package/package.json +4 -4
- package/src/core/client/CentrifugoRPCClient.ts +48 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useNamedRPC.ts +118 -0
package/README.md
CHANGED
|
@@ -335,46 +335,114 @@ interface UseRPCResult {
|
|
|
335
335
|
}
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
### useNamedRPC() - Native Centrifugo RPC
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
Hook for making **native Centrifugo RPC calls** via RPC proxy.
|
|
341
|
+
|
|
342
|
+
**This is the recommended approach** for request-response patterns. It uses Centrifugo's built-in RPC mechanism which proxies requests to Django.
|
|
343
|
+
|
|
344
|
+
**Flow:**
|
|
345
|
+
```
|
|
346
|
+
1. Client calls namedRPC('terminal.input', data)
|
|
347
|
+
2. Centrifuge.js sends RPC over WebSocket
|
|
348
|
+
3. Centrifugo proxies to Django: POST /centrifugo/rpc/
|
|
349
|
+
4. Django routes to @websocket_rpc handler
|
|
350
|
+
5. Response returned to client
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Basic Usage:**
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { useNamedRPC } from '@djangocfg/centrifugo';
|
|
357
|
+
|
|
358
|
+
function TerminalInput() {
|
|
359
|
+
const { call, isLoading, error } = useNamedRPC();
|
|
360
|
+
|
|
361
|
+
const handleSendInput = async (input: string) => {
|
|
362
|
+
try {
|
|
363
|
+
const result = await call('terminal.input', {
|
|
364
|
+
session_id: 'abc-123',
|
|
365
|
+
data: btoa(input) // Base64 encode
|
|
366
|
+
});
|
|
367
|
+
console.log('Result:', result);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('RPC failed:', error);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<button onClick={() => handleSendInput('ls -la')} disabled={isLoading}>
|
|
375
|
+
{isLoading ? 'Sending...' : 'Send Command'}
|
|
376
|
+
</button>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**With Type Safety:**
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
interface TerminalInputRequest {
|
|
385
|
+
session_id: string;
|
|
386
|
+
data: string; // Base64 encoded
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
interface TerminalInputResponse {
|
|
390
|
+
success: boolean;
|
|
391
|
+
message?: string;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { call } = useNamedRPC();
|
|
395
|
+
|
|
396
|
+
const result = await call<TerminalInputRequest, TerminalInputResponse>(
|
|
397
|
+
'terminal.input',
|
|
398
|
+
{ session_id: 'abc-123', data: btoa('ls -la') }
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
console.log(result.success); // Type-safe!
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Backend Handler:**
|
|
341
405
|
|
|
342
406
|
```python
|
|
343
|
-
# Django backend
|
|
344
|
-
from
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
407
|
+
# Django backend with @websocket_rpc decorator
|
|
408
|
+
from pydantic import BaseModel, Field
|
|
409
|
+
from django_cfg.apps.integrations.centrifugo.decorators import websocket_rpc
|
|
410
|
+
|
|
411
|
+
class TerminalInputParams(BaseModel):
|
|
412
|
+
session_id: str = Field(..., description="Session UUID")
|
|
413
|
+
data: str = Field(..., description="Base64 encoded input")
|
|
414
|
+
|
|
415
|
+
class SuccessResult(BaseModel):
|
|
416
|
+
success: bool
|
|
417
|
+
message: str = ""
|
|
418
|
+
|
|
419
|
+
@websocket_rpc("terminal.input")
|
|
420
|
+
async def terminal_input(conn, params: TerminalInputParams) -> SuccessResult:
|
|
421
|
+
"""Handle terminal input from browser."""
|
|
422
|
+
# Forward to gRPC/Electron terminal
|
|
423
|
+
await forward_to_terminal(params.session_id, base64.b64decode(params.data))
|
|
424
|
+
return SuccessResult(success=True, message="Input sent")
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Options:**
|
|
428
|
+
```typescript
|
|
429
|
+
interface UseNamedRPCOptions {
|
|
430
|
+
onError?: (error: Error) => void;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface UseNamedRPCResult {
|
|
434
|
+
call: <TRequest, TResponse>(
|
|
435
|
+
method: string,
|
|
436
|
+
data: TRequest
|
|
437
|
+
) => Promise<TResponse>;
|
|
438
|
+
isLoading: boolean;
|
|
439
|
+
error: Error | null;
|
|
440
|
+
reset: () => void;
|
|
441
|
+
}
|
|
376
442
|
```
|
|
377
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/setup) for configuration details.
|
|
445
|
+
|
|
378
446
|
## UI Components
|
|
379
447
|
|
|
380
448
|
All components are composable and can be used independently or together.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/centrifugo",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
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,8 +51,8 @@
|
|
|
51
51
|
"centrifuge": "^5.2.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
55
|
-
"@djangocfg/layouts": "^2.1.
|
|
54
|
+
"@djangocfg/ui-nextjs": "^2.1.4",
|
|
55
|
+
"@djangocfg/layouts": "^2.1.4",
|
|
56
56
|
"consola": "^3.4.2",
|
|
57
57
|
"lucide-react": "^0.545.0",
|
|
58
58
|
"moment": "^2.30.1",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"react-dom": "^19.1.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
63
|
+
"@djangocfg/typescript-config": "^2.1.4",
|
|
64
64
|
"@types/react": "^19.1.0",
|
|
65
65
|
"@types/react-dom": "^19.1.0",
|
|
66
66
|
"moment": "^2.30.1",
|
|
@@ -475,4 +475,52 @@ export class CentrifugoRPCClient {
|
|
|
475
475
|
getCentrifuge(): Centrifuge {
|
|
476
476
|
return this.centrifuge;
|
|
477
477
|
}
|
|
478
|
+
|
|
479
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
480
|
+
// Native Centrifugo RPC (via RPC Proxy)
|
|
481
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Call RPC method via native Centrifugo RPC proxy.
|
|
485
|
+
*
|
|
486
|
+
* This uses Centrifugo's built-in RPC mechanism which proxies
|
|
487
|
+
* requests to the backend (Django) via HTTP.
|
|
488
|
+
*
|
|
489
|
+
* Flow:
|
|
490
|
+
* 1. Client calls namedRPC('terminal.input', data)
|
|
491
|
+
* 2. Centrifuge.js sends RPC over WebSocket
|
|
492
|
+
* 3. Centrifugo proxies to Django: POST /centrifugo/rpc/
|
|
493
|
+
* 4. Django routes to @websocket_rpc handler
|
|
494
|
+
* 5. Response returned to client
|
|
495
|
+
*
|
|
496
|
+
* @param method - RPC method name (e.g., 'terminal.input')
|
|
497
|
+
* @param data - Request data
|
|
498
|
+
* @returns Promise that resolves with response data
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* const result = await client.namedRPC('terminal.input', {
|
|
502
|
+
* session_id: 'abc-123',
|
|
503
|
+
* data: btoa('ls -la')
|
|
504
|
+
* });
|
|
505
|
+
* console.log('Result:', result); // { success: true }
|
|
506
|
+
*/
|
|
507
|
+
async namedRPC<TRequest = any, TResponse = any>(
|
|
508
|
+
method: string,
|
|
509
|
+
data: TRequest
|
|
510
|
+
): Promise<TResponse> {
|
|
511
|
+
this.logger.info(`Native RPC: ${method}`, { data });
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const result = await this.centrifuge.rpc(method, data);
|
|
515
|
+
|
|
516
|
+
this.logger.success(`Native RPC success: ${method}`, {
|
|
517
|
+
hasData: !!result.data
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return result.data as TResponse;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
this.logger.error(`Native RPC failed: ${method}`, error);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
478
526
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -7,3 +7,6 @@ export type { UseSubscriptionOptions, UseSubscriptionResult } from './useSubscri
|
|
|
7
7
|
|
|
8
8
|
export { useRPC } from './useRPC';
|
|
9
9
|
export type { UseRPCOptions, UseRPCResult } from './useRPC';
|
|
10
|
+
|
|
11
|
+
export { useNamedRPC } from './useNamedRPC';
|
|
12
|
+
export type { UseNamedRPCOptions, UseNamedRPCResult } from './useNamedRPC';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNamedRPC Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for making native Centrifugo RPC calls via RPC proxy.
|
|
5
|
+
* Uses Centrifugo's built-in RPC mechanism which proxies to Django.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Client calls namedRPC('terminal.input', data)
|
|
9
|
+
* 2. Centrifuge.js sends RPC over WebSocket
|
|
10
|
+
* 3. Centrifugo proxies to Django: POST /centrifugo/rpc/
|
|
11
|
+
* 4. Django routes to @websocket_rpc handler
|
|
12
|
+
* 5. Response returned to client
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { call, isLoading, error } = useNamedRPC();
|
|
16
|
+
*
|
|
17
|
+
* const handleSendInput = async () => {
|
|
18
|
+
* const result = await call('terminal.input', {
|
|
19
|
+
* session_id: 'abc-123',
|
|
20
|
+
* data: btoa('ls -la')
|
|
21
|
+
* });
|
|
22
|
+
* console.log('Result:', result);
|
|
23
|
+
* };
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use client';
|
|
27
|
+
|
|
28
|
+
import { useState, useCallback, useRef } from 'react';
|
|
29
|
+
import { useCentrifugo } from '../providers/CentrifugoProvider';
|
|
30
|
+
import { createLogger } from '../core/logger';
|
|
31
|
+
|
|
32
|
+
export interface UseNamedRPCOptions {
|
|
33
|
+
onError?: (error: Error) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseNamedRPCResult {
|
|
37
|
+
call: <TRequest = any, TResponse = any>(
|
|
38
|
+
method: string,
|
|
39
|
+
data: TRequest
|
|
40
|
+
) => Promise<TResponse>;
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
error: Error | null;
|
|
43
|
+
reset: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useNamedRPC(
|
|
47
|
+
defaultOptions: UseNamedRPCOptions = {}
|
|
48
|
+
): UseNamedRPCResult {
|
|
49
|
+
const { client, isConnected } = useCentrifugo();
|
|
50
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
51
|
+
const [error, setError] = useState<Error | null>(null);
|
|
52
|
+
|
|
53
|
+
const logger = useRef(createLogger('useNamedRPC')).current;
|
|
54
|
+
|
|
55
|
+
const reset = useCallback(() => {
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
setError(null);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const call = useCallback(
|
|
61
|
+
async <TRequest = any, TResponse = any>(
|
|
62
|
+
method: string,
|
|
63
|
+
data: TRequest
|
|
64
|
+
): Promise<TResponse> => {
|
|
65
|
+
if (!client) {
|
|
66
|
+
const error = new Error('Centrifugo client not available');
|
|
67
|
+
setError(error);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isConnected) {
|
|
72
|
+
const error = new Error('Not connected to Centrifugo');
|
|
73
|
+
setError(error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reset previous state
|
|
78
|
+
setError(null);
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
logger.info(`Native RPC call: ${method}`, { data });
|
|
83
|
+
|
|
84
|
+
const result = await client.namedRPC<TRequest, TResponse>(method, data);
|
|
85
|
+
|
|
86
|
+
logger.success(`Native RPC success: ${method}`);
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
return result;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const rpcError =
|
|
91
|
+
err instanceof Error ? err : new Error('Native RPC call failed');
|
|
92
|
+
|
|
93
|
+
setError(rpcError);
|
|
94
|
+
logger.error(`Native RPC failed: ${method}`, rpcError);
|
|
95
|
+
|
|
96
|
+
// Call error callback if provided
|
|
97
|
+
if (defaultOptions.onError) {
|
|
98
|
+
try {
|
|
99
|
+
defaultOptions.onError(rpcError);
|
|
100
|
+
} catch (callbackError) {
|
|
101
|
+
logger.error('Error in onError callback', callbackError);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setIsLoading(false);
|
|
106
|
+
throw rpcError;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[client, isConnected, defaultOptions, logger]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
call,
|
|
114
|
+
isLoading,
|
|
115
|
+
error,
|
|
116
|
+
reset,
|
|
117
|
+
};
|
|
118
|
+
}
|