@djangocfg/centrifugo 2.1.53 → 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.
- package/README.md +218 -7
- package/package.json +5 -5
- package/src/core/client/CentrifugoRPCClient.ts +146 -532
- package/src/core/client/connection.ts +229 -0
- package/src/core/client/index.ts +47 -2
- package/src/core/client/rpc.ts +290 -0
- package/src/core/client/subscriptions.ts +176 -0
- package/src/core/client/types.ts +44 -0
- package/src/core/client/version.ts +92 -0
- package/src/events.ts +160 -47
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Management Module
|
|
3
|
+
*
|
|
4
|
+
* Handles Centrifuge WebSocket connection lifecycle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Centrifuge, type Subscription } from 'centrifuge';
|
|
8
|
+
|
|
9
|
+
import { dispatchConnected, dispatchDisconnected, dispatchReconnecting } from '../../events';
|
|
10
|
+
import { getConsolaLogger } from '../logger/consolaLogger';
|
|
11
|
+
|
|
12
|
+
import type { Logger } from '../logger';
|
|
13
|
+
import type { CentrifugoClientOptions, PendingRequest } from './types';
|
|
14
|
+
|
|
15
|
+
export interface ConnectionState {
|
|
16
|
+
centrifuge: Centrifuge;
|
|
17
|
+
replySubscription: Subscription | null;
|
|
18
|
+
pendingRequests: Map<string, PendingRequest>;
|
|
19
|
+
channelSubscriptions: Map<string, Subscription>;
|
|
20
|
+
userId: string;
|
|
21
|
+
replyChannel: string;
|
|
22
|
+
timeout: number;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create connection state from options.
|
|
28
|
+
* Handles both old positional args and new options-based API.
|
|
29
|
+
*/
|
|
30
|
+
export function createConnectionState(
|
|
31
|
+
urlOrOptions: string | CentrifugoClientOptions,
|
|
32
|
+
token?: string,
|
|
33
|
+
userId?: string,
|
|
34
|
+
timeout: number = 30000,
|
|
35
|
+
logger?: Logger
|
|
36
|
+
): ConnectionState {
|
|
37
|
+
// Parse arguments
|
|
38
|
+
let url: string;
|
|
39
|
+
let actualToken: string;
|
|
40
|
+
let actualUserId: string;
|
|
41
|
+
let actualTimeout: number;
|
|
42
|
+
let actualLogger: Logger | undefined;
|
|
43
|
+
let getToken: (() => Promise<string>) | undefined;
|
|
44
|
+
|
|
45
|
+
if (typeof urlOrOptions === 'object') {
|
|
46
|
+
url = urlOrOptions.url;
|
|
47
|
+
actualToken = urlOrOptions.token;
|
|
48
|
+
actualUserId = urlOrOptions.userId;
|
|
49
|
+
actualTimeout = urlOrOptions.timeout ?? 30000;
|
|
50
|
+
actualLogger = urlOrOptions.logger;
|
|
51
|
+
getToken = urlOrOptions.getToken;
|
|
52
|
+
} else {
|
|
53
|
+
url = urlOrOptions;
|
|
54
|
+
actualToken = token!;
|
|
55
|
+
actualUserId = userId!;
|
|
56
|
+
actualTimeout = timeout;
|
|
57
|
+
actualLogger = logger;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const log = actualLogger || getConsolaLogger('client');
|
|
61
|
+
const replyChannel = `user#${actualUserId}`;
|
|
62
|
+
|
|
63
|
+
// Build Centrifuge options
|
|
64
|
+
const centrifugeOptions: any = {
|
|
65
|
+
token: actualToken,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Add getToken callback for automatic token refresh
|
|
69
|
+
if (getToken) {
|
|
70
|
+
centrifugeOptions.getToken = async () => {
|
|
71
|
+
log.info('Token expired, refreshing...');
|
|
72
|
+
try {
|
|
73
|
+
const newToken = await getToken();
|
|
74
|
+
log.success('Token refreshed successfully');
|
|
75
|
+
return newToken;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
log.error('Failed to refresh token', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const centrifuge = new Centrifuge(url, centrifugeOptions);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
centrifuge,
|
|
87
|
+
replySubscription: null,
|
|
88
|
+
pendingRequests: new Map(),
|
|
89
|
+
channelSubscriptions: new Map(),
|
|
90
|
+
userId: actualUserId,
|
|
91
|
+
replyChannel,
|
|
92
|
+
timeout: actualTimeout,
|
|
93
|
+
logger: log,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Setup connection event handlers.
|
|
99
|
+
*/
|
|
100
|
+
export function setupConnectionHandlers(
|
|
101
|
+
state: ConnectionState,
|
|
102
|
+
onResponse: (data: any) => void
|
|
103
|
+
): void {
|
|
104
|
+
const { centrifuge, pendingRequests, logger, userId } = state;
|
|
105
|
+
|
|
106
|
+
// Handle disconnection - reject all pending requests
|
|
107
|
+
centrifuge.on('disconnected', (ctx) => {
|
|
108
|
+
pendingRequests.forEach(({ reject }) => {
|
|
109
|
+
reject(new Error('Disconnected from Centrifugo'));
|
|
110
|
+
});
|
|
111
|
+
pendingRequests.clear();
|
|
112
|
+
|
|
113
|
+
dispatchDisconnected({
|
|
114
|
+
userId,
|
|
115
|
+
reason: ctx.reason,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
logger.info('Disconnected from Centrifugo', { reason: ctx.reason });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Handle successful connection
|
|
122
|
+
centrifuge.on('connected', () => {
|
|
123
|
+
dispatchConnected({ userId });
|
|
124
|
+
logger.success('Connected to Centrifugo');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Handle reconnecting state
|
|
128
|
+
centrifuge.on('connecting', (ctx) => {
|
|
129
|
+
if (ctx.reason === 'transport closed') {
|
|
130
|
+
dispatchReconnecting({
|
|
131
|
+
userId,
|
|
132
|
+
attempt: 1,
|
|
133
|
+
reason: ctx.reason,
|
|
134
|
+
});
|
|
135
|
+
logger.info('Reconnecting to Centrifugo...', { reason: ctx.reason });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Connect to Centrifugo server.
|
|
142
|
+
*/
|
|
143
|
+
export async function connect(
|
|
144
|
+
state: ConnectionState,
|
|
145
|
+
onResponse: (data: any) => void
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const { centrifuge, replyChannel, logger } = state;
|
|
148
|
+
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
let resolved = false;
|
|
151
|
+
|
|
152
|
+
const onConnected = () => {
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
resolved = true;
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const onError = (ctx: any) => {
|
|
160
|
+
if (!resolved) {
|
|
161
|
+
resolved = true;
|
|
162
|
+
reject(new Error(ctx.message || 'Connection error'));
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
centrifuge.on('connected', onConnected);
|
|
167
|
+
centrifuge.on('error', onError);
|
|
168
|
+
|
|
169
|
+
// Start connection
|
|
170
|
+
centrifuge.connect();
|
|
171
|
+
|
|
172
|
+
// Subscribe to reply channel for RPC responses
|
|
173
|
+
const subscription = centrifuge.newSubscription(replyChannel);
|
|
174
|
+
state.replySubscription = subscription;
|
|
175
|
+
|
|
176
|
+
subscription.on('publication', (ctx: any) => {
|
|
177
|
+
onResponse(ctx.data);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
subscription.on('subscribed', () => {
|
|
181
|
+
logger.success(`Subscribed to reply channel: ${replyChannel}`);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
subscription.on('error', (ctx: any) => {
|
|
185
|
+
// Error code 105 = "already subscribed" (server-side subscription from JWT)
|
|
186
|
+
if (ctx.error?.code === 105) {
|
|
187
|
+
// This is fine, server-side subscription exists
|
|
188
|
+
} else {
|
|
189
|
+
logger.error(`Subscription error for ${replyChannel}:`, ctx.error);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
subscription.subscribe();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Disconnect from Centrifugo server.
|
|
199
|
+
*/
|
|
200
|
+
export function disconnect(state: ConnectionState): void {
|
|
201
|
+
const { centrifuge, replySubscription, channelSubscriptions, logger } = state;
|
|
202
|
+
|
|
203
|
+
// Unsubscribe from all event channels
|
|
204
|
+
channelSubscriptions.forEach((sub, channel) => {
|
|
205
|
+
try {
|
|
206
|
+
sub.unsubscribe();
|
|
207
|
+
centrifuge.removeSubscription(sub);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.error(`Error unsubscribing from ${channel}`, error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
channelSubscriptions.clear();
|
|
213
|
+
|
|
214
|
+
// Unsubscribe from RPC reply channel
|
|
215
|
+
if (replySubscription) {
|
|
216
|
+
replySubscription.unsubscribe();
|
|
217
|
+
state.replySubscription = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
centrifuge.disconnect();
|
|
221
|
+
logger.info('Disconnected from Centrifugo');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate unique correlation ID for RPC requests.
|
|
226
|
+
*/
|
|
227
|
+
export function generateCorrelationId(): string {
|
|
228
|
+
return `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
229
|
+
}
|
package/src/core/client/index.ts
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client Module
|
|
2
|
+
* Centrifugo Client Module
|
|
3
|
+
*
|
|
4
|
+
* Exports the main client and its supporting modules.
|
|
3
5
|
*/
|
|
4
6
|
|
|
7
|
+
// Main client (facade)
|
|
5
8
|
export { CentrifugoRPCClient } from './CentrifugoRPCClient';
|
|
6
|
-
|
|
9
|
+
|
|
10
|
+
// Types
|
|
11
|
+
export type {
|
|
12
|
+
CentrifugoClientOptions,
|
|
13
|
+
RPCOptions,
|
|
14
|
+
RetryOptions,
|
|
15
|
+
VersionCheckResult,
|
|
16
|
+
PendingRequest,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// Connection module (for advanced use cases)
|
|
20
|
+
export {
|
|
21
|
+
createConnectionState,
|
|
22
|
+
setupConnectionHandlers,
|
|
23
|
+
connect,
|
|
24
|
+
disconnect,
|
|
25
|
+
generateCorrelationId,
|
|
26
|
+
type ConnectionState,
|
|
27
|
+
} from './connection';
|
|
28
|
+
|
|
29
|
+
// Subscription module (for advanced use cases)
|
|
30
|
+
export {
|
|
31
|
+
subscribe,
|
|
32
|
+
unsubscribe,
|
|
33
|
+
unsubscribeAll,
|
|
34
|
+
getActiveSubscriptions,
|
|
35
|
+
getServerSideSubscriptions,
|
|
36
|
+
getAllSubscriptions,
|
|
37
|
+
type SubscriptionManager,
|
|
38
|
+
} from './subscriptions';
|
|
39
|
+
|
|
40
|
+
// RPC module (for advanced use cases)
|
|
41
|
+
export {
|
|
42
|
+
handleResponse,
|
|
43
|
+
call,
|
|
44
|
+
rpc,
|
|
45
|
+
namedRPC,
|
|
46
|
+
namedRPCNoWait,
|
|
47
|
+
type RPCManager,
|
|
48
|
+
} from './rpc';
|
|
49
|
+
|
|
50
|
+
// Version module (for advanced use cases)
|
|
51
|
+
export { checkApiVersion, type VersionChecker } from './version';
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Module
|
|
3
|
+
*
|
|
4
|
+
* Handles Centrifugo RPC patterns (request-response and fire-and-forget).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Centrifuge, Subscription } from 'centrifuge';
|
|
8
|
+
|
|
9
|
+
import { dispatchCentrifugoError } from '../../events';
|
|
10
|
+
|
|
11
|
+
import type { Logger } from '../logger';
|
|
12
|
+
import type { PendingRequest, RPCOptions, RetryOptions } from './types';
|
|
13
|
+
import { generateCorrelationId } from './connection';
|
|
14
|
+
|
|
15
|
+
export interface RPCManager {
|
|
16
|
+
centrifuge: Centrifuge;
|
|
17
|
+
pendingRequests: Map<string, PendingRequest>;
|
|
18
|
+
channelSubscriptions: Map<string, Subscription>;
|
|
19
|
+
userId: string;
|
|
20
|
+
timeout: number;
|
|
21
|
+
logger: Logger;
|
|
22
|
+
subscribe: (channel: string, callback: (data: any) => void) => () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handle RPC response from reply channel.
|
|
27
|
+
*/
|
|
28
|
+
export function handleResponse(
|
|
29
|
+
pendingRequests: Map<string, PendingRequest>,
|
|
30
|
+
data: any
|
|
31
|
+
): void {
|
|
32
|
+
const correlationId = data.correlation_id;
|
|
33
|
+
if (!correlationId) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pending = pendingRequests.get(correlationId);
|
|
38
|
+
if (!pending) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pendingRequests.delete(correlationId);
|
|
43
|
+
|
|
44
|
+
if (data.error) {
|
|
45
|
+
pending.reject(new Error(data.error.message || 'RPC error'));
|
|
46
|
+
} else {
|
|
47
|
+
pending.resolve(data.result);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Legacy RPC call via publish/subscribe pattern.
|
|
53
|
+
*
|
|
54
|
+
* @deprecated Use namedRPC for Centrifugo RPC Proxy
|
|
55
|
+
*/
|
|
56
|
+
export async function call<T = any>(
|
|
57
|
+
manager: RPCManager,
|
|
58
|
+
method: string,
|
|
59
|
+
params: any,
|
|
60
|
+
replyChannel: string
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const { centrifuge, pendingRequests, timeout, logger } = manager;
|
|
63
|
+
const correlationId = generateCorrelationId();
|
|
64
|
+
|
|
65
|
+
const message = {
|
|
66
|
+
method,
|
|
67
|
+
params,
|
|
68
|
+
correlation_id: correlationId,
|
|
69
|
+
reply_to: replyChannel,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
73
|
+
const timeoutId = setTimeout(() => {
|
|
74
|
+
pendingRequests.delete(correlationId);
|
|
75
|
+
reject(new Error(`RPC timeout: ${method}`));
|
|
76
|
+
}, timeout);
|
|
77
|
+
|
|
78
|
+
pendingRequests.set(correlationId, {
|
|
79
|
+
resolve: (result: T) => {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
resolve(result);
|
|
82
|
+
},
|
|
83
|
+
reject: (error: Error) => {
|
|
84
|
+
clearTimeout(timeoutId);
|
|
85
|
+
reject(error);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await centrifuge.publish('rpc.requests', message);
|
|
91
|
+
|
|
92
|
+
return promise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* RPC Pattern API (Request-Response via Correlation ID).
|
|
97
|
+
*
|
|
98
|
+
* @deprecated Use namedRPC for Centrifugo RPC Proxy
|
|
99
|
+
*/
|
|
100
|
+
export async function rpc<TRequest = any, TResponse = any>(
|
|
101
|
+
manager: RPCManager,
|
|
102
|
+
method: string,
|
|
103
|
+
params: TRequest,
|
|
104
|
+
options: RPCOptions = {}
|
|
105
|
+
): Promise<TResponse> {
|
|
106
|
+
const { centrifuge, logger, subscribe, userId } = manager;
|
|
107
|
+
const { timeout = 10000, replyChannel = `user#${userId}` } = options;
|
|
108
|
+
|
|
109
|
+
const correlationId = generateCorrelationId();
|
|
110
|
+
|
|
111
|
+
logger.info(`RPC request: ${method}`, { correlationId, params });
|
|
112
|
+
|
|
113
|
+
return new Promise<TResponse>((resolve, reject) => {
|
|
114
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
115
|
+
let unsubscribe: (() => void) | null = null;
|
|
116
|
+
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
if (timeoutId) {
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
timeoutId = null;
|
|
121
|
+
}
|
|
122
|
+
if (unsubscribe) {
|
|
123
|
+
unsubscribe();
|
|
124
|
+
unsubscribe = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
timeoutId = setTimeout(() => {
|
|
129
|
+
cleanup();
|
|
130
|
+
const error = new Error(`RPC timeout: ${method} (${timeout}ms)`);
|
|
131
|
+
logger.error(`RPC timeout: ${method}`, { correlationId, timeout });
|
|
132
|
+
reject(error);
|
|
133
|
+
}, timeout);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
unsubscribe = subscribe(replyChannel, (data: any) => {
|
|
137
|
+
try {
|
|
138
|
+
if (data.correlation_id === correlationId) {
|
|
139
|
+
cleanup();
|
|
140
|
+
|
|
141
|
+
if (data.error) {
|
|
142
|
+
logger.error(`RPC error: ${method}`, {
|
|
143
|
+
correlationId,
|
|
144
|
+
error: data.error,
|
|
145
|
+
});
|
|
146
|
+
reject(new Error(data.error.message || 'RPC error'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
logger.success(`RPC response: ${method}`, {
|
|
151
|
+
correlationId,
|
|
152
|
+
hasResult: !!data.result,
|
|
153
|
+
});
|
|
154
|
+
resolve(data.result as TResponse);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
cleanup();
|
|
158
|
+
logger.error(`Error processing RPC response: ${method}`, error);
|
|
159
|
+
reject(error);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const rpcChannel = `rpc#${method}`;
|
|
164
|
+
const sub = centrifuge.getSubscription(rpcChannel);
|
|
165
|
+
|
|
166
|
+
if (sub) {
|
|
167
|
+
sub
|
|
168
|
+
.publish({
|
|
169
|
+
correlation_id: correlationId,
|
|
170
|
+
method,
|
|
171
|
+
params,
|
|
172
|
+
reply_to: replyChannel,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
})
|
|
175
|
+
.catch((error: any) => {
|
|
176
|
+
cleanup();
|
|
177
|
+
logger.error(`Failed to publish RPC request: ${method}`, error);
|
|
178
|
+
reject(error);
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
cleanup();
|
|
182
|
+
reject(
|
|
183
|
+
new Error(
|
|
184
|
+
`Cannot publish RPC request: no subscription to ${rpcChannel}. ` +
|
|
185
|
+
`Backend should provide a publish endpoint or use server-side subscriptions.`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
cleanup();
|
|
191
|
+
logger.error(`Failed to setup RPC: ${method}`, error);
|
|
192
|
+
reject(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Call RPC method via native Centrifugo RPC proxy.
|
|
199
|
+
*
|
|
200
|
+
* This uses Centrifugo's built-in RPC mechanism which proxies
|
|
201
|
+
* requests to the backend (Django) via HTTP.
|
|
202
|
+
*
|
|
203
|
+
* Flow:
|
|
204
|
+
* 1. Client calls namedRPC('terminal.input', data)
|
|
205
|
+
* 2. Centrifuge.js sends RPC over WebSocket
|
|
206
|
+
* 3. Centrifugo proxies to Django: POST /centrifugo/rpc/
|
|
207
|
+
* 4. Django routes to @websocket_rpc handler
|
|
208
|
+
* 5. Response returned to client
|
|
209
|
+
*/
|
|
210
|
+
export async function namedRPC<TRequest = any, TResponse = any>(
|
|
211
|
+
manager: RPCManager,
|
|
212
|
+
method: string,
|
|
213
|
+
data: TRequest,
|
|
214
|
+
options?: { timeout?: number }
|
|
215
|
+
): Promise<TResponse> {
|
|
216
|
+
const { centrifuge, logger } = manager;
|
|
217
|
+
|
|
218
|
+
logger.info(`Native RPC: ${method}`, { data });
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const result = await centrifuge.rpc(method, data);
|
|
222
|
+
|
|
223
|
+
logger.success(`Native RPC success: ${method}`, {
|
|
224
|
+
hasData: !!result.data,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return result.data as TResponse;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error(`Native RPC failed: ${method}`, error);
|
|
230
|
+
|
|
231
|
+
// Dispatch error event for ErrorsTracker
|
|
232
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
233
|
+
const errorCode = (error as any)?.code;
|
|
234
|
+
dispatchCentrifugoError({
|
|
235
|
+
method,
|
|
236
|
+
error: errorMessage,
|
|
237
|
+
code: errorCode,
|
|
238
|
+
data,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Fire-and-forget RPC call - sends without waiting for response.
|
|
247
|
+
* Use for latency-sensitive operations like terminal input.
|
|
248
|
+
*
|
|
249
|
+
* Features:
|
|
250
|
+
* - Returns immediately (non-blocking)
|
|
251
|
+
* - Automatic retry with exponential backoff on failure
|
|
252
|
+
* - Configurable max retries and delays
|
|
253
|
+
*/
|
|
254
|
+
export function namedRPCNoWait<TRequest = any>(
|
|
255
|
+
manager: RPCManager,
|
|
256
|
+
method: string,
|
|
257
|
+
data: TRequest,
|
|
258
|
+
options?: RetryOptions
|
|
259
|
+
): void {
|
|
260
|
+
const { centrifuge, logger } = manager;
|
|
261
|
+
|
|
262
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
263
|
+
const baseDelayMs = options?.baseDelayMs ?? 100;
|
|
264
|
+
const maxDelayMs = options?.maxDelayMs ?? 2000;
|
|
265
|
+
|
|
266
|
+
const attemptSend = (attempt: number): void => {
|
|
267
|
+
centrifuge.rpc(method, data).catch((error) => {
|
|
268
|
+
if (attempt < maxRetries) {
|
|
269
|
+
// Exponential backoff: 100ms, 200ms, 400ms... capped at maxDelayMs
|
|
270
|
+
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
271
|
+
|
|
272
|
+
logger.warning(
|
|
273
|
+
`Fire-and-forget RPC failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms: ${method}`,
|
|
274
|
+
error
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
setTimeout(() => attemptSend(attempt + 1), delay);
|
|
278
|
+
} else {
|
|
279
|
+
// All retries exhausted
|
|
280
|
+
logger.error(
|
|
281
|
+
`Fire-and-forget RPC failed after ${maxRetries + 1} attempts: ${method}`,
|
|
282
|
+
error
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Start first attempt immediately
|
|
289
|
+
attemptSend(0);
|
|
290
|
+
}
|