@dxos/rpc 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/rpc",
3
- "version": "0.8.4-main.9be5663bfe",
3
+ "version": "0.8.4-main.abd8ff62ef",
4
4
  "description": "A lightweight, transport-agnostic RPC implementation",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -20,22 +20,20 @@
20
20
  }
21
21
  },
22
22
  "types": "dist/types/src/index.d.ts",
23
- "typesVersions": {
24
- "*": {}
25
- },
26
23
  "files": [
27
24
  "dist",
28
25
  "src"
29
26
  ],
30
27
  "dependencies": {
31
- "@dxos/debug": "0.8.4-main.9be5663bfe",
32
- "@dxos/codec-protobuf": "0.8.4-main.9be5663bfe",
33
- "@dxos/async": "0.8.4-main.9be5663bfe",
34
- "@dxos/node-std": "0.8.4-main.9be5663bfe",
35
- "@dxos/invariant": "0.8.4-main.9be5663bfe",
36
- "@dxos/protocols": "0.8.4-main.9be5663bfe",
37
- "@dxos/log": "0.8.4-main.9be5663bfe",
38
- "@dxos/util": "0.8.4-main.9be5663bfe"
28
+ "@dxos/async": "0.8.4-main.abd8ff62ef",
29
+ "@dxos/context": "0.8.4-main.abd8ff62ef",
30
+ "@dxos/codec-protobuf": "0.8.4-main.abd8ff62ef",
31
+ "@dxos/invariant": "0.8.4-main.abd8ff62ef",
32
+ "@dxos/debug": "0.8.4-main.abd8ff62ef",
33
+ "@dxos/log": "0.8.4-main.abd8ff62ef",
34
+ "@dxos/node-std": "0.8.4-main.abd8ff62ef",
35
+ "@dxos/util": "0.8.4-main.abd8ff62ef",
36
+ "@dxos/protocols": "0.8.4-main.abd8ff62ef"
39
37
  },
40
38
  "publishConfig": {
41
39
  "access": "public"
package/src/rpc.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { Trigger, asyncTimeout, synchronized } from '@dxos/async';
6
6
  import { type Any, type ProtoCodec, type RequestOptions, Stream } from '@dxos/codec-protobuf';
7
+ import { type Context, ContextRpcCodec } from '@dxos/context';
7
8
  import { StackTrace } from '@dxos/debug';
8
9
  import { invariant } from '@dxos/invariant';
9
10
  import { log } from '@dxos/log';
@@ -14,7 +15,7 @@ import { exponentialBackoffInterval } from '@dxos/util';
14
15
 
15
16
  import { decodeRpcError } from './errors';
16
17
 
17
- const DEFAULT_TIMEOUT = 3_000;
18
+ const DEFAULT_TIMEOUT = 30_000;
18
19
  const BYE_SEND_TIMEOUT = 2_000;
19
20
 
20
21
  const DEBUG_CALLS = true;
@@ -260,7 +261,7 @@ export class RpcPeer {
260
261
  */
261
262
  private async _receive(msg: Uint8Array): Promise<void> {
262
263
  const decoded = getRpcMessageCodec().decode(msg, { preserveAny: true });
263
- DEBUG_CALLS && log('received message', { type: Object.keys(decoded)[0] });
264
+ DEBUG_CALLS && log.trace('received message', { type: Object.keys(decoded)[0] });
264
265
 
265
266
  if (decoded.request) {
266
267
  if (this._state !== RpcState.OPENED && this._state !== RpcState.OPENING) {
@@ -290,10 +291,10 @@ export class RpcPeer {
290
291
  });
291
292
  });
292
293
  } else {
293
- DEBUG_CALLS && log('requesting...', { method: req.method });
294
+ DEBUG_CALLS && log.trace('requesting...', { method: req.method });
294
295
  const response = await this._callHandler(req);
295
296
  DEBUG_CALLS &&
296
- log('sending response', {
297
+ log.trace('sending response', {
297
298
  method: req.method,
298
299
  response: response.payload?.type_url,
299
300
  error: response.error,
@@ -309,7 +310,7 @@ export class RpcPeer {
309
310
  const responseId = decoded.response.id;
310
311
  invariant(typeof responseId === 'number');
311
312
  if (!this._outgoingRequests.has(responseId)) {
312
- log('received response with invalid id', { responseId });
313
+ log.trace('received response with invalid id', { responseId });
313
314
  return; // Ignore requests with incorrect id.
314
315
  }
315
316
 
@@ -319,7 +320,7 @@ export class RpcPeer {
319
320
  this._outgoingRequests.delete(responseId);
320
321
  }
321
322
 
322
- DEBUG_CALLS && log('response', { type_url: decoded.response.payload?.type_url });
323
+ DEBUG_CALLS && log.trace('response', { type_url: decoded.response.payload?.type_url });
323
324
  item.resolve(decoded.response);
324
325
  } else if (decoded.open) {
325
326
  log('received open message', { state: this._state });
@@ -374,7 +375,7 @@ export class RpcPeer {
374
375
  * Peer should be open before making this call.
375
376
  */
376
377
  async call(method: string, request: Any, options?: RequestOptions): Promise<Any> {
377
- DEBUG_CALLS && log('calling...', { method });
378
+ DEBUG_CALLS && log.trace('calling...', { method });
378
379
  throwIfNotOpen(this._state);
379
380
 
380
381
  let response: Response;
@@ -385,6 +386,13 @@ export class RpcPeer {
385
386
  this._outgoingRequests.set(id, new PendingRpcRequest(resolve, reject, false));
386
387
  });
387
388
 
389
+ let traceContext;
390
+ try {
391
+ traceContext = options?.ctx ? ContextRpcCodec.encode(options.ctx) : undefined;
392
+ } catch (err) {
393
+ log.warn('failed to encode trace context', { err });
394
+ }
395
+
388
396
  // Send request call.
389
397
  const sending = this._sendMessage({
390
398
  request: {
@@ -392,6 +400,7 @@ export class RpcPeer {
392
400
  method,
393
401
  payload: request,
394
402
  stream: false,
403
+ ...(traceContext ? { traceContext } : {}),
395
404
  },
396
405
  });
397
406
 
@@ -460,16 +469,30 @@ export class RpcPeer {
460
469
 
461
470
  this._outgoingRequests.set(id, new PendingRpcRequest(onResponse, closeStream, true));
462
471
 
463
- this._sendMessage({
464
- request: {
465
- id,
466
- method,
467
- payload: request,
468
- stream: true,
469
- },
470
- }).catch((err) => {
471
- close(err);
472
- });
472
+ let traceContext;
473
+ try {
474
+ traceContext = options?.ctx ? ContextRpcCodec.encode(options.ctx) : undefined;
475
+ } catch (err) {
476
+ log.warn('failed to encode trace context', { err });
477
+ }
478
+
479
+ try {
480
+ this._sendMessage({
481
+ request: {
482
+ id,
483
+ method,
484
+ payload: request,
485
+ stream: true,
486
+ ...(traceContext ? { traceContext } : {}),
487
+ },
488
+ }).catch((err) => {
489
+ this._outgoingRequests.delete(id);
490
+ close(err);
491
+ });
492
+ } catch (err) {
493
+ this._outgoingRequests.delete(id);
494
+ throw err;
495
+ }
473
496
 
474
497
  return () => {
475
498
  this._sendMessage({
@@ -483,17 +506,32 @@ export class RpcPeer {
483
506
  }
484
507
 
485
508
  private async _sendMessage(message: RpcMessage, timeout?: number): Promise<void> {
486
- DEBUG_CALLS && log('sending message', { type: Object.keys(message)[0] });
509
+ DEBUG_CALLS && log.trace('sending message', { type: Object.keys(message)[0] });
487
510
  await this._params.port.send(getRpcMessageCodec().encode(message, { preserveAny: true }), timeout);
488
511
  }
489
512
 
513
+ private _getHandlerRpcOptions(req: Request): RequestOptions | undefined {
514
+ let traceCtx: Context | undefined;
515
+ if (req.traceContext) {
516
+ try {
517
+ traceCtx = ContextRpcCodec.decode(req.traceContext);
518
+ } catch (err) {
519
+ log.warn('failed to decode trace context', { traceContext: req.traceContext, err });
520
+ }
521
+ }
522
+ if (!traceCtx && !this._params.handlerRpcOptions) {
523
+ return undefined;
524
+ }
525
+ return { ...this._params.handlerRpcOptions, ...(traceCtx ? { ctx: traceCtx } : {}) };
526
+ }
527
+
490
528
  private async _callHandler(req: Request): Promise<Response> {
491
529
  try {
492
530
  invariant(typeof req.id === 'number');
493
531
  invariant(req.payload);
494
532
  invariant(req.method);
495
533
 
496
- const response = await this._params.callHandler(req.method, req.payload, this._params.handlerRpcOptions);
534
+ const response = await this._params.callHandler(req.method, req.payload, this._getHandlerRpcOptions(req));
497
535
  return {
498
536
  id: req.id,
499
537
  payload: response,
@@ -513,7 +551,7 @@ export class RpcPeer {
513
551
  invariant(req.payload);
514
552
  invariant(req.method);
515
553
 
516
- const responseStream = this._params.streamHandler(req.method, req.payload, this._params.handlerRpcOptions);
554
+ const responseStream = this._params.streamHandler(req.method, req.payload, this._getHandlerRpcOptions(req));
517
555
  responseStream.onReady(() => {
518
556
  callback({
519
557
  id: req.id,
@@ -5,7 +5,8 @@
5
5
  import { beforeEach, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { latch, sleep } from '@dxos/async';
8
- import { Stream } from '@dxos/codec-protobuf/stream';
8
+ import { type RequestOptions, Stream } from '@dxos/codec-protobuf';
9
+ import { Context, TRACE_SPAN_ATTRIBUTE } from '@dxos/context';
9
10
  import { schema } from '@dxos/protocols/proto';
10
11
  import {
11
12
  type TestRpcResponse,
@@ -584,4 +585,127 @@ describe('Protobuf service', () => {
584
585
  );
585
586
  await expect(promise).rejects.toThrow(/Timeout/);
586
587
  });
588
+
589
+ test('W3C trace context propagates to handler options', async ({ expect }) => {
590
+ const traceContext = {
591
+ traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
592
+ tracestate: 'vendorkey=vendorvalue',
593
+ };
594
+
595
+ const [alicePort, bobPort] = createLinkedPorts();
596
+ let receivedCtx: Context | undefined;
597
+
598
+ const server = createProtoRpcPeer({
599
+ exposed: {
600
+ TestService: schema.getService('example.testing.rpc.TestService'),
601
+ },
602
+ handlers: {
603
+ TestService: {
604
+ testCall: async (req: any, options?: RequestOptions) => {
605
+ receivedCtx = options?.ctx;
606
+ return { data: 'responseData' };
607
+ },
608
+ voidCall: async () => {},
609
+ },
610
+ },
611
+ port: alicePort,
612
+ });
613
+
614
+ const callerCtx = new Context({ attributes: { [TRACE_SPAN_ATTRIBUTE]: traceContext } });
615
+
616
+ const client = createProtoRpcPeer({
617
+ requested: {
618
+ TestService: schema.getService('example.testing.rpc.TestService'),
619
+ },
620
+ port: bobPort,
621
+ });
622
+
623
+ await Promise.all([server.open(), client.open()]);
624
+
625
+ await client.rpc.TestService.testCall({ data: 'requestData' }, { ctx: callerCtx });
626
+
627
+ expect(receivedCtx).toBeInstanceOf(Context);
628
+ const received = receivedCtx!.getAttribute(TRACE_SPAN_ATTRIBUTE);
629
+ expect(received.traceparent).toEqual(traceContext.traceparent);
630
+ expect(received.tracestate).toEqual(traceContext.tracestate);
631
+ });
632
+
633
+ test('handler receives no ctx when caller sends no trace context', async ({ expect }) => {
634
+ const [alicePort, bobPort] = createLinkedPorts();
635
+ let receivedOptions: RequestOptions | undefined;
636
+
637
+ const server = createProtoRpcPeer({
638
+ exposed: {
639
+ TestService: schema.getService('example.testing.rpc.TestService'),
640
+ },
641
+ handlers: {
642
+ TestService: {
643
+ testCall: async (req: any, options?: RequestOptions) => {
644
+ receivedOptions = options;
645
+ return { data: 'responseData' };
646
+ },
647
+ voidCall: async () => {},
648
+ },
649
+ },
650
+ port: alicePort,
651
+ });
652
+
653
+ const client = createProtoRpcPeer({
654
+ requested: {
655
+ TestService: schema.getService('example.testing.rpc.TestService'),
656
+ },
657
+ port: bobPort,
658
+ });
659
+
660
+ await Promise.all([server.open(), client.open()]);
661
+
662
+ await client.rpc.TestService.testCall({ data: 'requestData' });
663
+
664
+ expect(receivedOptions).toBeUndefined();
665
+ });
666
+
667
+ test('W3C trace context propagates on streaming RPC', async ({ expect }) => {
668
+ const traceContext = {
669
+ traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
670
+ };
671
+
672
+ const [alicePort, bobPort] = createLinkedPorts();
673
+ let receivedCtx: Context | undefined;
674
+
675
+ const server = createProtoRpcPeer({
676
+ exposed: {
677
+ TestStreamService: schema.getService('example.testing.rpc.TestStreamService'),
678
+ },
679
+ handlers: {
680
+ TestStreamService: {
681
+ testCall: (_req: any, options?: RequestOptions) => {
682
+ receivedCtx = options?.ctx;
683
+ return new Stream<{ data: string }>(({ next, close }) => {
684
+ next({ data: 'streamData' });
685
+ close();
686
+ });
687
+ },
688
+ },
689
+ },
690
+ port: alicePort,
691
+ });
692
+
693
+ const callerCtx = new Context({ attributes: { [TRACE_SPAN_ATTRIBUTE]: traceContext } });
694
+
695
+ const client = createProtoRpcPeer({
696
+ requested: {
697
+ TestStreamService: schema.getService('example.testing.rpc.TestStreamService'),
698
+ },
699
+ port: bobPort,
700
+ });
701
+
702
+ await Promise.all([server.open(), client.open()]);
703
+
704
+ const stream = client.rpc.TestStreamService.testCall({ data: 'requestData' }, { ctx: callerCtx });
705
+ await Stream.consumeData(stream);
706
+
707
+ expect(receivedCtx).toBeInstanceOf(Context);
708
+ const received = receivedCtx!.getAttribute(TRACE_SPAN_ATTRIBUTE);
709
+ expect(received.traceparent).toEqual(traceContext.traceparent);
710
+ });
587
711
  });