starpc 0.49.20 → 0.51.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.
Files changed (78) hide show
  1. package/dist/integration/cross-language/ts-client.js +5 -79
  2. package/dist/integration/cross-language/ts-server.js +4 -84
  3. package/dist/mock/index.d.ts +2 -2
  4. package/dist/mock/index.js +2 -2
  5. package/dist/srpc/client.d.ts +1 -3
  6. package/dist/srpc/client.js +2 -22
  7. package/dist/srpc/common-rpc.d.ts +1 -11
  8. package/dist/srpc/common-rpc.js +5 -66
  9. package/dist/srpc/handler.d.ts +1 -2
  10. package/dist/srpc/index.d.ts +0 -4
  11. package/dist/srpc/index.js +0 -2
  12. package/dist/srpc/invoker.d.ts +1 -2
  13. package/dist/srpc/server-rpc.js +1 -3
  14. package/dist/srpc/server.test.js +35 -1
  15. package/echo/echo.go +8 -8
  16. package/echo/echo.pb.go +12 -36
  17. package/echo/echo_srpc.pb.cpp +1 -1
  18. package/echo/echo_srpc.pb.go +1 -1
  19. package/echo/echo_srpc.pb.hpp +1 -1
  20. package/echo/echo_srpc.pb.rs +1 -1
  21. package/echo/server.go +1 -1
  22. package/go.mod +13 -12
  23. package/go.sum +26 -24
  24. package/integration/cross-language/go-client/main.go +4 -142
  25. package/integration/cross-language/go-server/main.go +3 -149
  26. package/integration/cross-language/run.bash +5 -105
  27. package/integration/cross-language/ts-client.ts +5 -90
  28. package/integration/cross-language/ts-server.ts +6 -94
  29. package/mock/index.ts +2 -2
  30. package/mock/mock.go +1 -1
  31. package/mock/mock.pb.go +12 -36
  32. package/mock/mock_srpc.pb.cpp +1 -1
  33. package/mock/mock_srpc.pb.go +1 -1
  34. package/mock/mock_srpc.pb.hpp +1 -1
  35. package/mock/mock_srpc.pb.rs +1 -1
  36. package/package.json +19 -26
  37. package/srpc/accept.go +1 -1
  38. package/srpc/client-prefix.go +1 -1
  39. package/srpc/client-set.go +1 -1
  40. package/srpc/client.go +1 -1
  41. package/srpc/client.ts +4 -31
  42. package/srpc/common-rpc.go +15 -69
  43. package/srpc/common-rpc.ts +6 -78
  44. package/srpc/common-rpc_test.go +81 -0
  45. package/srpc/errors.go +26 -1
  46. package/srpc/handler.ts +1 -2
  47. package/srpc/index.ts +0 -4
  48. package/srpc/invoker-prefix.go +1 -1
  49. package/srpc/invoker.go +2 -2
  50. package/srpc/invoker.ts +5 -9
  51. package/srpc/message.go +1 -1
  52. package/srpc/msg-stream.go +1 -9
  53. package/srpc/mux-verbose.go +1 -1
  54. package/srpc/mux.go +1 -1
  55. package/srpc/muxed-conn.go +1 -1
  56. package/srpc/muxed-yamux.go +1 -1
  57. package/srpc/rpcproto.pb.go +102 -323
  58. package/srpc/server-rpc.go +1 -1
  59. package/srpc/server-rpc.ts +1 -6
  60. package/srpc/server.test.ts +49 -1
  61. package/srpc/stream-pipe.go +1 -1
  62. package/srpc/stream-rwc.go +1 -1
  63. package/srpc/stream-yamux.go +1 -1
  64. package/srpc/stream.go +0 -20
  65. package/srpc/websocket.go +1 -1
  66. package/dist/srpc/call-receipt.d.ts +0 -17
  67. package/dist/srpc/call-receipt.js +0 -105
  68. package/dist/srpc/call-receipt.test.d.ts +0 -1
  69. package/dist/srpc/call-receipt.test.js +0 -374
  70. package/dist/srpc/server-invocation.d.ts +0 -17
  71. package/dist/srpc/server-invocation.js +0 -37
  72. package/srpc/call-receipt-e2e_test.go +0 -111
  73. package/srpc/call-receipt.go +0 -112
  74. package/srpc/call-receipt.test.ts +0 -438
  75. package/srpc/call-receipt.ts +0 -130
  76. package/srpc/call-receipt_test.go +0 -536
  77. package/srpc/server-invocation.go +0 -58
  78. package/srpc/server-invocation.ts +0 -82
@@ -1,25 +1,10 @@
1
1
  import net from 'net';
2
- import { closeSync, openSync, writeSync } from 'node:fs';
3
2
  import { pipe } from 'it-pipe';
4
3
  import { pushable } from 'it-pushable';
5
4
  import { Client } from '../../srpc/client.js';
6
5
  import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
7
6
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
8
- import { EchoMsg, runClientTest } from '../../echo/index.js';
9
- function emitReceiptEvent(line) {
10
- console.log(line);
11
- const fifo = process.env.RECEIPT_EVENT_FIFO;
12
- if (!fifo) {
13
- return;
14
- }
15
- const fd = openSync(fifo, 'w');
16
- try {
17
- writeSync(fd, `${line}\n`);
18
- }
19
- finally {
20
- closeSync(fd);
21
- }
22
- }
7
+ import { runClientTest } from '../../echo/index.js';
23
8
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
24
9
  function tcpSocketToPacketStream(socket) {
25
10
  const socketSource = async function* () {
@@ -51,85 +36,26 @@ function tcpSocketToPacketStream(socket) {
51
36
  };
52
37
  }
53
38
  async function main() {
54
- const receiptMode = process.argv[2] === 'receipt';
55
- const receiptCase = receiptMode ? process.argv[3] : undefined;
56
- const addr = receiptMode ? process.argv[4] : process.argv[2];
39
+ const addr = process.argv[2];
57
40
  if (!addr) {
58
- console.error('usage: ts-client [receipt <case>] <host:port>');
41
+ console.error('usage: ts-client <host:port>');
59
42
  process.exit(1);
60
43
  }
61
- if (receiptMode &&
62
- !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')) {
63
- throw new Error(`unknown receipt case: ${receiptCase}`);
64
- }
65
44
  const [host, portStr] = addr.split(':');
66
45
  const port = parseInt(portStr, 10);
67
- let activeSocket;
68
46
  const openStream = async () => {
69
47
  const { promise, resolve, reject } = Promise.withResolvers();
70
48
  const socket = net.connect(port, host, () => {
71
- activeSocket = socket;
72
49
  resolve(tcpSocketToPacketStream(socket));
73
50
  });
74
51
  socket.on('error', reject);
75
52
  return promise;
76
53
  };
77
54
  const client = new Client(openStream);
78
- if (receiptMode) {
79
- console.log(`Running held receipt test via TCP (${receiptCase})...`);
80
- const request = EchoMsg.create({ body: 'held receipt' });
81
- const held = await client.requestWithReceipt('echo.Echoer', 'Echo', EchoMsg.toBinary(request));
82
- const result = EchoMsg.fromBinary(held.response);
83
- if (result.body !== request.body) {
84
- throw new Error(`expected ${request.body}, got ${result.body}`);
85
- }
86
- switch (receiptCase) {
87
- case 'commit':
88
- await held.receipt.commit();
89
- break;
90
- case 'abort':
91
- await held.receipt.abort();
92
- break;
93
- case 'loss':
94
- activeSocket?.resetAndDestroy();
95
- await expectReceiptFailure(held.receipt);
96
- break;
97
- case 'bare-close':
98
- activeSocket?.end();
99
- await expectReceiptFailure(held.receipt);
100
- break;
101
- }
102
- emitReceiptEvent(`CLIENT_RECEIPT_RESOLVED ${receiptTerminalName(receiptCase ?? '')}`);
103
- }
104
- else {
105
- console.log('Running client test via TCP...');
106
- await runClientTest(client);
107
- }
55
+ console.log('Running client test via TCP...');
56
+ await runClientTest(client);
108
57
  console.log('All tests passed.');
109
58
  }
110
- function receiptTerminalName(receiptCase) {
111
- switch (receiptCase) {
112
- case 'commit':
113
- return 'committed';
114
- case 'abort':
115
- return 'canceled';
116
- case 'loss':
117
- return 'transportLost';
118
- case 'bare-close':
119
- return 'closed';
120
- default:
121
- return 'unknown';
122
- }
123
- }
124
- async function expectReceiptFailure(receipt) {
125
- try {
126
- await receipt.commit();
127
- }
128
- catch {
129
- return;
130
- }
131
- throw new Error('receipt commit unexpectedly succeeded');
132
- }
133
59
  process.on('unhandledRejection', (ev) => {
134
60
  console.error('Unhandled rejection', ev);
135
61
  process.exit(1);
@@ -1,27 +1,11 @@
1
1
  import net from 'net';
2
- import { closeSync, openSync, writeSync } from 'node:fs';
3
2
  import { pipe } from 'it-pipe';
4
3
  import { pushable } from 'it-pushable';
5
4
  import { createMux, createHandler, Server } from '../../srpc/index.js';
6
5
  import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
7
6
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
8
- import { EchoerServer, EchoMsg } from '../../echo/index.js';
7
+ import { EchoerServer } from '../../echo/index.js';
9
8
  import { EchoerDefinition } from '../../echo/echo_srpc.pb.js';
10
- import { Packet } from '../../srpc/rpcproto.pb.js';
11
- function emitReceiptEvent(line) {
12
- console.log(line);
13
- const fifo = process.env.RECEIPT_EVENT_FIFO;
14
- if (!fifo) {
15
- return;
16
- }
17
- const fd = openSync(fifo, 'w');
18
- try {
19
- writeSync(fd, `${line}\n`);
20
- }
21
- finally {
22
- closeSync(fd);
23
- }
24
- }
25
9
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
26
10
  // Each Uint8Array in source/sink is one packet (no length prefix).
27
11
  function tcpSocketToPacketStream(socket) {
@@ -41,12 +25,7 @@ function tcpSocketToPacketStream(socket) {
41
25
  sink: async (source) => {
42
26
  for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
43
27
  const data = chunk instanceof Uint8Array ? chunk : chunk.subarray();
44
- const packet = Packet.fromBinary(data.subarray(4));
45
- const receiptCompletion = receiptMode &&
46
- packet.body?.case === 'callData' &&
47
- packet.body.value.complete &&
48
- !packet.body.value.error;
49
- const writeDone = new Promise((resolve, reject) => {
28
+ await new Promise((resolve, reject) => {
50
29
  socket.write(data, (err) => {
51
30
  if (err)
52
31
  reject(err);
@@ -54,69 +33,15 @@ function tcpSocketToPacketStream(socket) {
54
33
  resolve();
55
34
  });
56
35
  });
57
- if (receiptCompletion) {
58
- emitReceiptEvent('SERVER_RECEIPT_ACK committed');
59
- }
60
- await writeDone;
61
- if (receiptCompletion) {
62
- finishReceiptServer();
63
- }
64
36
  }
65
37
  socket.end();
66
38
  },
67
39
  };
68
40
  }
69
41
  const mux = createMux();
70
- const receiptMode = process.argv[2] === 'receipt';
71
- const receiptCase = receiptMode ? process.argv[3] : undefined;
72
- if (receiptMode &&
73
- !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')) {
74
- console.error(`unknown receipt case: ${receiptCase}`);
75
- process.exit(1);
76
- }
77
- const receiptServerDone = Promise.withResolvers();
78
- let receiptServerFinished = false;
79
- function finishReceiptServer() {
80
- if (!receiptServerFinished) {
81
- receiptServerFinished = true;
82
- receiptServerDone.resolve();
83
- }
84
- }
85
- if (receiptMode) {
86
- mux.registerLookupMethod(async (serviceID, methodID) => {
87
- if (serviceID !== 'echo.Echoer' || methodID !== 'Echo') {
88
- return null;
89
- }
90
- return async (dataSource, dataSink, invocation) => {
91
- let requestData;
92
- for await (const data of dataSource) {
93
- requestData = data;
94
- break;
95
- }
96
- if (!requestData) {
97
- throw new Error('receipt request was empty');
98
- }
99
- const requestMsg = EchoMsg.fromBinary(requestData);
100
- await dataSink((async function* () {
101
- yield EchoMsg.toBinary(requestMsg);
102
- if (!invocation) {
103
- throw new Error('receipt invocation was missing');
104
- }
105
- emitReceiptEvent('SERVER_RECEIPT_WAITING');
106
- const terminal = await invocation.waitTerminal(new AbortController().signal);
107
- emitReceiptEvent(`SERVER_RECEIPT_TERMINAL ${terminal}`);
108
- if (terminal !== 'committed') {
109
- finishReceiptServer();
110
- }
111
- })());
112
- };
113
- });
114
- }
115
42
  const server = new Server(mux.lookupMethod);
116
- if (!receiptMode) {
117
- const echoer = new EchoerServer(server);
118
- mux.register(createHandler(EchoerDefinition, echoer));
119
- }
43
+ const echoer = new EchoerServer(server);
44
+ mux.register(createHandler(EchoerDefinition, echoer));
120
45
  const tcpServer = net.createServer((socket) => {
121
46
  const stream = tcpSocketToPacketStream(socket);
122
47
  server.handlePacketStream(stream);
@@ -125,11 +50,6 @@ tcpServer.listen(0, '127.0.0.1', () => {
125
50
  const addr = tcpServer.address();
126
51
  console.log(`LISTENING ${addr.address}:${addr.port}`);
127
52
  });
128
- if (receiptMode) {
129
- void receiptServerDone.promise.then(() => {
130
- tcpServer.close();
131
- });
132
- }
133
53
  process.on('SIGINT', () => {
134
54
  tcpServer.close();
135
55
  process.exit(0);
@@ -1,2 +1,2 @@
1
- export * from "./mock.pb.js";
2
- export * from "./mock_srpc.pb.js";
1
+ export * from './mock.pb.js';
2
+ export * from './mock_srpc.pb.js';
@@ -1,2 +1,2 @@
1
- export * from "./mock.pb.js";
2
- export * from "./mock_srpc.pb.js";
1
+ export * from './mock.pb.js';
2
+ export * from './mock_srpc.pb.js';
@@ -1,12 +1,10 @@
1
1
  import type { ProtoRpc } from './proto-rpc.js';
2
2
  import type { OpenStreamFunc } from './stream.js';
3
- import type { HeldCall, ReceiptRpc } from './call-receipt.js';
4
- export declare class Client implements ProtoRpc, ReceiptRpc {
3
+ export declare class Client implements ProtoRpc {
5
4
  private openStreamCtr;
6
5
  constructor(openStreamFn?: OpenStreamFunc);
7
6
  setOpenStreamFn(openStreamFn?: OpenStreamFunc): void;
8
7
  request(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<Uint8Array>;
9
- requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
10
8
  clientStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): Promise<Uint8Array>;
11
9
  serverStreamingRequest(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
12
10
  bidirectionalStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
@@ -5,7 +5,6 @@ import { ClientRPC } from './client-rpc.js';
5
5
  import { writeToPushable } from './pushable.js';
6
6
  import { decodePacketSource, encodePacketSource } from './packet.js';
7
7
  import { OpenStreamCtr } from './open-stream-ctr.js';
8
- import { CallReceipt } from './call-receipt.js';
9
8
  // Client implements the ts-proto Rpc interface with the drpcproto protocol.
10
9
  export class Client {
11
10
  // openStreamCtr contains the OpenStreamFunc.
@@ -28,25 +27,6 @@ export class Client {
28
27
  call.close(err);
29
28
  throw err;
30
29
  }
31
- // requestWithReceipt reads one response and retains the call terminal.
32
- async requestWithReceipt(service, method, data, abortSignal) {
33
- const call = await this.startRpc(service, method, data, abortSignal);
34
- const iterator = call.rpcDataSource[Symbol.asyncIterator]();
35
- try {
36
- const result = await iterator.next();
37
- if (result.done) {
38
- throw new Error('empty response');
39
- }
40
- return {
41
- response: result.value,
42
- receipt: new CallReceipt(call, iterator),
43
- };
44
- }
45
- catch (err) {
46
- await call.close(err instanceof Error ? err : new Error('receipt read failed'));
47
- throw err;
48
- }
49
- }
50
30
  // clientStreamingRequest starts a client side streaming request.
51
31
  async clientStreamingRequest(service, method, data, abortSignal) {
52
32
  const call = await this.startRpc(service, method, null, abortSignal);
@@ -102,8 +82,8 @@ export class Client {
102
82
  const stream = await openStreamFn();
103
83
  const call = new ClientRPC(rpcService, rpcMethod);
104
84
  const onAbort = () => {
105
- call.writeCallCancel();
106
- call.close(new Error(ERR_RPC_ABORT));
85
+ void call.writeCallCancel().catch(() => undefined);
86
+ void call.close(new Error(ERR_RPC_ABORT)).catch(() => undefined);
107
87
  };
108
88
  abortSignal?.addEventListener('abort', onAbort, { once: true });
109
89
  pipe(stream, decodePacketSource, call, encodePacketSource, stream)
@@ -1,7 +1,5 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
- import type { CallData, CallStart } from './rpcproto.pb.js';
3
- import { Packet } from './rpcproto.pb.js';
4
- import type { TerminalKind } from './server-invocation.js';
2
+ import { Packet, type CallData, type CallStart } from './rpcproto.pb.js';
5
3
  export declare class CommonRPC {
6
4
  readonly sink: Sink<Source<Packet>>;
7
5
  readonly source: AsyncIterable<Packet>;
@@ -11,20 +9,12 @@ export declare class CommonRPC {
11
9
  protected service?: string;
12
10
  protected method?: string;
13
11
  private closed?;
14
- private remoteCompleted;
15
12
  private remoteError?;
16
- private remoteSourceClosed;
17
- private remoteTerminal?;
18
13
  private readonly invocationController;
19
- private readonly terminalPromise;
20
- private resolveTerminal;
21
14
  private readonly writeDrainAbort;
22
15
  constructor();
23
16
  get isClosed(): boolean | Error;
24
17
  protected get invocationSignal(): AbortSignal;
25
- protected waitTerminal(ownerSignal: AbortSignal): Promise<TerminalKind>;
26
- getTerminalKind(): TerminalKind | undefined;
27
- private recordRemoteTerminal;
28
18
  writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
29
19
  private writeCallDataPacket;
30
20
  writeCallCancel(waitForDrain?: boolean): Promise<void>;
@@ -24,25 +24,13 @@ export class CommonRPC {
24
24
  method;
25
25
  // closed indicates this rpc has been closed already.
26
26
  closed;
27
- // remoteCompleted is set only by an explicit remote CallData completion.
28
- remoteCompleted = false;
29
27
  // remoteError records a remote error or transport failure.
30
28
  remoteError;
31
- // remoteSourceClosed records an incoming source ending without a packet error.
32
- remoteSourceClosed = false;
33
- // remoteTerminal is the first valid remote terminal.
34
- remoteTerminal;
35
- // invocationController cancels the server invocation on a remote terminal.
29
+ // invocationController cancels the server invocation when the RPC closes.
36
30
  invocationController = new AbortController();
37
- // terminalPromise resolves when a remote terminal is recorded.
38
- terminalPromise;
39
- resolveTerminal;
40
31
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
41
32
  writeDrainAbort = new AbortController();
42
33
  constructor() {
43
- const { promise, resolve } = Promise.withResolvers();
44
- this.terminalPromise = promise;
45
- this.resolveTerminal = resolve;
46
34
  this.sink = this._createSink();
47
35
  this.source = this._source;
48
36
  this.rpcDataSource = this._rpcDataSource;
@@ -51,49 +39,10 @@ export class CommonRPC {
51
39
  get isClosed() {
52
40
  return this.closed ?? false;
53
41
  }
54
- // invocationSignal is canceled when the RPC reaches a terminal.
42
+ // invocationSignal is canceled when the RPC closes.
55
43
  get invocationSignal() {
56
44
  return this.invocationController.signal;
57
45
  }
58
- // waitTerminal waits for the remote terminal or external owner cancellation.
59
- async waitTerminal(ownerSignal) {
60
- const { promise: ownerDone, resolve: resolveOwnerDone } = Promise.withResolvers();
61
- const onAbort = () => resolveOwnerDone();
62
- ownerSignal.addEventListener('abort', onAbort, { once: true });
63
- let ownerAborted = ownerSignal.aborted;
64
- try {
65
- for (;;) {
66
- const terminal = this.getTerminalKind();
67
- if (terminal !== undefined) {
68
- if (terminal === 'closed' &&
69
- this.remoteSourceClosed &&
70
- !this.closed) {
71
- await this.close();
72
- }
73
- return terminal;
74
- }
75
- if (ownerAborted) {
76
- return 'abandoned';
77
- }
78
- await Promise.race([this.terminalPromise, ownerDone]);
79
- ownerAborted = ownerSignal.aborted;
80
- }
81
- }
82
- finally {
83
- ownerSignal.removeEventListener('abort', onAbort);
84
- }
85
- }
86
- // getTerminalKind returns the observed remote terminal, if any.
87
- getTerminalKind() {
88
- return this.remoteTerminal;
89
- }
90
- recordRemoteTerminal(kind) {
91
- if (this.remoteTerminal !== undefined) {
92
- return;
93
- }
94
- this.remoteTerminal = kind;
95
- this.resolveTerminal();
96
- }
97
46
  // writeCallData writes the call data packet.
98
47
  async writeCallData(data, complete, error) {
99
48
  await this.writeCallDataPacket(data, complete, error);
@@ -215,26 +164,22 @@ export class CommonRPC {
215
164
  throw new Error('call start must be sent before call data');
216
165
  }
217
166
  this.pushRpcData(packet.data, packet.dataIsZero);
218
- const remoteError = packet.error ?
219
- new RemoteRPCError(this.service, this.method, packet.error)
167
+ const remoteError = packet.error
168
+ ? new RemoteRPCError(this.service, this.method, packet.error)
220
169
  : undefined;
221
170
  if (remoteError) {
222
171
  this.remoteError ??= remoteError;
223
172
  this.invocationController.abort();
224
- this.recordRemoteTerminal('transportLost');
225
173
  }
226
174
  if (packet.complete && !remoteError) {
227
- this.remoteCompleted = true;
228
- this.recordRemoteTerminal('committed');
229
175
  this._rpcDataSource.end(remoteError);
230
176
  }
231
177
  else if (remoteError) {
232
178
  this._rpcDataSource.end(remoteError);
233
179
  }
234
180
  }
235
- // handleCallCancel handles a CallCancel packet.
181
+ // handleCallCancel aborts the invocation and closes the call.
236
182
  async handleCallCancel() {
237
- this.recordRemoteTerminal('canceled');
238
183
  await this.close(new Error(ERR_RPC_ABORT));
239
184
  }
240
185
  // close closes the call, optionally with an error.
@@ -246,7 +191,6 @@ export class CommonRPC {
246
191
  if (!this.remoteError && err) {
247
192
  this.remoteError = err;
248
193
  }
249
- this.recordRemoteTerminal(err ? 'transportLost' : 'closed');
250
194
  this.invocationController.abort();
251
195
  // note: this does nothing if _source is already ended.
252
196
  if (err && err.message) {
@@ -259,24 +203,19 @@ export class CommonRPC {
259
203
  this._source.end();
260
204
  this._rpcDataSource.end(err);
261
205
  }
262
- // _createSink returns a value for the sink field.
263
206
  _createSink() {
264
207
  return async (source) => {
265
208
  try {
266
209
  if (Symbol.asyncIterator in source) {
267
- // Handle async source
268
210
  for await (const msg of source) {
269
211
  await this.handlePacket(msg);
270
212
  }
271
213
  }
272
214
  else {
273
- // Handle sync source
274
215
  for (const msg of source) {
275
216
  await this.handlePacket(msg);
276
217
  }
277
218
  }
278
- this.remoteSourceClosed = true;
279
- this.recordRemoteTerminal('closed');
280
219
  }
281
220
  catch (err) {
282
221
  this.close(err);
@@ -1,7 +1,6 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
2
  import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
3
- import type { ServerInvocation } from './server-invocation.js';
4
- export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: ServerInvocation) => Promise<void>;
3
+ export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: AbortSignal) => Promise<void>;
5
4
  export interface Handler {
6
5
  getServiceID(): string;
7
6
  getMethodIDs(): string[];
@@ -1,9 +1,5 @@
1
1
  export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
2
2
  export { Client } from './client.js';
3
- export { CallReceipt } from './call-receipt.js';
4
- export type { HeldCall, ReceiptRpc } from './call-receipt.js';
5
- export { ServerInvocation } from './server-invocation.js';
6
- export type { TerminalKind } from './server-invocation.js';
7
3
  export { Server } from './server.js';
8
4
  export { StreamConn } from './conn.js';
9
5
  export type { StreamConnParams, StreamHandler } from './conn.js';
@@ -1,7 +1,5 @@
1
1
  export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
2
2
  export { Client } from './client.js';
3
- export { CallReceipt } from './call-receipt.js';
4
- export { ServerInvocation } from './server-invocation.js';
5
3
  export { Server } from './server.js';
6
4
  export { StreamConn } from './conn.js';
7
5
  export { WebSocketConn } from './websocket.js';
@@ -2,6 +2,5 @@ import type { MethodDefinition } from './definition.js';
2
2
  import { InvokeFn } from './handler.js';
3
3
  import type { MessageType, Message } from '@aptre/protobuf-es-lite';
4
4
  import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite';
5
- import type { ServerInvocation } from './server-invocation.js';
6
- export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, invocation?: ServerInvocation) => Promise<O>) | ((request: R, invocation?: ServerInvocation) => AsyncIterable<O>) | ((request: AsyncIterable<R>, invocation?: ServerInvocation) => Promise<O>) | ((request: AsyncIterable<R>, invocation?: ServerInvocation) => AsyncIterable<O>);
5
+ export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, invocation?: AbortSignal) => Promise<O>) | ((request: R, invocation?: AbortSignal) => AsyncIterable<O>) | ((request: AsyncIterable<R>, invocation?: AbortSignal) => Promise<O>) | ((request: AsyncIterable<R>, invocation?: AbortSignal) => AsyncIterable<O>);
7
6
  export declare function createInvokeFn<R extends Message<R>, O extends Message<O>>(methodInfo: MethodDefinition<MessageType<R>, MessageType<O>, MethodKind, MethodIdempotency | undefined>, methodProto: MethodProto<R, O>): InvokeFn;
@@ -1,5 +1,4 @@
1
1
  import { CommonRPC } from './common-rpc.js';
2
- import { ServerInvocation } from './server-invocation.js';
3
2
  // ServerRPC is an ongoing RPC from the server side.
4
3
  export class ServerRPC extends CommonRPC {
5
4
  // lookupMethod looks up the incoming RPC methods.
@@ -38,9 +37,8 @@ export class ServerRPC extends CommonRPC {
38
37
  // invokeRPC starts invoking the RPC handler.
39
38
  async invokeRPC(invokeFn) {
40
39
  const dataSink = this._createDataSink();
41
- const invocation = new ServerInvocation(this.invocationSignal, (ownerSignal) => this.waitTerminal(ownerSignal));
42
40
  try {
43
- await invokeFn(this.rpcDataSource, dataSink, invocation);
41
+ await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal);
44
42
  }
45
43
  catch (err) {
46
44
  this.close(err);
@@ -1,7 +1,7 @@
1
1
  import { describe, it, beforeEach, expect, vi } from 'vitest';
2
2
  import { pipe } from 'it-pipe';
3
3
  import { createHandler, createMux, Server, Client, StreamConn, ChannelStream, combineUint8ArrayListTransform, Packet, } from '../srpc/index.js';
4
- import { EchoerDefinition, EchoerServer, EchoerServiceName, runClientTest, } from '../echo/index.js';
4
+ import { EchoerDefinition, EchoerServer, EchoerServiceName, EchoMsg, runClientTest, } from '../echo/index.js';
5
5
  import { runAbortControllerTest, runRpcStreamTest, } from '../echo/client-test.js';
6
6
  describe('srpc server', () => {
7
7
  let client;
@@ -39,6 +39,33 @@ describe('srpc server', () => {
39
39
  it('should pass rpc stream tests', async () => {
40
40
  await runRpcStreamTest(client);
41
41
  });
42
+ it('passes the exact invocation signal after async request decode', async () => {
43
+ const controller = new AbortController();
44
+ let observedSignal;
45
+ const handler = createHandler(EchoerDefinition, {
46
+ Echo: async (request, signal) => {
47
+ observedSignal = signal;
48
+ return request;
49
+ },
50
+ });
51
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo');
52
+ if (!invokeFn) {
53
+ throw new Error('Echo method was not found');
54
+ }
55
+ const request = EchoMsg.create({ body: 'signal identity' });
56
+ const drained = Promise.withResolvers();
57
+ await invokeFn((async function* () {
58
+ await Promise.resolve();
59
+ yield EchoMsg.toBinary(request);
60
+ })(), async (source) => {
61
+ for await (const _data of source) {
62
+ // Drain the encoded response so the invocation pipeline completes.
63
+ }
64
+ drained.resolve();
65
+ }, controller.signal);
66
+ await drained.promise;
67
+ expect(observedSignal).toBe(controller.signal);
68
+ });
42
69
  it('keeps detached server-streaming responses open after request source completes', async () => {
43
70
  const mux = createMux();
44
71
  const response = new TextEncoder().encode('delayed init');
@@ -166,6 +193,13 @@ describe('srpc server', () => {
166
193
  await new Promise((resolve) => setTimeout(resolve, 50));
167
194
  expect(removeEventListener).toHaveBeenCalledWith('abort', expect.any(Function));
168
195
  });
196
+ it('settles a late abort after the call has already closed', async () => {
197
+ const controller = new AbortController();
198
+ vi.spyOn(controller.signal, 'removeEventListener').mockImplementation(() => undefined);
199
+ await client.request(EchoerServiceName, 'Echo', new TextEncoder().encode('late abort'), controller.signal);
200
+ controller.abort();
201
+ await Promise.resolve();
202
+ });
169
203
  it('tears down passive channel close state', async () => {
170
204
  const { port1, port2 } = new MessageChannel();
171
205
  const opts = { idleTimeoutMs: 1000, keepAliveMs: 1000 };
package/echo/echo.go CHANGED
@@ -7,15 +7,15 @@ import (
7
7
 
8
8
  // _ is a type assertion
9
9
  var (
10
- _ srpc.StreamRecv[*EchoMsg] = (SRPCEchoer_EchoBidiStreamClient)(nil)
11
- _ srpc.StreamRecv[*EchoMsg] = (SRPCEchoer_EchoServerStreamClient)(nil)
10
+ _ srpc.StreamRecv[*EchoMsg] = SRPCEchoer_EchoBidiStreamClient(nil)
11
+ _ srpc.StreamRecv[*EchoMsg] = SRPCEchoer_EchoServerStreamClient(nil)
12
12
 
13
- _ srpc.StreamSend[*EchoMsg] = (SRPCEchoer_EchoBidiStreamClient)(nil)
14
- _ srpc.StreamSend[*EchoMsg] = (SRPCEchoer_EchoClientStreamClient)(nil)
13
+ _ srpc.StreamSend[*EchoMsg] = SRPCEchoer_EchoBidiStreamClient(nil)
14
+ _ srpc.StreamSend[*EchoMsg] = SRPCEchoer_EchoClientStreamClient(nil)
15
15
 
16
- _ srpc.StreamSendAndClose[*EchoMsg] = (SRPCEchoer_EchoBidiStreamStream)(nil)
17
- _ srpc.StreamSendAndClose[*EchoMsg] = (SRPCEchoer_EchoServerStreamStream)(nil)
16
+ _ srpc.StreamSendAndClose[*EchoMsg] = SRPCEchoer_EchoBidiStreamStream(nil)
17
+ _ srpc.StreamSendAndClose[*EchoMsg] = SRPCEchoer_EchoServerStreamStream(nil)
18
18
 
19
- _ srpc.StreamRecv[*rpcstream.RpcStreamPacket] = (SRPCEchoer_RpcStreamStream)(nil)
20
- _ srpc.StreamSendAndClose[*rpcstream.RpcStreamPacket] = (SRPCEchoer_RpcStreamStream)(nil)
19
+ _ srpc.StreamRecv[*rpcstream.RpcStreamPacket] = SRPCEchoer_RpcStreamStream(nil)
20
+ _ srpc.StreamSendAndClose[*rpcstream.RpcStreamPacket] = SRPCEchoer_RpcStreamStream(nil)
21
21
  )