@djangocfg/centrifugo 2.1.54 → 2.1.55

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.
@@ -1,38 +1,62 @@
1
1
  /**
2
- * Base Centrifugo RPC Client
2
+ * Centrifugo RPC Client
3
3
  *
4
- * Handles WebSocket connection and RPC call correlation.
4
+ * Facade for WebSocket connection and RPC call handling.
5
5
  * Provides both RPC (request/response) and Subscription (pub/sub) patterns.
6
+ *
7
+ * Architecture:
8
+ * - connection.ts - Connection lifecycle management
9
+ * - subscriptions.ts - Channel subscription management
10
+ * - rpc.ts - RPC methods (namedRPC, namedRPCNoWait)
11
+ * - version.ts - API version checking
12
+ * - types.ts - Type definitions
6
13
  */
7
14
 
8
- import { Centrifuge } from 'centrifuge';
9
-
10
- import { getConsolaLogger } from '../logger/consolaLogger';
11
- import { dispatchCentrifugoError } from '../../events';
15
+ import type { Centrifuge, Subscription } from 'centrifuge';
12
16
 
13
17
  import type { Logger } from '../logger';
14
- export interface CentrifugoClientOptions {
15
- url: string;
16
- token: string;
17
- userId: string;
18
- timeout?: number;
19
- logger?: Logger;
20
- /**
21
- * Callback to get fresh token when current token expires.
22
- * Centrifuge-js calls this automatically on token expiration.
23
- */
24
- getToken?: () => Promise<string>;
25
- }
18
+ import type {
19
+ CentrifugoClientOptions,
20
+ PendingRequest,
21
+ RPCOptions,
22
+ RetryOptions,
23
+ VersionCheckResult,
24
+ } from './types';
25
+
26
+ import {
27
+ createConnectionState,
28
+ setupConnectionHandlers,
29
+ connect as connectToServer,
30
+ disconnect as disconnectFromServer,
31
+ type ConnectionState,
32
+ } from './connection';
33
+
34
+ import {
35
+ subscribe as subscribeToChannel,
36
+ unsubscribe as unsubscribeFromChannel,
37
+ unsubscribeAll as unsubscribeFromAllChannels,
38
+ getActiveSubscriptions as getClientSubscriptions,
39
+ getServerSideSubscriptions as getServerSubscriptions,
40
+ getAllSubscriptions as getCombinedSubscriptions,
41
+ type SubscriptionManager,
42
+ } from './subscriptions';
43
+
44
+ import {
45
+ handleResponse,
46
+ call as legacyCall,
47
+ rpc as legacyRpc,
48
+ namedRPC as nativeNamedRPC,
49
+ namedRPCNoWait as nativeNamedRPCNoWait,
50
+ type RPCManager,
51
+ } from './rpc';
52
+
53
+ import { checkApiVersion as checkVersion } from './version';
54
+
55
+ // Re-export types for backwards compatibility
56
+ export type { CentrifugoClientOptions } from './types';
26
57
 
27
58
  export class CentrifugoRPCClient {
28
- private centrifuge: Centrifuge;
29
- private subscription: any;
30
- private channelSubscriptions: Map<string, any> = new Map();
31
- private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
32
- private readonly userId: string;
33
- private readonly replyChannel: string;
34
- private readonly timeout: number;
35
- private readonly logger: Logger;
59
+ private state: ConnectionState;
36
60
 
37
61
  constructor(options: CentrifugoClientOptions);
38
62
  /** @deprecated Use options object instead */
@@ -50,588 +74,148 @@ export class CentrifugoRPCClient {
50
74
  timeout: number = 30000,
51
75
  logger?: Logger
52
76
  ) {
53
- // Handle both old and new API
54
- let url: string;
55
- let actualToken: string;
56
- let actualUserId: string;
57
- let actualTimeout: number;
58
- let actualLogger: Logger | undefined;
59
- let getToken: (() => Promise<string>) | undefined;
60
-
61
- if (typeof urlOrOptions === 'object') {
62
- // New options-based API
63
- url = urlOrOptions.url;
64
- actualToken = urlOrOptions.token;
65
- actualUserId = urlOrOptions.userId;
66
- actualTimeout = urlOrOptions.timeout ?? 30000;
67
- actualLogger = urlOrOptions.logger;
68
- getToken = urlOrOptions.getToken;
69
- } else {
70
- // Legacy positional arguments API
71
- url = urlOrOptions;
72
- actualToken = token!;
73
- actualUserId = userId!;
74
- actualTimeout = timeout;
75
- actualLogger = logger;
76
- }
77
-
78
- this.userId = actualUserId;
79
- this.replyChannel = `user#${actualUserId}`;
80
- this.timeout = actualTimeout;
81
- this.logger = actualLogger || getConsolaLogger('client');
82
-
83
- // Build Centrifuge options
84
- const centrifugeOptions: any = {
85
- token: actualToken,
86
- };
87
-
88
- // Add getToken callback for automatic token refresh
89
- if (getToken) {
90
- centrifugeOptions.getToken = async () => {
91
- this.logger.info('Token expired, refreshing...');
92
- try {
93
- const newToken = await getToken();
94
- this.logger.success('Token refreshed successfully');
95
- return newToken;
96
- } catch (error) {
97
- this.logger.error('Failed to refresh token', error);
98
- throw error;
99
- }
100
- };
101
- }
102
-
103
- this.centrifuge = new Centrifuge(url, centrifugeOptions);
104
-
105
- this.centrifuge.on('disconnected', (ctx) => {
106
- // Reject all pending requests
107
- this.pendingRequests.forEach(({ reject }) => {
108
- reject(new Error('Disconnected from Centrifugo'));
109
- });
110
- this.pendingRequests.clear();
111
- });
77
+ this.state = createConnectionState(urlOrOptions, token, userId, timeout, logger);
78
+ setupConnectionHandlers(this.state, (data) => this.handleResponse(data));
112
79
  }
113
80
 
81
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+ // Connection API
83
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
+
114
85
  async connect(): Promise<void> {
115
- return new Promise((resolve, reject) => {
116
- let resolved = false;
117
-
118
- // Listen to Centrifuge connection events
119
- const onConnected = () => {
120
- if (!resolved) {
121
- resolved = true;
122
- resolve();
123
- }
124
- };
125
-
126
- const onError = (ctx: any) => {
127
- if (!resolved) {
128
- resolved = true;
129
- reject(new Error(ctx.message || 'Connection error'));
130
- }
131
- };
132
-
133
- this.centrifuge.on('connected', onConnected);
134
- this.centrifuge.on('error', onError);
135
-
136
- // Start connection
137
- this.centrifuge.connect();
138
-
139
- // Subscribe to reply channel
140
- this.subscription = this.centrifuge.newSubscription(this.replyChannel);
141
-
142
- this.subscription.on('publication', (ctx: any) => {
143
- this.handleResponse(ctx.data);
144
- });
145
-
146
- this.subscription.on('subscribed', () => {
147
- // Subscription successful (optional, we already resolved on 'connected')
148
- });
149
-
150
- this.subscription.on('error', (ctx: any) => {
151
- // Error code 105 = "already subscribed" (server-side subscription from JWT)
152
- // This is not an error - the channel is already active via server-side subscription
153
- if (ctx.error?.code === 105) {
154
- // This is fine, server-side subscription exists
155
- } else {
156
- this.logger.error(`Subscription error for ${this.replyChannel}:`, ctx.error);
157
- }
158
- });
159
-
160
- this.subscription.subscribe();
161
- });
86
+ return connectToServer(this.state, (data) => this.handleResponse(data));
162
87
  }
163
88
 
164
89
  async disconnect(): Promise<void> {
165
- // Unsubscribe from all event channels
166
- this.unsubscribeAll();
90
+ disconnectFromServer(this.state);
91
+ }
167
92
 
168
- // Unsubscribe from RPC reply channel
169
- if (this.subscription) {
170
- this.subscription.unsubscribe();
171
- }
93
+ getCentrifuge(): Centrifuge {
94
+ return this.state.centrifuge;
95
+ }
96
+
97
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98
+ // Subscription API
99
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
172
100
 
173
- this.centrifuge.disconnect();
101
+ subscribe(channel: string, callback: (data: any) => void): () => void {
102
+ return subscribeToChannel(this.subscriptionManager, channel, callback);
174
103
  }
175
104
 
176
- async call<T = any>(method: string, params: any): Promise<T> {
177
- const correlationId = this.generateCorrelationId();
105
+ unsubscribe(channel: string): void {
106
+ unsubscribeFromChannel(this.subscriptionManager, channel);
107
+ }
178
108
 
179
- const message = {
180
- method,
181
- params,
182
- correlation_id: correlationId,
183
- reply_to: this.replyChannel,
184
- };
109
+ unsubscribeAll(): void {
110
+ unsubscribeFromAllChannels(this.subscriptionManager);
111
+ }
185
112
 
186
- // Create promise for response
187
- const promise = new Promise<T>((resolve, reject) => {
188
- const timeoutId = setTimeout(() => {
189
- this.pendingRequests.delete(correlationId);
190
- reject(new Error(`RPC timeout: ${method}`));
191
- }, this.timeout);
192
-
193
- this.pendingRequests.set(correlationId, {
194
- resolve: (result: T) => {
195
- clearTimeout(timeoutId);
196
- resolve(result);
197
- },
198
- reject: (error: Error) => {
199
- clearTimeout(timeoutId);
200
- reject(error);
201
- },
202
- });
203
- });
204
-
205
- // Publish request
206
- await this.centrifuge.publish('rpc.requests', message);
207
-
208
- return promise;
113
+ getActiveSubscriptions(): string[] {
114
+ return getClientSubscriptions(this.subscriptionManager);
209
115
  }
210
116
 
211
- private handleResponse(data: any): void {
212
- const correlationId = data.correlation_id;
213
- if (!correlationId) {
214
- return;
215
- }
216
-
217
- const pending = this.pendingRequests.get(correlationId);
218
- if (!pending) {
219
- return;
220
- }
221
-
222
- this.pendingRequests.delete(correlationId);
223
-
224
- if (data.error) {
225
- pending.reject(new Error(data.error.message || 'RPC error'));
226
- } else {
227
- pending.resolve(data.result);
228
- }
117
+ getServerSideSubscriptions(): string[] {
118
+ return getServerSubscriptions(this.subscriptionManager);
229
119
  }
230
120
 
231
- private generateCorrelationId(): string {
232
- return `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
121
+ getAllSubscriptions(): string[] {
122
+ return getCombinedSubscriptions(this.subscriptionManager);
233
123
  }
234
124
 
235
125
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
236
- // RPC Pattern API (Request-Response via Correlation ID)
126
+ // RPC Pattern API (Legacy - Correlation ID)
237
127
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
238
128
 
239
- /**
240
- * Send RPC request and wait for response.
241
- *
242
- * Uses correlation ID pattern to match request with response:
243
- * 1. Generate unique correlation_id
244
- * 2. Subscribe to personal reply channel (user#{userId})
245
- * 3. Publish request with correlation_id
246
- * 4. Wait for response with matching correlation_id
247
- * 5. Return response or timeout
248
- *
249
- * @param method - RPC method name (e.g., 'tasks.get_stats')
250
- * @param params - Request parameters
251
- * @param options - RPC options
252
- * @returns Promise that resolves with response data
253
- *
254
- * @example
255
- * const result = await client.rpc('tasks.get_stats', { bot_id: '123' });
256
- * console.log('Stats:', result);
257
- *
258
- * @example
259
- * // With custom timeout and reply channel
260
- * const result = await client.rpc('bot.command',
261
- * { command: 'start' },
262
- * { timeout: 30000, replyChannel: 'bot#123#replies' }
263
- * );
264
- */
129
+ async call<T = any>(method: string, params: any): Promise<T> {
130
+ return legacyCall(this.rpcManager, method, params, this.state.replyChannel);
131
+ }
132
+
265
133
  async rpc<TRequest = any, TResponse = any>(
266
134
  method: string,
267
135
  params: TRequest,
268
- options: {
269
- timeout?: number;
270
- replyChannel?: string;
271
- } = {}
136
+ options: RPCOptions = {}
272
137
  ): Promise<TResponse> {
273
- const {
274
- timeout = 10000,
275
- replyChannel = `user#${this.userId}`,
276
- } = options;
277
-
278
- const correlationId = this.generateCorrelationId();
279
-
280
- this.logger.info(`RPC request: ${method}`, { correlationId, params });
281
-
282
- return new Promise<TResponse>((resolve, reject) => {
283
- let timeoutId: NodeJS.Timeout | null = null;
284
- let unsubscribe: (() => void) | null = null;
285
-
286
- // Cleanup function
287
- const cleanup = () => {
288
- if (timeoutId) {
289
- clearTimeout(timeoutId);
290
- timeoutId = null;
291
- }
292
- if (unsubscribe) {
293
- unsubscribe();
294
- unsubscribe = null;
295
- }
296
- };
297
-
298
- // Set timeout
299
- timeoutId = setTimeout(() => {
300
- cleanup();
301
- const error = new Error(`RPC timeout: ${method} (${timeout}ms)`);
302
- this.logger.error(`RPC timeout: ${method}`, { correlationId, timeout });
303
- reject(error);
304
- }, timeout);
305
-
306
- // Subscribe to reply channel
307
- try {
308
- unsubscribe = this.subscribe(replyChannel, (data: any) => {
309
- try {
310
- // Check if this is our response
311
- if (data.correlation_id === correlationId) {
312
- cleanup();
313
-
314
- // Check for error in response
315
- if (data.error) {
316
- this.logger.error(`RPC error: ${method}`, {
317
- correlationId,
318
- error: data.error
319
- });
320
- reject(new Error(data.error.message || 'RPC error'));
321
- return;
322
- }
323
-
324
- this.logger.success(`RPC response: ${method}`, {
325
- correlationId,
326
- hasResult: !!data.result
327
- });
328
- resolve(data.result as TResponse);
329
- }
330
- } catch (error) {
331
- cleanup();
332
- this.logger.error(`Error processing RPC response: ${method}`, error);
333
- reject(error);
334
- }
335
- });
336
-
337
- // Publish request to RPC channel
338
- // Note: This uses Centrifuge's publish, not our HTTP publish
339
- // The backend should be subscribed to this channel
340
- const rpcChannel = `rpc#${method}`;
341
- const sub = this.centrifuge.getSubscription(rpcChannel);
342
-
343
- if (sub) {
344
- // If we have a subscription, use it to publish
345
- sub.publish({
346
- correlation_id: correlationId,
347
- method,
348
- params,
349
- reply_to: replyChannel,
350
- timestamp: Date.now(),
351
- }).catch((error: any) => {
352
- cleanup();
353
- this.logger.error(`Failed to publish RPC request: ${method}`, error);
354
- reject(error);
355
- });
356
- } else {
357
- // Otherwise, we need to publish via HTTP API
358
- // This requires the backend to expose a publish endpoint
359
- cleanup();
360
- reject(new Error(
361
- `Cannot publish RPC request: no subscription to ${rpcChannel}. ` +
362
- `Backend should provide a publish endpoint or use server-side subscriptions.`
363
- ));
364
- }
365
- } catch (error) {
366
- cleanup();
367
- this.logger.error(`Failed to setup RPC: ${method}`, error);
368
- reject(error);
369
- }
370
- });
138
+ return legacyRpc(this.rpcManager, method, params, options);
371
139
  }
372
140
 
373
141
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
374
- // Channel Subscription API (for gRPC events)
142
+ // Native Centrifugo RPC (via RPC Proxy)
375
143
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
376
144
 
377
- /**
378
- * Subscribe to a Centrifugo channel for real-time events.
379
- *
380
- * @param channel - Channel name (e.g., 'bot#bot-123#heartbeat')
381
- * @param callback - Callback for received messages
382
- * @returns Unsubscribe function
383
- *
384
- * @example
385
- * const unsubscribe = client.subscribe('bot#bot-123#heartbeat', (data) => {
386
- * console.log('Heartbeat:', data);
387
- * });
388
- *
389
- * // Later: unsubscribe when done
390
- * unsubscribe();
391
- */
392
- subscribe(channel: string, callback: (data: any) => void): () => void {
393
- // Check if already subscribed - reuse existing subscription
394
- const existingSub = this.channelSubscriptions.get(channel);
395
- if (existingSub) {
396
- this.logger.warning(`Already subscribed to ${channel}, reusing existing subscription`);
397
-
398
- // Add new callback handler to existing subscription
399
- const handler = (ctx: any) => {
400
- try {
401
- callback(ctx.data);
402
- } catch (error) {
403
- this.logger.error(`Error in subscription callback for ${channel}`, error);
404
- }
405
- };
406
-
407
- existingSub.on('publication', handler);
408
-
409
- // Return unsubscribe that only removes this specific handler
410
- return () => {
411
- existingSub.off('publication', handler);
412
- };
413
- }
414
-
415
- // Create new subscription using Centrifuge's getSubscription or newSubscription
416
- let sub = this.centrifuge.getSubscription(channel);
417
- if (!sub) {
418
- sub = this.centrifuge.newSubscription(channel);
419
- }
420
-
421
- // Handle publications with error handling
422
- const publicationHandler = (ctx: any) => {
423
- try {
424
- callback(ctx.data);
425
- } catch (error) {
426
- this.logger.error(`Error in subscription callback for ${channel}`, error);
427
- }
428
- };
429
-
430
- sub.on('publication', publicationHandler);
431
-
432
- // Handle subscription lifecycle
433
- sub.on('subscribed', () => {
434
- this.logger.success(`Subscribed to ${channel}`);
435
- });
436
-
437
- sub.on('error', (ctx: any) => {
438
- this.logger.error(`Subscription error for ${channel}`, ctx.error);
439
- });
440
-
441
- // Start subscription
442
- sub.subscribe();
443
-
444
- // Store subscription
445
- this.channelSubscriptions.set(channel, sub);
446
-
447
- // Return unsubscribe function
448
- return () => this.unsubscribe(channel);
145
+ async namedRPC<TRequest = any, TResponse = any>(
146
+ method: string,
147
+ data: TRequest,
148
+ options?: { timeout?: number }
149
+ ): Promise<TResponse> {
150
+ return nativeNamedRPC(this.rpcManager, method, data, options);
449
151
  }
450
152
 
451
- /**
452
- * Unsubscribe from a channel.
453
- * Properly removes subscription from both internal map and Centrifuge client.
454
- *
455
- * @param channel - Channel name
456
- */
457
- unsubscribe(channel: string): void {
458
- const sub = this.channelSubscriptions.get(channel);
459
- if (!sub) {
460
- return;
461
- }
462
-
463
- try {
464
- // Unsubscribe from channel
465
- sub.unsubscribe();
466
-
467
- // Remove subscription from Centrifuge client registry
468
- this.centrifuge.removeSubscription(sub);
469
-
470
- // Remove from our internal map
471
- this.channelSubscriptions.delete(channel);
472
-
473
- this.logger.info(`Unsubscribed from ${channel}`);
474
- } catch (error) {
475
- this.logger.error(`Error unsubscribing from ${channel}`, error);
476
- // Still remove from our map even if there was an error
477
- this.channelSubscriptions.delete(channel);
478
- }
153
+ namedRPCNoWait<TRequest = any>(
154
+ method: string,
155
+ data: TRequest,
156
+ options?: RetryOptions
157
+ ): void {
158
+ nativeNamedRPCNoWait(this.rpcManager, method, data, options);
479
159
  }
480
160
 
481
- /**
482
- * Unsubscribe from all channels.
483
- * Properly removes all subscriptions from both internal map and Centrifuge client.
484
- */
485
- unsubscribeAll(): void {
486
- if (this.channelSubscriptions.size === 0) {
487
- return;
488
- }
489
-
490
- this.channelSubscriptions.forEach((sub, channel) => {
491
- try {
492
- sub.unsubscribe();
493
- this.centrifuge.removeSubscription(sub);
494
- } catch (error) {
495
- this.logger.error(`Error unsubscribing from ${channel}`, error);
496
- }
497
- });
498
-
499
- this.channelSubscriptions.clear();
500
- this.logger.info('Unsubscribed from all channels');
501
- }
161
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162
+ // API Version Checking
163
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
502
164
 
503
- /**
504
- * Get list of active client-side subscriptions.
505
- */
506
- getActiveSubscriptions(): string[] {
507
- return Array.from(this.channelSubscriptions.keys());
165
+ async checkApiVersion(clientVersion: string): Promise<VersionCheckResult> {
166
+ return checkVersion(
167
+ {
168
+ namedRPC: (method, data) => this.namedRPC(method, data),
169
+ logger: this.state.logger,
170
+ },
171
+ clientVersion
172
+ );
508
173
  }
509
174
 
510
- /**
511
- * Get list of server-side subscriptions (from JWT token).
512
- *
513
- * These are channels automatically subscribed by Centrifugo server
514
- * based on the 'channels' claim in the JWT token.
515
- */
516
- getServerSideSubscriptions(): string[] {
517
- try {
518
- // Access Centrifuge.js internal state for server-side subs
519
- // @ts-ignore - accessing internal property
520
- const serverSubs = this.centrifuge._serverSubs || {};
521
- return Object.keys(serverSubs);
522
- } catch (error) {
523
- return [];
524
- }
525
- }
175
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
176
+ // Internal Helpers
177
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
526
178
 
527
- /**
528
- * Get all active subscriptions (both client-side and server-side).
529
- */
530
- getAllSubscriptions(): string[] {
531
- const clientSubs = this.getActiveSubscriptions();
532
- const serverSubs = this.getServerSideSubscriptions();
179
+ private handleResponse(data: any): void {
180
+ handleResponse(this.state.pendingRequests, data);
181
+ }
533
182
 
534
- // Combine and deduplicate
535
- return Array.from(new Set([...clientSubs, ...serverSubs]));
183
+ private get subscriptionManager(): SubscriptionManager {
184
+ return {
185
+ channelSubscriptions: this.state.channelSubscriptions,
186
+ centrifuge: this.state.centrifuge,
187
+ logger: this.state.logger,
188
+ };
536
189
  }
537
190
 
538
- /**
539
- * Get native Centrifuge client for direct access to low-level APIs.
540
- * Use with caution - prefer using the high-level methods when possible.
541
- */
542
- getCentrifuge(): Centrifuge {
543
- return this.centrifuge;
191
+ private get rpcManager(): RPCManager {
192
+ return {
193
+ centrifuge: this.state.centrifuge,
194
+ pendingRequests: this.state.pendingRequests,
195
+ channelSubscriptions: this.state.channelSubscriptions,
196
+ userId: this.state.userId,
197
+ timeout: this.state.timeout,
198
+ logger: this.state.logger,
199
+ subscribe: (channel, callback) => this.subscribe(channel, callback),
200
+ };
544
201
  }
545
202
 
546
203
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
547
- // Native Centrifugo RPC (via RPC Proxy)
204
+ // Getters for Testing/Debugging
548
205
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
549
206
 
550
- /**
551
- * Call RPC method via native Centrifugo RPC proxy.
552
- *
553
- * This uses Centrifugo's built-in RPC mechanism which proxies
554
- * requests to the backend (Django) via HTTP.
555
- *
556
- * Flow:
557
- * 1. Client calls namedRPC('terminal.input', data)
558
- * 2. Centrifuge.js sends RPC over WebSocket
559
- * 3. Centrifugo proxies to Django: POST /centrifugo/rpc/
560
- * 4. Django routes to @websocket_rpc handler
561
- * 5. Response returned to client
562
- *
563
- * @param method - RPC method name (e.g., 'terminal.input')
564
- * @param data - Request data
565
- * @returns Promise that resolves with response data
566
- *
567
- * @example
568
- * const result = await client.namedRPC('terminal.input', {
569
- * session_id: 'abc-123',
570
- * data: btoa('ls -la')
571
- * });
572
- * console.log('Result:', result); // { success: true }
573
- */
574
- async namedRPC<TRequest = any, TResponse = any>(
575
- method: string,
576
- data: TRequest,
577
- options?: { timeout?: number }
578
- ): Promise<TResponse> {
579
- this.logger.info(`Native RPC: ${method}`, { data });
580
-
581
- try {
582
- // Note: centrifuge.rpc does not support timeout parameter directly
583
- // Timeout should be handled at higher level or via AbortController
584
- const result = await this.centrifuge.rpc(method, data);
585
-
586
- this.logger.success(`Native RPC success: ${method}`, {
587
- hasData: !!result.data
588
- });
589
-
590
- return result.data as TResponse;
591
- } catch (error) {
592
- this.logger.error(`Native RPC failed: ${method}`, error);
593
-
594
- // Dispatch error event for ErrorsTracker
595
- const errorMessage = error instanceof Error ? error.message : String(error);
596
- const errorCode = (error as any)?.code;
597
- dispatchCentrifugoError({
598
- method,
599
- error: errorMessage,
600
- code: errorCode,
601
- data,
602
- });
603
-
604
- throw error;
605
- }
207
+ /** @internal */
208
+ get logger(): Logger {
209
+ return this.state.logger;
210
+ }
211
+
212
+ /** @internal */
213
+ get userId(): string {
214
+ return this.state.userId;
606
215
  }
607
216
 
608
- /**
609
- * Fire-and-forget RPC call - sends without waiting for response.
610
- * Use for latency-sensitive operations like terminal input.
611
- *
612
- * Flow:
613
- * 1. Client calls namedRPCNoWait('terminal.input', data)
614
- * 2. Centrifuge.js sends RPC over WebSocket
615
- * 3. Client returns immediately (doesn't wait for response)
616
- * 4. Backend processes asynchronously
617
- *
618
- * @param method - RPC method name (e.g., 'terminal.input')
619
- * @param data - Request data
620
- *
621
- * @example
622
- * // Terminal input - user types, we send immediately
623
- * client.namedRPCNoWait('terminal.input', {
624
- * session_id: 'abc-123',
625
- * data: btoa('ls')
626
- * });
627
- */
628
- namedRPCNoWait<TRequest = any>(method: string, data: TRequest): void {
629
- this.logger.info(`Native RPC (fire-and-forget): ${method}`, { data });
630
-
631
- // Fire and forget - don't await the promise
632
- this.centrifuge.rpc(method, data).catch((error) => {
633
- // Log but don't throw - this is fire-and-forget
634
- this.logger.warning(`Fire-and-forget RPC failed: ${method}`, error);
635
- });
217
+ /** @internal */
218
+ get replyChannel(): string {
219
+ return this.state.replyChannel;
636
220
  }
637
221
  }