livekit-client 2.18.10 → 2.19.0
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/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +720 -430
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.pt.worker.js.map +1 -1
- package/dist/livekit-client.pt.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +0 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -2
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +5 -13
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
- package/dist/src/room/rpc/client/events.d.ts +8 -0
- package/dist/src/room/rpc/client/events.d.ts.map +1 -0
- package/dist/src/room/rpc/index.d.ts +6 -0
- package/dist/src/room/rpc/index.d.ts.map +1 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
- package/dist/src/room/rpc/server/events.d.ts +8 -0
- package/dist/src/room/rpc/server/events.d.ts.map +1 -0
- package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/src/room/rpc/utils.d.ts.map +1 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/version.d.ts +8 -0
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +0 -3
- package/dist/ts4.2/room/Room.d.ts +4 -2
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +5 -13
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
- package/dist/ts4.2/room/rpc/index.d.ts +7 -0
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
- package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/ts4.2/version.d.ts +8 -0
- package/package.json +1 -1
- package/src/room/RTCEngine.ts +0 -26
- package/src/room/Room.ts +83 -81
- package/src/room/participant/LocalParticipant.ts +16 -180
- package/src/room/participant/RemoteParticipant.ts +9 -0
- package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
- package/src/room/rpc/client/RpcClientManager.ts +269 -0
- package/src/room/rpc/client/events.ts +9 -0
- package/src/room/rpc/index.ts +14 -0
- package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
- package/src/room/rpc/server/RpcServerManager.ts +293 -0
- package/src/room/rpc/server/events.ts +9 -0
- package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
- package/src/room/utils.ts +2 -1
- package/src/version.ts +10 -0
- package/dist/src/room/rpc.d.ts.map +0 -1
- package/src/room/rpc.test.ts +0 -301
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { default as RpcClientManager } from './client/RpcClientManager';
|
|
2
|
+
export type { RpcClientManagerCallbacks } from './client/events';
|
|
3
|
+
export { default as RpcServerManager } from './server/RpcServerManager';
|
|
4
|
+
export type { RpcServerManagerCallbacks } from './server/events';
|
|
5
|
+
export {
|
|
6
|
+
type PerformRpcParams,
|
|
7
|
+
RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
8
|
+
RPC_RESPONSE_DATA_STREAM_TOPIC,
|
|
9
|
+
RpcRequestAttrs,
|
|
10
|
+
RpcError,
|
|
11
|
+
type RpcInvocationData,
|
|
12
|
+
byteLength,
|
|
13
|
+
truncateBytes,
|
|
14
|
+
} from './utils';
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { RpcRequest } from '@livekit/protocol';
|
|
2
|
+
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import log from '../../../logger';
|
|
4
|
+
import { subscribeToEvents } from '../../../utils/subscribeToEvents';
|
|
5
|
+
import { CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT } from '../../../version';
|
|
6
|
+
import type RTCEngine from '../../RTCEngine';
|
|
7
|
+
import OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
|
|
8
|
+
import { RPC_RESPONSE_DATA_STREAM_TOPIC, RpcError, RpcRequestAttrs } from '../utils';
|
|
9
|
+
import RpcServerManager from './RpcServerManager';
|
|
10
|
+
import type { RpcServerManagerCallbacks } from './events';
|
|
11
|
+
|
|
12
|
+
describe('RpcServerManager', () => {
|
|
13
|
+
describe('v1 -> v1', () => {
|
|
14
|
+
let rpcServerManager: RpcServerManager;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
const outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
18
|
+
{} as unknown as RTCEngine,
|
|
19
|
+
log,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
rpcServerManager = new RpcServerManager(
|
|
23
|
+
log,
|
|
24
|
+
outgoingDataStreamManager,
|
|
25
|
+
(_identity) => CLIENT_PROTOCOL_DEFAULT,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should receive a rpc message from a participant', async () => {
|
|
30
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
31
|
+
'sendDataPacket',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const handler = async () => 'response payload';
|
|
35
|
+
rpcServerManager.registerRpcMethod('test-method', handler);
|
|
36
|
+
|
|
37
|
+
const requestId = crypto.randomUUID();
|
|
38
|
+
const responseTimeoutMs = 10_000;
|
|
39
|
+
await rpcServerManager.handleIncomingRpcRequest(
|
|
40
|
+
'caller-identity',
|
|
41
|
+
new RpcRequest({
|
|
42
|
+
id: requestId,
|
|
43
|
+
method: 'test-method',
|
|
44
|
+
payload: 'request payload',
|
|
45
|
+
responseTimeoutMs,
|
|
46
|
+
version: 1,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// The first event is an acknowledgement of the request
|
|
51
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
52
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
53
|
+
expect(ackEvent.packet.value.value.requestId).toStrictEqual(requestId);
|
|
54
|
+
|
|
55
|
+
// And the second being the actual response
|
|
56
|
+
const responseEvent = await managerEvents.waitFor('sendDataPacket');
|
|
57
|
+
assert(responseEvent.packet.value.case === 'rpcResponse');
|
|
58
|
+
const rpcResponse = responseEvent.packet.value.value;
|
|
59
|
+
expect(rpcResponse.requestId).toStrictEqual(requestId);
|
|
60
|
+
assert(rpcResponse.value.case === 'payload');
|
|
61
|
+
expect(rpcResponse.value.value).toStrictEqual('response payload');
|
|
62
|
+
|
|
63
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should register a RPC method handler', async () => {
|
|
67
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
68
|
+
'sendDataPacket',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const methodName = 'testMethod';
|
|
72
|
+
const handler = vi.fn().mockResolvedValue('test response');
|
|
73
|
+
|
|
74
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
75
|
+
|
|
76
|
+
await rpcServerManager.handleIncomingRpcRequest(
|
|
77
|
+
'remote-identity',
|
|
78
|
+
new RpcRequest({
|
|
79
|
+
id: 'test-request-id',
|
|
80
|
+
method: methodName,
|
|
81
|
+
payload: 'test payload',
|
|
82
|
+
responseTimeoutMs: 5000,
|
|
83
|
+
version: 1,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(handler).toHaveBeenCalledWith({
|
|
88
|
+
requestId: 'test-request-id',
|
|
89
|
+
callerIdentity: 'remote-identity',
|
|
90
|
+
payload: 'test payload',
|
|
91
|
+
responseTimeout: 5000,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Ensure the first event was for the ack
|
|
95
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
96
|
+
expect(ackEvent.packet.value.case).toStrictEqual('rpcAck');
|
|
97
|
+
|
|
98
|
+
// And the second event was for the response
|
|
99
|
+
const responseEvent = await managerEvents.waitFor('sendDataPacket');
|
|
100
|
+
expect(responseEvent.packet.value.case).toStrictEqual('rpcResponse');
|
|
101
|
+
|
|
102
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should catch and transform unhandled errors in the RPC method handler', async () => {
|
|
106
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
107
|
+
'sendDataPacket',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const methodName = 'errorMethod';
|
|
111
|
+
const errorMessage = 'Test error';
|
|
112
|
+
const handler = async () => {
|
|
113
|
+
throw new Error(errorMessage);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
117
|
+
|
|
118
|
+
await rpcServerManager.handleIncomingRpcRequest(
|
|
119
|
+
'remote-identity',
|
|
120
|
+
new RpcRequest({
|
|
121
|
+
id: 'test-error-request-id',
|
|
122
|
+
method: methodName,
|
|
123
|
+
payload: 'test payload',
|
|
124
|
+
responseTimeoutMs: 5000,
|
|
125
|
+
version: 1,
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Ensure the first event was for the ack
|
|
130
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
131
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
132
|
+
|
|
133
|
+
// And the second event was for the error response
|
|
134
|
+
const errorEvent = await managerEvents.waitFor('sendDataPacket');
|
|
135
|
+
assert(errorEvent.packet.value.case === 'rpcResponse');
|
|
136
|
+
assert(errorEvent.packet.value.value.value.case === 'error');
|
|
137
|
+
const errorResponse = errorEvent.packet.value.value.value.value;
|
|
138
|
+
expect(errorResponse.code).toStrictEqual(RpcError.ErrorCode.APPLICATION_ERROR);
|
|
139
|
+
|
|
140
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should pass through RpcError thrown by the RPC method handler', async () => {
|
|
144
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
145
|
+
'sendDataPacket',
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const methodName = 'rpcErrorMethod';
|
|
149
|
+
const errorCode = 101;
|
|
150
|
+
const errorMessage = 'some-error-message';
|
|
151
|
+
const handler = async () => {
|
|
152
|
+
throw new RpcError(errorCode, errorMessage);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
156
|
+
|
|
157
|
+
await rpcServerManager.handleIncomingRpcRequest(
|
|
158
|
+
'remote-identity',
|
|
159
|
+
new RpcRequest({
|
|
160
|
+
id: 'test-rpc-error-request-id',
|
|
161
|
+
method: methodName,
|
|
162
|
+
payload: 'test payload',
|
|
163
|
+
responseTimeoutMs: 5000,
|
|
164
|
+
version: 1,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Ensure the first event was for the ack
|
|
169
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
170
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
171
|
+
|
|
172
|
+
// And the second event was for the error response
|
|
173
|
+
const errorEvent = await managerEvents.waitFor('sendDataPacket');
|
|
174
|
+
assert(errorEvent.packet.value.case === 'rpcResponse');
|
|
175
|
+
assert(errorEvent.packet.value.value.value.case === 'error');
|
|
176
|
+
const errorResponse = errorEvent.packet.value.value.value.value;
|
|
177
|
+
expect(errorResponse.code).toStrictEqual(errorCode);
|
|
178
|
+
expect(errorResponse.message).toStrictEqual(errorMessage);
|
|
179
|
+
|
|
180
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('v2 -> v2', () => {
|
|
185
|
+
let rpcServerManager: RpcServerManager;
|
|
186
|
+
let outgoingDataStreamManager: OutgoingDataStreamManager;
|
|
187
|
+
let mockStreamTextWriter: {
|
|
188
|
+
write: ReturnType<typeof vi.fn>;
|
|
189
|
+
close: ReturnType<typeof vi.fn>;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
outgoingDataStreamManager = new OutgoingDataStreamManager({} as unknown as RTCEngine, log);
|
|
194
|
+
|
|
195
|
+
mockStreamTextWriter = {
|
|
196
|
+
write: vi.fn().mockResolvedValue(undefined),
|
|
197
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
198
|
+
};
|
|
199
|
+
vi.spyOn(outgoingDataStreamManager, 'streamText').mockResolvedValue(
|
|
200
|
+
mockStreamTextWriter as any,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
rpcServerManager = new RpcServerManager(
|
|
204
|
+
log,
|
|
205
|
+
outgoingDataStreamManager,
|
|
206
|
+
(_identity) => CLIENT_PROTOCOL_DATA_STREAM_RPC,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function makeDataStreamAttrs(requestId: string, method: string, responseTimeout: number) {
|
|
211
|
+
return {
|
|
212
|
+
[RpcRequestAttrs.RPC_REQUEST_ID]: requestId,
|
|
213
|
+
[RpcRequestAttrs.RPC_REQUEST_METHOD]: method,
|
|
214
|
+
[RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS]: `${responseTimeout}`,
|
|
215
|
+
[RpcRequestAttrs.RPC_REQUEST_VERSION]: '2',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mockTextStreamReader(payload: string) {
|
|
220
|
+
return { readAll: vi.fn().mockResolvedValue(payload) } as any;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
it('should receive a small rpc request (< 15kb) and send a small response via data stream from a participant', async () => {
|
|
224
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
225
|
+
'sendDataPacket',
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const handler = async () => 'response payload';
|
|
229
|
+
rpcServerManager.registerRpcMethod('test-method', handler);
|
|
230
|
+
|
|
231
|
+
const requestId = crypto.randomUUID();
|
|
232
|
+
const responseTimeoutMs = 10_000;
|
|
233
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
234
|
+
mockTextStreamReader('request payload'),
|
|
235
|
+
'caller-identity',
|
|
236
|
+
makeDataStreamAttrs(requestId, 'test-method', responseTimeoutMs),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// The first event is an acknowledgement of the request
|
|
240
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
241
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
242
|
+
expect(ackEvent.packet.value.value.requestId).toStrictEqual(requestId);
|
|
243
|
+
|
|
244
|
+
// The response should have been sent via data stream, not packet
|
|
245
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
246
|
+
expect(outgoingDataStreamManager.streamText).toHaveBeenCalledWith(
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
|
|
249
|
+
destinationIdentities: ['caller-identity'],
|
|
250
|
+
attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
expect(mockStreamTextWriter.write).toHaveBeenCalledWith('response payload');
|
|
254
|
+
expect(mockStreamTextWriter.close).toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should receive a large rpc request (> 15kb) and send a large response via data stream from a participant', async () => {
|
|
258
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
259
|
+
'sendDataPacket',
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const handler = async () => new Array(20_000).fill('B').join('');
|
|
263
|
+
rpcServerManager.registerRpcMethod('test-method', handler);
|
|
264
|
+
|
|
265
|
+
const requestId = crypto.randomUUID();
|
|
266
|
+
const responseTimeoutMs = 10_000;
|
|
267
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
268
|
+
mockTextStreamReader(new Array(20_000).fill('A').join('')),
|
|
269
|
+
'caller-identity',
|
|
270
|
+
makeDataStreamAttrs(requestId, 'test-method', responseTimeoutMs),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// The first event is an acknowledgement of the request
|
|
274
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
275
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
276
|
+
expect(ackEvent.packet.value.value.requestId).toStrictEqual(requestId);
|
|
277
|
+
|
|
278
|
+
// The response should have been sent via data stream, not packet
|
|
279
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
280
|
+
expect(outgoingDataStreamManager.streamText).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
|
|
283
|
+
destinationIdentities: ['caller-identity'],
|
|
284
|
+
attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
expect(mockStreamTextWriter.write).toHaveBeenCalledWith(new Array(20_000).fill('B').join(''));
|
|
288
|
+
expect(mockStreamTextWriter.close).toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should register an RPC method handler', async () => {
|
|
292
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
293
|
+
'sendDataPacket',
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
const methodName = 'testMethod';
|
|
297
|
+
const handler = vi.fn().mockResolvedValue('test response');
|
|
298
|
+
|
|
299
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
300
|
+
|
|
301
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
302
|
+
mockTextStreamReader('test payload'),
|
|
303
|
+
'remote-identity',
|
|
304
|
+
makeDataStreamAttrs('test-request-id', methodName, 5000),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(handler).toHaveBeenCalledWith({
|
|
308
|
+
requestId: 'test-request-id',
|
|
309
|
+
callerIdentity: 'remote-identity',
|
|
310
|
+
payload: 'test payload',
|
|
311
|
+
responseTimeout: 5000,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Ensure the ack was sent
|
|
315
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
316
|
+
expect(ackEvent.packet.value.case).toStrictEqual('rpcAck');
|
|
317
|
+
|
|
318
|
+
// Response goes via data stream, not packet
|
|
319
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
320
|
+
expect(outgoingDataStreamManager.streamText).toHaveBeenCalled();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should catch and transform unhandled errors in the RPC method handler', async () => {
|
|
324
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
325
|
+
'sendDataPacket',
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
const methodName = 'errorMethod';
|
|
329
|
+
const errorMessage = 'Test error';
|
|
330
|
+
const handler = async () => {
|
|
331
|
+
throw new Error(errorMessage);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
335
|
+
|
|
336
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
337
|
+
mockTextStreamReader('test payload'),
|
|
338
|
+
'remote-identity',
|
|
339
|
+
makeDataStreamAttrs('test-error-request-id', methodName, 5000),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Ensure the first event was for the ack
|
|
343
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
344
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
345
|
+
|
|
346
|
+
// Error responses always go via packet, even for v2 callers
|
|
347
|
+
const errorEvent = await managerEvents.waitFor('sendDataPacket');
|
|
348
|
+
assert(errorEvent.packet.value.case === 'rpcResponse');
|
|
349
|
+
assert(errorEvent.packet.value.value.value.case === 'error');
|
|
350
|
+
const errorResponse = errorEvent.packet.value.value.value.value;
|
|
351
|
+
expect(errorResponse.code).toStrictEqual(RpcError.ErrorCode.APPLICATION_ERROR);
|
|
352
|
+
|
|
353
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should pass through RpcError thrown by the RPC method handler', async () => {
|
|
357
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
358
|
+
'sendDataPacket',
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const methodName = 'rpcErrorMethod';
|
|
362
|
+
const errorCode = 101;
|
|
363
|
+
const errorMessage = 'some-error-message';
|
|
364
|
+
const handler = async () => {
|
|
365
|
+
throw new RpcError(errorCode, errorMessage);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
rpcServerManager.registerRpcMethod(methodName, handler);
|
|
369
|
+
|
|
370
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
371
|
+
mockTextStreamReader('test payload'),
|
|
372
|
+
'remote-identity',
|
|
373
|
+
makeDataStreamAttrs('test-rpc-error-request-id', methodName, 5000),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Ensure the first event was for the ack
|
|
377
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
378
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
379
|
+
|
|
380
|
+
// Error responses always go via packet, even for v2 callers
|
|
381
|
+
const errorEvent = await managerEvents.waitFor('sendDataPacket');
|
|
382
|
+
assert(errorEvent.packet.value.case === 'rpcResponse');
|
|
383
|
+
assert(errorEvent.packet.value.value.value.case === 'error');
|
|
384
|
+
const errorResponse = errorEvent.packet.value.value.value.value;
|
|
385
|
+
expect(errorResponse.code).toStrictEqual(errorCode);
|
|
386
|
+
expect(errorResponse.message).toStrictEqual(errorMessage);
|
|
387
|
+
|
|
388
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should ack and respond with UNSUPPORTED_METHOD for an unregistered method', async () => {
|
|
392
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
393
|
+
'sendDataPacket',
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
// Intentionally do not call registerRpcMethod for "unknown-method".
|
|
397
|
+
|
|
398
|
+
const requestId = crypto.randomUUID();
|
|
399
|
+
await rpcServerManager.handleIncomingDataStream(
|
|
400
|
+
mockTextStreamReader('request payload'),
|
|
401
|
+
'caller-identity',
|
|
402
|
+
makeDataStreamAttrs(requestId, 'unknown-method', 5000),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Ack must be sent first so the caller knows the handler is alive.
|
|
406
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
407
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
408
|
+
expect(ackEvent.packet.value.value.requestId).toStrictEqual(requestId);
|
|
409
|
+
|
|
410
|
+
// Then a v1 RpcResponse packet with UNSUPPORTED_METHOD - never a data stream.
|
|
411
|
+
const errorEvent = await managerEvents.waitFor('sendDataPacket');
|
|
412
|
+
assert(errorEvent.packet.value.case === 'rpcResponse');
|
|
413
|
+
assert(errorEvent.packet.value.value.value.case === 'error');
|
|
414
|
+
const errorResponse = errorEvent.packet.value.value.value.value;
|
|
415
|
+
expect(errorResponse.code).toStrictEqual(RpcError.ErrorCode.UNSUPPORTED_METHOD);
|
|
416
|
+
|
|
417
|
+
expect(outgoingDataStreamManager.streamText).not.toHaveBeenCalled();
|
|
418
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('v1 -> v2', () => {
|
|
423
|
+
it('should use v1 protocol (RpcResponse packet) when responding to a v1 caller', async () => {
|
|
424
|
+
const outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
425
|
+
{} as unknown as RTCEngine,
|
|
426
|
+
log,
|
|
427
|
+
);
|
|
428
|
+
const streamTextSpy = vi.spyOn(outgoingDataStreamManager, 'streamText');
|
|
429
|
+
|
|
430
|
+
const rpcServerManager = new RpcServerManager(
|
|
431
|
+
log,
|
|
432
|
+
outgoingDataStreamManager,
|
|
433
|
+
(_identity) => CLIENT_PROTOCOL_DEFAULT,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const managerEvents = subscribeToEvents<RpcServerManagerCallbacks>(rpcServerManager, [
|
|
437
|
+
'sendDataPacket',
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
const handler = async () => 'response payload';
|
|
441
|
+
rpcServerManager.registerRpcMethod('test-method', handler);
|
|
442
|
+
|
|
443
|
+
const requestId = crypto.randomUUID();
|
|
444
|
+
await rpcServerManager.handleIncomingRpcRequest(
|
|
445
|
+
'v1-caller',
|
|
446
|
+
new RpcRequest({
|
|
447
|
+
id: requestId,
|
|
448
|
+
method: 'test-method',
|
|
449
|
+
payload: 'request payload',
|
|
450
|
+
responseTimeoutMs: 10_000,
|
|
451
|
+
version: 1,
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Ack via packet
|
|
456
|
+
const ackEvent = await managerEvents.waitFor('sendDataPacket');
|
|
457
|
+
assert(ackEvent.packet.value.case === 'rpcAck');
|
|
458
|
+
|
|
459
|
+
// Response should be a v1 RpcResponse packet, not a data stream
|
|
460
|
+
expect(streamTextSpy).not.toHaveBeenCalled();
|
|
461
|
+
const responseEvent = await managerEvents.waitFor('sendDataPacket');
|
|
462
|
+
assert(responseEvent.packet.value.case === 'rpcResponse');
|
|
463
|
+
const rpcResponse = responseEvent.packet.value.value;
|
|
464
|
+
expect(rpcResponse.requestId).toStrictEqual(requestId);
|
|
465
|
+
assert(rpcResponse.value.case === 'payload');
|
|
466
|
+
expect(rpcResponse.value.value).toStrictEqual('response payload');
|
|
467
|
+
|
|
468
|
+
expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|