@dxos/rpc 2.33.9-dev.d0ae5f95 → 2.33.9-dev.d0dce35f

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/src/rpc.test.ts CHANGED
@@ -13,17 +13,22 @@ import { Any } from './proto/gen/google/protobuf';
13
13
  import { RpcPeer } from './rpc';
14
14
  import { createLinkedPorts } from './testutil';
15
15
 
16
+ const createPayload = (value = ''): Any => ({
17
+ type_url: '',
18
+ value: Buffer.from(value)
19
+ });
20
+
16
21
  describe('RpcPeer', () => {
17
22
  describe('handshake', () => {
18
23
  test('can open', async () => {
19
24
  const [alicePort, bobPort] = createLinkedPorts();
20
25
 
21
26
  const alice = new RpcPeer({
22
- messageHandler: async msg => ({}),
27
+ messageHandler: async msg => createPayload(),
23
28
  port: alicePort
24
29
  });
25
30
  const bob = new RpcPeer({
26
- messageHandler: async msg => ({}),
31
+ messageHandler: async msg => createPayload(),
27
32
  port: bobPort
28
33
  });
29
34
 
@@ -36,11 +41,11 @@ describe('RpcPeer', () => {
36
41
  test('open waits for the other peer to call open', async () => {
37
42
  const [alicePort, bobPort] = createLinkedPorts();
38
43
  const alice: RpcPeer = new RpcPeer({
39
- messageHandler: async msg => ({}),
44
+ messageHandler: async msg => createPayload(),
40
45
  port: alicePort
41
46
  });
42
47
  const bob = new RpcPeer({
43
- messageHandler: async msg => ({}),
48
+ messageHandler: async msg => createPayload(),
44
49
  port: bobPort
45
50
  });
46
51
 
@@ -64,7 +69,7 @@ describe('RpcPeer', () => {
64
69
  const [alicePort, bobPort] = createLinkedPorts();
65
70
 
66
71
  const alice: RpcPeer = new RpcPeer({
67
- messageHandler: async msg => ({}),
72
+ messageHandler: async msg => createPayload(),
68
73
  port: alicePort
69
74
  });
70
75
  const aliceOpen = alice.open();
@@ -72,7 +77,7 @@ describe('RpcPeer', () => {
72
77
  await sleep(5);
73
78
 
74
79
  const bob = new RpcPeer({
75
- messageHandler: async msg => ({}),
80
+ messageHandler: async msg => createPayload(),
76
81
  port: bobPort
77
82
  });
78
83
 
@@ -88,7 +93,7 @@ describe('RpcPeer', () => {
88
93
  let portOpen = false;
89
94
 
90
95
  const alice: RpcPeer = new RpcPeer({
91
- messageHandler: async msg => ({}),
96
+ messageHandler: async msg => createPayload(),
92
97
  port: {
93
98
  send: msg => {
94
99
  portOpen && alicePort.send(msg);
@@ -98,7 +103,7 @@ describe('RpcPeer', () => {
98
103
  });
99
104
 
100
105
  const bob = new RpcPeer({
101
- messageHandler: async msg => ({}),
106
+ messageHandler: async msg => createPayload(),
102
107
  port: {
103
108
  send: msg => {
104
109
  portOpen && bobPort.send(msg);
@@ -123,7 +128,7 @@ describe('RpcPeer', () => {
123
128
  const [alicePort, bobPort] = createLinkedPorts();
124
129
 
125
130
  const alice: RpcPeer = new RpcPeer({
126
- messageHandler: async msg => ({}),
131
+ messageHandler: async msg => createPayload(),
127
132
  port: {
128
133
  send: msg => { },
129
134
  subscribe: alicePort.subscribe
@@ -131,7 +136,7 @@ describe('RpcPeer', () => {
131
136
  });
132
137
 
133
138
  const bob = new RpcPeer({
134
- messageHandler: async msg => ({}),
139
+ messageHandler: async msg => createPayload(),
135
140
  port: bobPort
136
141
  });
137
142
 
@@ -160,12 +165,12 @@ describe('RpcPeer', () => {
160
165
  messageHandler: async (method, msg) => {
161
166
  expect(method).toEqual('method');
162
167
  expect(msg.value).toEqual(Buffer.from('request'));
163
- return { value: Buffer.from('response') };
168
+ return createPayload('response');
164
169
  },
165
170
  port: alicePort
166
171
  });
167
172
  const bob = new RpcPeer({
168
- messageHandler: async (method, msg) => ({}),
173
+ messageHandler: async (method, msg) => createPayload(),
169
174
  port: bobPort
170
175
  });
171
176
 
@@ -174,8 +179,8 @@ describe('RpcPeer', () => {
174
179
  bob.open()
175
180
  ]);
176
181
 
177
- const response = await bob.call('method', { value: Buffer.from('request') });
178
- expect(response).toEqual({ value: Buffer.from('response') });
182
+ const response = await bob.call('method', createPayload('request'));
183
+ expect(response).toEqual(createPayload('response'));
179
184
  });
180
185
 
181
186
  test('can send multiple requests', async () => {
@@ -197,7 +202,7 @@ describe('RpcPeer', () => {
197
202
  port: alicePort
198
203
  });
199
204
  const bob = new RpcPeer({
200
- messageHandler: async msg => ({}),
205
+ messageHandler: async msg => createPayload(),
201
206
  port: bobPort
202
207
  });
203
208
 
@@ -206,14 +211,14 @@ describe('RpcPeer', () => {
206
211
  bob.open()
207
212
  ]);
208
213
 
209
- expect((await bob.call('method', { value: Buffer.from('request') })).value).toEqual(Buffer.from('request'));
214
+ expect((await bob.call('method', createPayload('request'))).value).toEqual(Buffer.from('request'));
210
215
 
211
- const parallel1 = bob.call('method', { value: Buffer.from('p1') });
212
- const parallel2 = bob.call('method', { value: Buffer.from('p2') });
213
- const error = bob.call('method', { value: Buffer.from('error') });
216
+ const parallel1 = bob.call('method', createPayload('p1'));
217
+ const parallel2 = bob.call('method', createPayload('p2'));
218
+ const error = bob.call('method', createPayload('error'));
214
219
 
215
- await expect(await parallel1).toEqual({ value: Buffer.from('p1') });
216
- await expect(await parallel2).toEqual({ value: Buffer.from('p2') });
220
+ await expect(await parallel1).toEqual(createPayload('p1'));
221
+ await expect(await parallel2).toEqual(createPayload('p2'));
217
222
  await expect(error).toBeRejected();
218
223
  });
219
224
 
@@ -232,7 +237,7 @@ describe('RpcPeer', () => {
232
237
  port: alicePort
233
238
  });
234
239
  const bob = new RpcPeer({
235
- messageHandler: async msg => ({}),
240
+ messageHandler: async msg => createPayload(),
236
241
  port: bobPort
237
242
  });
238
243
 
@@ -243,7 +248,7 @@ describe('RpcPeer', () => {
243
248
 
244
249
  let error!: Error;
245
250
  try {
246
- await bob.call('RpcMethodName', { value: Buffer.from('request') });
251
+ await bob.call('RpcMethodName', createPayload('request'));
247
252
  } catch (err: any) {
248
253
  error = err;
249
254
  }
@@ -266,7 +271,7 @@ describe('RpcPeer', () => {
266
271
  port: alicePort
267
272
  });
268
273
  const bob = new RpcPeer({
269
- messageHandler: async msg => ({}),
274
+ messageHandler: async msg => createPayload(),
270
275
  port: bobPort
271
276
  });
272
277
 
@@ -275,7 +280,7 @@ describe('RpcPeer', () => {
275
280
  bob.open()
276
281
  ]);
277
282
 
278
- const req = bob.call('method', { value: Buffer.from('request') });
283
+ const req = bob.call('method', createPayload('request'));
279
284
  bob.close();
280
285
 
281
286
  await expect(req).toBeRejected();
@@ -293,7 +298,7 @@ describe('RpcPeer', () => {
293
298
  port: alicePort
294
299
  });
295
300
  const bob = new RpcPeer({
296
- messageHandler: async msg => ({}),
301
+ messageHandler: async msg => createPayload(),
297
302
  port: bobPort,
298
303
  timeout: 50
299
304
  });
@@ -304,8 +309,34 @@ describe('RpcPeer', () => {
304
309
  ]);
305
310
 
306
311
  alice.close();
307
- const req = bob.call('method', { value: Buffer.from('request') });
312
+ const req = bob.call('method', createPayload('request'));
313
+
314
+ await expect(req).toBeRejected();
315
+ });
316
+
317
+ test('requests failing on timeout', async () => {
318
+ const [alicePort, bobPort] = createLinkedPorts();
319
+
320
+ const alice: RpcPeer = new RpcPeer({
321
+ messageHandler: async (method, msg) => {
322
+ expect(method).toEqual('method');
323
+ await sleep(50);
324
+ return msg;
325
+ },
326
+ port: alicePort
327
+ });
328
+ const bob = new RpcPeer({
329
+ messageHandler: async msg => createPayload(),
330
+ port: bobPort,
331
+ timeout: 10
332
+ });
308
333
 
334
+ await Promise.all([
335
+ alice.open(),
336
+ bob.open()
337
+ ]);
338
+
339
+ const req = bob.call('method', createPayload('request'));
309
340
  await expect(req).toBeRejected();
310
341
  });
311
342
 
@@ -316,20 +347,20 @@ describe('RpcPeer', () => {
316
347
  const [alicePort, bobPort] = createLinkedPorts();
317
348
 
318
349
  const alice = new RpcPeer({
319
- messageHandler: async msg => ({}),
350
+ messageHandler: async msg => createPayload(),
320
351
  streamHandler: (method, msg) => {
321
352
  expect(method).toEqual('method');
322
353
  expect(msg.value!).toEqual(Buffer.from('request'));
323
354
  return new Stream<Any>(({ next, close }) => {
324
- next({ value: Buffer.from('res1') });
325
- next({ value: Buffer.from('res2') });
355
+ next(createPayload('res1'));
356
+ next(createPayload('res2'));
326
357
  close();
327
358
  });
328
359
  },
329
360
  port: alicePort
330
361
  });
331
362
  const bob = new RpcPeer({
332
- messageHandler: async msg => ({}),
363
+ messageHandler: async msg => createPayload(),
333
364
  port: bobPort
334
365
  });
335
366
 
@@ -338,12 +369,13 @@ describe('RpcPeer', () => {
338
369
  bob.open()
339
370
  ]);
340
371
 
341
- const stream = await bob.callStream('method', { value: Buffer.from('request') });
372
+ const stream = await bob.callStream('method', createPayload('request'));
342
373
  expect(stream).toBeA(Stream);
343
374
 
344
375
  expect(await Stream.consume(stream)).toEqual([
345
- { data: { value: Buffer.from('res1') } },
346
- { data: { value: Buffer.from('res2') } },
376
+ { ready: true },
377
+ { data: createPayload('res1') },
378
+ { data: createPayload('res2') },
347
379
  { closed: true }
348
380
  ]);
349
381
  });
@@ -352,7 +384,7 @@ describe('RpcPeer', () => {
352
384
  const [alicePort, bobPort] = createLinkedPorts();
353
385
 
354
386
  const alice = new RpcPeer({
355
- messageHandler: async msg => ({}),
387
+ messageHandler: async msg => createPayload(),
356
388
  streamHandler: (method, msg) => {
357
389
  expect(method).toEqual('method');
358
390
  expect(msg.value).toEqual(Buffer.from('request'));
@@ -363,7 +395,7 @@ describe('RpcPeer', () => {
363
395
  port: alicePort
364
396
  });
365
397
  const bob = new RpcPeer({
366
- messageHandler: async msg => ({}),
398
+ messageHandler: async msg => createPayload(),
367
399
  port: bobPort
368
400
  });
369
401
 
@@ -372,7 +404,7 @@ describe('RpcPeer', () => {
372
404
  bob.open()
373
405
  ]);
374
406
 
375
- const stream = await bob.callStream('method', { value: Buffer.from('request') });
407
+ const stream = await bob.callStream('method', createPayload('request'));
376
408
  expect(stream).toBeA(Stream);
377
409
 
378
410
  const msgs = await Stream.consume(stream);
@@ -388,7 +420,7 @@ describe('RpcPeer', () => {
388
420
 
389
421
  let closeCalled = false;
390
422
  const alice = new RpcPeer({
391
- messageHandler: async msg => ({}),
423
+ messageHandler: async msg => createPayload(),
392
424
  streamHandler: (method, msg) => new Stream<Any>(({ next, close }) => () => {
393
425
  closeCalled = true;
394
426
  }),
@@ -396,7 +428,7 @@ describe('RpcPeer', () => {
396
428
  });
397
429
 
398
430
  const bob = new RpcPeer({
399
- messageHandler: async msg => ({}),
431
+ messageHandler: async msg => createPayload(),
400
432
  port: bobPort
401
433
  });
402
434
 
@@ -405,13 +437,49 @@ describe('RpcPeer', () => {
405
437
  bob.open()
406
438
  ]);
407
439
 
408
- const stream = bob.callStream('method', { value: Buffer.from('request') });
440
+ const stream = bob.callStream('method', createPayload('request'));
409
441
  stream.close();
410
442
 
411
443
  await sleep(1);
412
444
 
413
445
  expect(closeCalled).toEqual(true);
414
446
  });
447
+
448
+ test('reports stream being ready', async () => {
449
+ const [alicePort, bobPort] = createLinkedPorts();
450
+
451
+ const alice = new RpcPeer({
452
+ messageHandler: async msg => createPayload(),
453
+ streamHandler: (method, msg) => {
454
+ expect(method).toEqual('method');
455
+ expect(msg.value!).toEqual(Buffer.from('request'));
456
+ return new Stream<Any>(({ ready, close }) => {
457
+ ready();
458
+ close();
459
+ });
460
+ },
461
+ port: alicePort
462
+ });
463
+ const bob = new RpcPeer({
464
+ messageHandler: async msg => createPayload(),
465
+ port: bobPort
466
+ });
467
+
468
+ await Promise.all([
469
+ alice.open(),
470
+ bob.open()
471
+ ]);
472
+
473
+ const stream = await bob.callStream('method', createPayload('request'));
474
+ expect(stream).toBeA(Stream);
475
+
476
+ await stream.waitUntilReady();
477
+
478
+ expect(await Stream.consume(stream)).toEqual([
479
+ { ready: true },
480
+ { closed: true }
481
+ ]);
482
+ });
415
483
  });
416
484
 
417
485
  describe('with disabled handshake', () => {
@@ -419,7 +487,7 @@ describe('RpcPeer', () => {
419
487
  const [alicePort] = createLinkedPorts();
420
488
 
421
489
  const alice = new RpcPeer({
422
- messageHandler: async msg => ({}),
490
+ messageHandler: async msg => createPayload(),
423
491
  port: alicePort,
424
492
  noHandshake: true
425
493
  });
@@ -434,13 +502,13 @@ describe('RpcPeer', () => {
434
502
  messageHandler: async (method, msg) => {
435
503
  expect(method).toEqual('method');
436
504
  expect(msg.value).toEqual(Buffer.from('request'));
437
- return { value: Buffer.from('response') };
505
+ return createPayload('response');
438
506
  },
439
507
  port: alicePort,
440
508
  noHandshake: true
441
509
  });
442
510
  const bob = new RpcPeer({
443
- messageHandler: async (method, msg) => ({}),
511
+ messageHandler: async (method, msg) => createPayload(),
444
512
  port: bobPort,
445
513
  noHandshake: true
446
514
  });
@@ -448,8 +516,8 @@ describe('RpcPeer', () => {
448
516
  await alice.open();
449
517
  await bob.open();
450
518
 
451
- const response = await bob.call('method', { value: Buffer.from('request') });
452
- expect(response).toEqual({ value: Buffer.from('response') });
519
+ const response = await bob.call('method', createPayload('request'));
520
+ expect(response).toEqual(createPayload('response'));
453
521
  });
454
522
  });
455
523
  });
package/src/rpc.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import debug from 'debug';
6
6
  import assert from 'node:assert';
7
7
 
8
- import { sleep, synchronized, Trigger } from '@dxos/async';
8
+ import { synchronized, Trigger } from '@dxos/async';
9
9
  import { Stream } from '@dxos/codec-protobuf';
10
10
  import { StackTrace } from '@dxos/debug';
11
11
  import { exponentialBackoffInterval } from '@dxos/util';
@@ -143,7 +143,12 @@ export class RpcPeer {
143
143
  if (decoded.request) {
144
144
  if (!this._open) {
145
145
  log('Received request while not open.');
146
- await this._sendMessage({ response: { error: encodeError(new RpcClosedError()) } });
146
+ await this._sendMessage({
147
+ response: {
148
+ id: decoded.request.id,
149
+ error: encodeError(new RpcClosedError())
150
+ }
151
+ });
147
152
  return;
148
153
  }
149
154
 
@@ -169,16 +174,18 @@ export class RpcPeer {
169
174
  return; // Ignore when not open.
170
175
  }
171
176
 
172
- assert(typeof decoded.response.id === 'number');
173
- if (!this._outgoingRequests.has(decoded.response.id)) {
174
- log(`Received response with incorrect id: ${decoded.response.id}`);
177
+ const responseId = decoded.response.id;
178
+
179
+ assert(typeof responseId === 'number');
180
+ if (!this._outgoingRequests.has(responseId)) {
181
+ log(`Received response with incorrect id: ${responseId}`);
175
182
  return; // Ignore requests with incorrect id.
176
183
  }
177
184
 
178
- const item = this._outgoingRequests.get(decoded.response.id)!;
185
+ const item = this._outgoingRequests.get(responseId)!;
179
186
  // Delete the request record if no more responses are expected.
180
187
  if (!item.stream) {
181
- this._outgoingRequests.delete(decoded.response.id);
188
+ this._outgoingRequests.delete(responseId);
182
189
  }
183
190
 
184
191
  log(`Response: ${decoded.response.payload?.type_url}`);
@@ -240,20 +247,18 @@ export class RpcPeer {
240
247
  // Here we're attaching a dummy handler that will not interfere with error handling to avoid that warning.
241
248
  promise.catch(() => {});
242
249
 
243
- await this._sendMessage({
250
+ void this._sendMessage({
244
251
  request: {
245
252
  id,
246
253
  method,
247
- payload: request
254
+ payload: request,
255
+ stream: false
248
256
  }
249
257
  });
250
258
 
251
- const timeoutPromise = sleep(this._options.timeout ?? DEFAULT_TIMEOUT).then(() => Promise.reject(new Error('Timeout')));
252
- timeoutPromise.catch(() => {}); // Mute the promise.
253
-
254
259
  let response: Response;
255
260
  try {
256
- response = await Promise.race([promise, timeoutPromise]);
261
+ response = await Promise.race([promise, createTimeoutPromise(this._options.timeout ?? DEFAULT_TIMEOUT, new Error(`RPC call timed out: ${method}`))]);
257
262
  } catch (err) {
258
263
  if (err instanceof RpcClosedError) {
259
264
  // Rethrow the error here to have the correct stack-trace.
@@ -286,9 +291,11 @@ export class RpcPeer {
286
291
 
287
292
  const id = this._nextId++;
288
293
 
289
- return new Stream(({ next, close }) => {
294
+ return new Stream(({ ready, next, close }) => {
290
295
  const onResponse = (response: Response) => {
291
- if (response.close) {
296
+ if (response.streamReady) {
297
+ ready();
298
+ } else if (response.close) {
292
299
  close();
293
300
  } else if (response.error) {
294
301
  assert(response.error.name);
@@ -362,6 +369,12 @@ export class RpcPeer {
362
369
  assert(req.payload);
363
370
  assert(req.method);
364
371
  const responseStream = this._options.streamHandler(req.method, req.payload);
372
+ responseStream.onReady(() => {
373
+ callback({
374
+ id: req.id,
375
+ streamReady: true
376
+ });
377
+ });
365
378
  responseStream.subscribe(
366
379
  msg => {
367
380
  callback({
@@ -410,3 +423,24 @@ const encodeError = (err: any): ErrorResponse => {
410
423
  };
411
424
  }
412
425
  };
426
+
427
+ /**
428
+ * Creates a promise that will be rejected after a certain timeout.
429
+ * The promise will never cause unhandledPromiseRejection.
430
+ * The timeout will not block the Node.JS process from exiting.
431
+ */
432
+ const createTimeoutPromise = (timeout: number, error: Error) => {
433
+ const timeoutPromise = new Promise<any>((resolve, reject) => {
434
+ const timeoutId = setTimeout(
435
+ () => reject(error),
436
+ timeout
437
+ );
438
+
439
+ // `unref` prevents the timeout from blocking Node.JS process from exiting. Not available in browsers.
440
+ if (typeof timeoutId === 'object' && 'unref' in timeoutId) {
441
+ timeoutId.unref();
442
+ }
443
+ });
444
+ timeoutPromise.catch(() => {}); // Mute the promise.
445
+ return timeoutPromise;
446
+ };
@@ -161,6 +161,7 @@ describe('Protobuf service', () => {
161
161
  const stream = client.rpc.testCall({ data: 'requestData' });
162
162
 
163
163
  expect(await Stream.consume(stream)).toEqual([
164
+ { ready: true },
164
165
  { data: { data: 'foo' } },
165
166
  { data: { data: 'bar' } },
166
167
  { data: { data: 'baz' } },
package/src/service.ts CHANGED
@@ -86,9 +86,12 @@ export const createBundledRpcClient = <S>(descriptors: ServiceBundle<S>, options
86
86
 
87
87
  const rpc: S = {} as S;
88
88
  for (const serviceName of Object.keys(descriptors) as (keyof S)[]) {
89
+ // Get full service name with the package name without '.' at the beginning.
90
+ const serviceFqn = descriptors[serviceName].serviceProto.fullName.slice(1);
91
+
89
92
  rpc[serviceName] = descriptors[serviceName].createClient({
90
- call: (method, req) => peer.call(`${String(serviceName)}.${method}`, req),
91
- callStream: (method, req) => peer.callStream(`${String(serviceName)}.${method}`, req)
93
+ call: (method, req) => peer.call(`${serviceFqn}.${method}`, req),
94
+ callStream: (method, req) => peer.callStream(`${serviceFqn}.${method}`, req)
92
95
  });
93
96
  }
94
97
 
@@ -106,7 +109,10 @@ export interface RpcBundledServerOptions<S> extends Omit<RpcPeerOptions, 'messag
106
109
  export const createBundledRpcServer = <S>({ services, handlers, ...rest }: RpcBundledServerOptions<S>): RpcPeer => {
107
110
  const rpc: Record<string, ServiceHandler<any>> = {};
108
111
  for (const serviceName of Object.keys(services) as (keyof S)[]) {
109
- rpc[serviceName as any] = services[serviceName].createServer(handlers[serviceName] as any);
112
+ // Get full service name with the package name without '.' at the beginning.
113
+ const serviceFqn = services[serviceName].serviceProto.fullName.slice(1);
114
+
115
+ rpc[serviceFqn] = services[serviceName].createServer(handlers[serviceName] as any);
110
116
  }
111
117
 
112
118
  const peer = new RpcPeer({
@@ -133,9 +139,11 @@ export const createBundledRpcServer = <S>({ services, handlers, ...rest }: RpcBu
133
139
  };
134
140
 
135
141
  const parseMethodName = (method: string): [serviceName: string, methodName: string] => {
136
- const [serviceName, ...rest] = method.split('.');
137
- if (rest.length === 0) {
138
- throw new Error('Method name required');
142
+ const separator = method.lastIndexOf('.');
143
+ const serviceName = method.slice(0, separator);
144
+ const methodName = method.slice(separator + 1);
145
+ if (serviceName.length === 0 || methodName.length === 0) {
146
+ throw new Error(`Invalid method: ${method}`);
139
147
  }
140
- return [serviceName, rest.join('.')];
148
+ return [serviceName, methodName];
141
149
  };