@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 CHANGED
@@ -335,46 +335,114 @@ interface UseRPCResult {
335
335
  }
336
336
  ```
337
337
 
338
- **Backend Implementation:**
338
+ ### useNamedRPC() - Native Centrifugo RPC
339
339
 
340
- The backend should subscribe to `rpc#{method}` channels and publish responses:
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 example
344
- from django_cfg.apps.integrations.centrifugo import get_centrifugo_publisher
345
-
346
- publisher = get_centrifugo_publisher()
347
-
348
- # Listen to RPC requests (via Centrifugo subscription)
349
- async def handle_rpc_request(data):
350
- correlation_id = data['correlation_id']
351
- method = data['method']
352
- params = data['params']
353
- reply_to = data['reply_to']
354
-
355
- try:
356
- # Process request
357
- result = await process_task_stats(params['bot_id'])
358
-
359
- # Send response
360
- await publisher.publish(
361
- channel=reply_to,
362
- data={
363
- 'correlation_id': correlation_id,
364
- 'result': result,
365
- }
366
- )
367
- except Exception as e:
368
- # Send error response
369
- await publisher.publish(
370
- channel=reply_to,
371
- data={
372
- 'correlation_id': correlation_id,
373
- 'error': {'message': str(e)},
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",
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.3",
55
- "@djangocfg/layouts": "^2.1.3",
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.3",
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
  }
@@ -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
+ }