starpc 0.49.17 → 0.49.20

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 (73) hide show
  1. package/README.md +1 -3
  2. package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
  3. package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
  4. package/dist/echo/echo.pb.d.ts +1 -1
  5. package/dist/echo/echo.pb.js +3 -3
  6. package/dist/integration/cross-language/ts-client.js +84 -11
  7. package/dist/integration/cross-language/ts-server.js +84 -4
  8. package/dist/mock/mock.pb.d.ts +1 -1
  9. package/dist/mock/mock.pb.js +3 -3
  10. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  11. package/dist/rpcstream/rpcstream.pb.js +14 -8
  12. package/dist/srpc/call-receipt.d.ts +17 -0
  13. package/dist/srpc/call-receipt.js +105 -0
  14. package/dist/srpc/call-receipt.test.d.ts +1 -0
  15. package/dist/srpc/call-receipt.test.js +374 -0
  16. package/dist/srpc/client.d.ts +3 -1
  17. package/dist/srpc/client.js +20 -0
  18. package/dist/srpc/common-rpc.d.ts +13 -1
  19. package/dist/srpc/common-rpc.js +86 -6
  20. package/dist/srpc/handler.d.ts +2 -1
  21. package/dist/srpc/index.d.ts +4 -0
  22. package/dist/srpc/index.js +2 -0
  23. package/dist/srpc/invoker.d.ts +2 -1
  24. package/dist/srpc/invoker.js +2 -2
  25. package/dist/srpc/rpcproto.pb.d.ts +1 -1
  26. package/dist/srpc/rpcproto.pb.js +7 -7
  27. package/dist/srpc/server-invocation.d.ts +17 -0
  28. package/dist/srpc/server-invocation.js +37 -0
  29. package/dist/srpc/server-rpc.js +3 -1
  30. package/echo/echo.pb.go +1 -1
  31. package/echo/echo.pb.ts +6 -5
  32. package/echo/echo_srpc.pb.cpp +1 -1
  33. package/echo/echo_srpc.pb.go +1 -1
  34. package/echo/echo_srpc.pb.hpp +1 -1
  35. package/echo/echo_srpc.pb.rs +1 -1
  36. package/integration/cross-language/go-client/main.go +137 -5
  37. package/integration/cross-language/go-server/main.go +146 -3
  38. package/integration/cross-language/run.bash +117 -13
  39. package/integration/cross-language/ts-client.ts +94 -13
  40. package/integration/cross-language/ts-server.ts +94 -6
  41. package/mock/mock.pb.go +1 -1
  42. package/mock/mock.pb.ts +6 -5
  43. package/mock/mock_srpc.pb.cpp +1 -1
  44. package/mock/mock_srpc.pb.go +1 -1
  45. package/mock/mock_srpc.pb.hpp +1 -1
  46. package/mock/mock_srpc.pb.rs +1 -1
  47. package/package.json +1 -1
  48. package/srpc/call-receipt-e2e_test.go +111 -0
  49. package/srpc/call-receipt.go +112 -0
  50. package/srpc/call-receipt.test.ts +438 -0
  51. package/srpc/call-receipt.ts +130 -0
  52. package/srpc/call-receipt_test.go +536 -0
  53. package/srpc/client-rpc.go +1 -8
  54. package/srpc/client.ts +29 -2
  55. package/srpc/common-rpc.go +134 -29
  56. package/srpc/common-rpc.ts +98 -8
  57. package/srpc/common-rpc_test.go +52 -0
  58. package/srpc/handler.ts +2 -0
  59. package/srpc/index.ts +4 -0
  60. package/srpc/invoker.ts +10 -5
  61. package/srpc/msg-stream.go +8 -0
  62. package/srpc/muxed-conn.go +8 -3
  63. package/srpc/muxed-conn_test.go +79 -0
  64. package/srpc/rpcproto.pb.go +1 -1
  65. package/srpc/rpcproto.pb.ts +28 -27
  66. package/srpc/server-invocation.go +58 -0
  67. package/srpc/server-invocation.ts +82 -0
  68. package/srpc/server-rpc-invoke.go +7 -0
  69. package/srpc/server-rpc-invoke_goscript.go +12 -0
  70. package/srpc/server-rpc-invoke_goscript_test.go +27 -0
  71. package/srpc/server-rpc.go +9 -2
  72. package/srpc/server-rpc.ts +6 -2
  73. package/srpc/stream.go +20 -0
@@ -0,0 +1,374 @@
1
+ /// <reference lib="es2024.promise" />
2
+ import { describe, expect, it } from 'vitest';
3
+ import { pushable } from 'it-pushable';
4
+ import { EchoMsg } from '../echo/echo.pb.js';
5
+ import { Client } from './client.js';
6
+ import { ClientRPC } from './client-rpc.js';
7
+ import { Server } from './server.js';
8
+ import { CallReceipt } from './call-receipt.js';
9
+ import { Packet } from './rpcproto.pb.js';
10
+ import { ServerRPC } from './server-rpc.js';
11
+ const response = new Uint8Array([1, 2, 3]);
12
+ describe('generated service compatibility', () => {
13
+ it('accepts plain abort signals and terminal-capable handlers', async () => {
14
+ const server = {
15
+ Echo: async (request, invocation) => {
16
+ void invocation;
17
+ return request;
18
+ },
19
+ };
20
+ const request = EchoMsg.create({ body: 'compatibility' });
21
+ const signal = new AbortController().signal;
22
+ await expect(server.Echo(request, signal)).resolves.toEqual(request);
23
+ });
24
+ });
25
+ describe('held unary receipt', () => {
26
+ it('waits for a server completion acknowledgment', async () => {
27
+ const sent = [];
28
+ const terminal = Promise.withResolvers();
29
+ const client = new Client(async () => ({
30
+ source: responseSource(terminal.promise),
31
+ sink: async (source) => {
32
+ for await (const data of source) {
33
+ const packet = Packet.fromBinary(data);
34
+ sent.push(packet);
35
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
36
+ terminal.resolve();
37
+ }
38
+ }
39
+ },
40
+ }));
41
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
42
+ const commit = held.receipt.commit();
43
+ expect(held.receipt.settled).toBe(true);
44
+ await expect(commit).resolves.toBeUndefined();
45
+ const terminalPackets = sent.filter((packet) => {
46
+ const body = packet.body;
47
+ return (body?.case === 'callCancel' ||
48
+ (body?.case === 'callData' && body.value.complete));
49
+ });
50
+ expect(terminalPackets).toHaveLength(1);
51
+ expect(terminalPackets[0]?.body?.case).toBe('callData');
52
+ await expect(held.receipt.done).resolves.toBeUndefined();
53
+ });
54
+ it('commits through a real TypeScript server', async () => {
55
+ const terminal = Promise.withResolvers();
56
+ const server = new Server(async () => {
57
+ return async (_source, dataSink, invocation) => {
58
+ await dataSink((async function* () {
59
+ yield response;
60
+ if (!invocation) {
61
+ throw new Error('missing invocation');
62
+ }
63
+ terminal.resolve(await invocation.waitTerminal(new AbortController().signal));
64
+ })());
65
+ };
66
+ });
67
+ const client = new Client(async () => {
68
+ const clientToServer = pushable({ objectMode: true });
69
+ const serverToClient = pushable({ objectMode: true });
70
+ server.handlePacketStream({
71
+ source: clientToServer,
72
+ sink: async (source) => {
73
+ for await (const packet of source) {
74
+ serverToClient.push(packet);
75
+ }
76
+ serverToClient.end();
77
+ },
78
+ });
79
+ return {
80
+ source: serverToClient,
81
+ sink: async (source) => {
82
+ for await (const packet of source) {
83
+ clientToServer.push(packet);
84
+ }
85
+ clientToServer.end();
86
+ },
87
+ };
88
+ });
89
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
90
+ await expect(held.receipt.commit()).resolves.toBeUndefined();
91
+ await expect(terminal.promise).resolves.toBe('committed');
92
+ });
93
+ it('rejects commit after a bare remote close', async () => {
94
+ const call = new ClientRPC('test.Service', 'Unary');
95
+ const iterator = call.rpcDataSource[Symbol.asyncIterator]();
96
+ await call.handleCallData({
97
+ data: response,
98
+ dataIsZero: false,
99
+ complete: false,
100
+ error: '',
101
+ });
102
+ const first = await iterator.next();
103
+ expect(first.done).toBe(false);
104
+ const receipt = new CallReceipt(call, iterator);
105
+ await call.close();
106
+ await expect(receipt.commit()).rejects.toThrow();
107
+ await expect(receipt.done).rejects.toThrow('receipt closed before commit');
108
+ });
109
+ it('aborts a held receipt with one cancel packet', async () => {
110
+ const sent = [];
111
+ const terminal = Promise.withResolvers();
112
+ const cancelSeen = Promise.withResolvers();
113
+ const client = new Client(async () => ({
114
+ source: responseSource(terminal.promise),
115
+ sink: async (source) => {
116
+ for await (const data of source) {
117
+ const packet = Packet.fromBinary(data);
118
+ sent.push(packet);
119
+ if (packet.body?.case === 'callCancel') {
120
+ cancelSeen.resolve();
121
+ }
122
+ }
123
+ },
124
+ }));
125
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
126
+ await held.receipt.abort();
127
+ await cancelSeen.promise;
128
+ const terminalPackets = sent.filter((packet) => packet.body?.case === 'callCancel');
129
+ expect(terminalPackets).toHaveLength(1);
130
+ await expect(held.receipt.done).rejects.toThrow();
131
+ });
132
+ it('allows exactly one concurrent commit or abort terminal', async () => {
133
+ const sent = [];
134
+ const terminal = Promise.withResolvers();
135
+ const client = new Client(async () => ({
136
+ source: responseSource(terminal.promise),
137
+ sink: async (source) => {
138
+ for await (const data of source) {
139
+ const packet = Packet.fromBinary(data);
140
+ sent.push(packet);
141
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
142
+ terminal.resolve();
143
+ }
144
+ }
145
+ },
146
+ }));
147
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
148
+ const results = await Promise.allSettled([
149
+ held.receipt.commit(),
150
+ held.receipt.abort(),
151
+ ]);
152
+ const terminalPackets = sent.filter((packet) => {
153
+ const body = packet.body;
154
+ return (body?.case === 'callCancel' ||
155
+ (body?.case === 'callData' && body.value.complete));
156
+ });
157
+ expect(terminalPackets).toHaveLength(1);
158
+ expect(results).toHaveLength(2);
159
+ });
160
+ it('rejects a trailing response datum instead of committing', async () => {
161
+ const terminal = Promise.withResolvers();
162
+ const client = new Client(async () => ({
163
+ source: (async function* () {
164
+ yield Packet.toBinary({
165
+ body: {
166
+ case: 'callData',
167
+ value: {
168
+ data: response,
169
+ dataIsZero: false,
170
+ complete: false,
171
+ error: '',
172
+ },
173
+ },
174
+ });
175
+ await terminal.promise;
176
+ yield Packet.toBinary({
177
+ body: {
178
+ case: 'callData',
179
+ value: {
180
+ data: new Uint8Array([9]),
181
+ dataIsZero: false,
182
+ complete: false,
183
+ error: '',
184
+ },
185
+ },
186
+ });
187
+ })(),
188
+ sink: async (source) => {
189
+ for await (const data of source) {
190
+ const packet = Packet.fromBinary(data);
191
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
192
+ terminal.resolve();
193
+ }
194
+ }
195
+ },
196
+ }));
197
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
198
+ await expect(held.receipt.commit()).rejects.toThrow('unexpected trailing response data');
199
+ });
200
+ it('rejects done on a remote terminal error', async () => {
201
+ const client = new Client(async () => ({
202
+ source: (async function* () {
203
+ yield Packet.toBinary({
204
+ body: {
205
+ case: 'callData',
206
+ value: {
207
+ data: response,
208
+ dataIsZero: false,
209
+ complete: false,
210
+ error: '',
211
+ },
212
+ },
213
+ });
214
+ yield Packet.toBinary({
215
+ body: {
216
+ case: 'callData',
217
+ value: {
218
+ data: new Uint8Array(),
219
+ dataIsZero: false,
220
+ complete: true,
221
+ error: 'remote failure',
222
+ },
223
+ },
224
+ });
225
+ })(),
226
+ sink: async (source) => {
227
+ for await (const _data of source) {
228
+ void _data;
229
+ }
230
+ },
231
+ }));
232
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
233
+ await expect(held.receipt.done).rejects.toThrow('remote failure');
234
+ });
235
+ });
236
+ describe('server invocation terminal', () => {
237
+ it.each([
238
+ ['explicit completion', 'committed'],
239
+ ['cancel', 'canceled'],
240
+ ['transport loss', 'transportLost'],
241
+ ['remote error packet', 'transportLost'],
242
+ ['bare close', 'closed'],
243
+ ['remote error packet with completion', 'transportLost'],
244
+ ])('%s is classified distinctly', async (_name, expected) => {
245
+ const captured = Promise.withResolvers();
246
+ const owner = new AbortController();
247
+ const observed = Promise.withResolvers();
248
+ const rpc = new ServerRPC(async () => {
249
+ return async (_source, _sink, invocation) => {
250
+ if (!invocation) {
251
+ throw new Error('missing invocation');
252
+ }
253
+ captured.resolve(invocation);
254
+ observed.resolve(await invocation.waitTerminal(owner.signal));
255
+ };
256
+ });
257
+ await rpc.handleCallStart({
258
+ rpcService: 'test.Service',
259
+ rpcMethod: 'Unary',
260
+ data: new Uint8Array(),
261
+ dataIsZero: true,
262
+ });
263
+ const invocation = await captured.promise;
264
+ if (expected === 'committed') {
265
+ await rpc.handleCallData({ complete: true });
266
+ }
267
+ else if (expected === 'canceled') {
268
+ await rpc.handleCallCancel();
269
+ }
270
+ else if (expected === 'transportLost') {
271
+ if (_name === 'remote error packet' ||
272
+ _name === 'remote error packet with completion') {
273
+ await rpc.handleCallData({
274
+ complete: _name === 'remote error packet with completion',
275
+ error: 'remote failure',
276
+ });
277
+ }
278
+ else {
279
+ await rpc.close(new Error('transport loss'));
280
+ }
281
+ }
282
+ else {
283
+ await rpc.close();
284
+ }
285
+ await expect(observed.promise).resolves.toBe(expected);
286
+ owner.abort();
287
+ if (_name === 'remote error packet with completion') {
288
+ const rpcState = rpc;
289
+ if (!('remoteCompleted' in rpcState)) {
290
+ throw new Error('remote completion discriminator is missing');
291
+ }
292
+ expect(rpcState.remoteCompleted).toBe(false);
293
+ }
294
+ if (expected === 'closed' || expected === 'transportLost') {
295
+ expect(invocation.signal.aborted).toBe(true);
296
+ }
297
+ });
298
+ it.each(['transport loss', 'cancel'])('keeps completion after %s', async (followUp) => {
299
+ const captured = Promise.withResolvers();
300
+ const owner = new AbortController();
301
+ const rpc = new ServerRPC(async () => {
302
+ return async (_source, _sink, invocation) => {
303
+ if (!invocation) {
304
+ throw new Error('missing invocation');
305
+ }
306
+ captured.resolve(invocation);
307
+ await invocation.waitTerminal(owner.signal);
308
+ };
309
+ });
310
+ await rpc.handleCallStart({
311
+ rpcService: 'test.Service',
312
+ rpcMethod: 'Unary',
313
+ data: new Uint8Array(),
314
+ dataIsZero: true,
315
+ });
316
+ const invocation = await captured.promise;
317
+ await rpc.handleCallData({ complete: true });
318
+ if (followUp === 'transport loss') {
319
+ await rpc.close(new Error('transport loss'));
320
+ }
321
+ else {
322
+ await rpc.handleCallCancel();
323
+ }
324
+ await expect(invocation.waitTerminal(owner.signal)).resolves.toBe('committed');
325
+ owner.abort();
326
+ });
327
+ it('returns abandoned only for owner cancellation without a remote terminal', async () => {
328
+ const captured = Promise.withResolvers();
329
+ const owner = new AbortController();
330
+ const rpc = new ServerRPC(async () => {
331
+ return async (_source, _sink, invocation) => {
332
+ if (!invocation) {
333
+ throw new Error('missing invocation');
334
+ }
335
+ captured.resolve(invocation);
336
+ await invocation.waitTerminal(owner.signal);
337
+ };
338
+ });
339
+ await rpc.handleCallStart({
340
+ rpcService: 'test.Service',
341
+ rpcMethod: 'Unary',
342
+ data: new Uint8Array(),
343
+ dataIsZero: true,
344
+ });
345
+ const invocation = await captured.promise;
346
+ owner.abort();
347
+ await expect(invocation.waitTerminal(owner.signal)).resolves.toBe('abandoned');
348
+ });
349
+ });
350
+ async function* responseSource(terminal) {
351
+ yield Packet.toBinary({
352
+ body: {
353
+ case: 'callData',
354
+ value: {
355
+ data: response,
356
+ dataIsZero: false,
357
+ complete: false,
358
+ error: '',
359
+ },
360
+ },
361
+ });
362
+ await terminal;
363
+ yield Packet.toBinary({
364
+ body: {
365
+ case: 'callData',
366
+ value: {
367
+ data: new Uint8Array(),
368
+ dataIsZero: false,
369
+ complete: true,
370
+ error: '',
371
+ },
372
+ },
373
+ });
374
+ }
@@ -1,10 +1,12 @@
1
1
  import type { ProtoRpc } from './proto-rpc.js';
2
2
  import type { OpenStreamFunc } from './stream.js';
3
- export declare class Client implements ProtoRpc {
3
+ import type { HeldCall, ReceiptRpc } from './call-receipt.js';
4
+ export declare class Client implements ProtoRpc, ReceiptRpc {
4
5
  private openStreamCtr;
5
6
  constructor(openStreamFn?: OpenStreamFunc);
6
7
  setOpenStreamFn(openStreamFn?: OpenStreamFunc): void;
7
8
  request(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<Uint8Array>;
9
+ requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
8
10
  clientStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): Promise<Uint8Array>;
9
11
  serverStreamingRequest(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
10
12
  bidirectionalStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
@@ -5,6 +5,7 @@ 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';
8
9
  // Client implements the ts-proto Rpc interface with the drpcproto protocol.
9
10
  export class Client {
10
11
  // openStreamCtr contains the OpenStreamFunc.
@@ -27,6 +28,25 @@ export class Client {
27
28
  call.close(err);
28
29
  throw err;
29
30
  }
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
+ }
30
50
  // clientStreamingRequest starts a client side streaming request.
31
51
  async clientStreamingRequest(service, method, data, abortSignal) {
32
52
  const call = await this.startRpc(service, method, null, abortSignal);
@@ -1,6 +1,7 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
2
  import type { CallData, CallStart } from './rpcproto.pb.js';
3
3
  import { Packet } from './rpcproto.pb.js';
4
+ import type { TerminalKind } from './server-invocation.js';
4
5
  export declare class CommonRPC {
5
6
  readonly sink: Sink<Source<Packet>>;
6
7
  readonly source: AsyncIterable<Packet>;
@@ -10,12 +11,23 @@ export declare class CommonRPC {
10
11
  protected service?: string;
11
12
  protected method?: string;
12
13
  private closed?;
14
+ private remoteCompleted;
15
+ private remoteError?;
16
+ private remoteSourceClosed;
17
+ private remoteTerminal?;
18
+ private readonly invocationController;
19
+ private readonly terminalPromise;
20
+ private resolveTerminal;
13
21
  private readonly writeDrainAbort;
14
22
  constructor();
15
23
  get isClosed(): boolean | Error;
24
+ protected get invocationSignal(): AbortSignal;
25
+ protected waitTerminal(ownerSignal: AbortSignal): Promise<TerminalKind>;
26
+ getTerminalKind(): TerminalKind | undefined;
27
+ private recordRemoteTerminal;
16
28
  writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
17
29
  private writeCallDataPacket;
18
- writeCallCancel(): Promise<void>;
30
+ writeCallCancel(waitForDrain?: boolean): Promise<void>;
19
31
  writeCallDataFromSource(dataSource: AsyncIterable<Uint8Array>): Promise<void>;
20
32
  protected writePacket(packet: Packet, options?: WritePacketOptions): Promise<void>;
21
33
  handleMessage(message: Uint8Array): Promise<void>;
@@ -24,9 +24,25 @@ 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
+ // remoteError records a remote error or transport failure.
30
+ 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.
36
+ invocationController = new AbortController();
37
+ // terminalPromise resolves when a remote terminal is recorded.
38
+ terminalPromise;
39
+ resolveTerminal;
27
40
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
28
41
  writeDrainAbort = new AbortController();
29
42
  constructor() {
43
+ const { promise, resolve } = Promise.withResolvers();
44
+ this.terminalPromise = promise;
45
+ this.resolveTerminal = resolve;
30
46
  this.sink = this._createSink();
31
47
  this.source = this._source;
32
48
  this.rpcDataSource = this._rpcDataSource;
@@ -35,6 +51,49 @@ export class CommonRPC {
35
51
  get isClosed() {
36
52
  return this.closed ?? false;
37
53
  }
54
+ // invocationSignal is canceled when the RPC reaches a terminal.
55
+ get invocationSignal() {
56
+ return this.invocationController.signal;
57
+ }
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
+ }
38
97
  // writeCallData writes the call data packet.
39
98
  async writeCallData(data, complete, error) {
40
99
  await this.writeCallDataPacket(data, complete, error);
@@ -55,13 +114,16 @@ export class CommonRPC {
55
114
  }, writeOptions);
56
115
  }
57
116
  // writeCallCancel writes the call cancel packet.
58
- async writeCallCancel() {
117
+ async writeCallCancel(waitForDrain = false) {
59
118
  await this.writePacket({
60
119
  body: {
61
120
  case: 'callCancel',
62
121
  value: true,
63
122
  },
64
123
  }, { waitForDrain: false });
124
+ if (waitForDrain) {
125
+ await this._source.onEmpty({ signal: this.writeDrainAbort.signal });
126
+ }
65
127
  }
66
128
  // writeCallDataFromSource writes all call data from the iterable.
67
129
  async writeCallDataFromSource(dataSource) {
@@ -153,16 +215,27 @@ export class CommonRPC {
153
215
  throw new Error('call start must be sent before call data');
154
216
  }
155
217
  this.pushRpcData(packet.data, packet.dataIsZero);
156
- if (packet.error) {
157
- this._rpcDataSource.end(new RemoteRPCError(this.service, this.method, packet.error));
218
+ const remoteError = packet.error ?
219
+ new RemoteRPCError(this.service, this.method, packet.error)
220
+ : undefined;
221
+ if (remoteError) {
222
+ this.remoteError ??= remoteError;
223
+ this.invocationController.abort();
224
+ this.recordRemoteTerminal('transportLost');
158
225
  }
159
- else if (packet.complete) {
160
- this._rpcDataSource.end();
226
+ if (packet.complete && !remoteError) {
227
+ this.remoteCompleted = true;
228
+ this.recordRemoteTerminal('committed');
229
+ this._rpcDataSource.end(remoteError);
230
+ }
231
+ else if (remoteError) {
232
+ this._rpcDataSource.end(remoteError);
161
233
  }
162
234
  }
163
235
  // handleCallCancel handles a CallCancel packet.
164
236
  async handleCallCancel() {
165
- this.close(new Error(ERR_RPC_ABORT));
237
+ this.recordRemoteTerminal('canceled');
238
+ await this.close(new Error(ERR_RPC_ABORT));
166
239
  }
167
240
  // close closes the call, optionally with an error.
168
241
  async close(err) {
@@ -170,6 +243,11 @@ export class CommonRPC {
170
243
  return;
171
244
  }
172
245
  this.closed = err ?? true;
246
+ if (!this.remoteError && err) {
247
+ this.remoteError = err;
248
+ }
249
+ this.recordRemoteTerminal(err ? 'transportLost' : 'closed');
250
+ this.invocationController.abort();
173
251
  // note: this does nothing if _source is already ended.
174
252
  if (err && err.message) {
175
253
  await this.writeCallDataPacket(undefined, true, err.message, {
@@ -197,6 +275,8 @@ export class CommonRPC {
197
275
  await this.handlePacket(msg);
198
276
  }
199
277
  }
278
+ this.remoteSourceClosed = true;
279
+ this.recordRemoteTerminal('closed');
200
280
  }
201
281
  catch (err) {
202
282
  this.close(err);
@@ -1,6 +1,7 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
2
  import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
3
- export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>) => Promise<void>;
3
+ import type { ServerInvocation } from './server-invocation.js';
4
+ export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: ServerInvocation) => Promise<void>;
4
5
  export interface Handler {
5
6
  getServiceID(): string;
6
7
  getMethodIDs(): string[];
@@ -1,5 +1,9 @@
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';
3
7
  export { Server } from './server.js';
4
8
  export { StreamConn } from './conn.js';
5
9
  export type { StreamConnParams, StreamHandler } from './conn.js';
@@ -1,5 +1,7 @@
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';
3
5
  export { Server } from './server.js';
4
6
  export { StreamConn } from './conn.js';
5
7
  export { WebSocketConn } from './websocket.js';
@@ -2,5 +2,6 @@ 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
- export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R) => Promise<O>) | ((request: R) => AsyncIterable<O>) | ((request: AsyncIterable<R>) => Promise<O>) | ((request: AsyncIterable<R>) => AsyncIterable<O>);
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>);
6
7
  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;
@@ -6,7 +6,7 @@ import { MethodKind } from '@aptre/protobuf-es-lite';
6
6
  // createInvokeFn builds an InvokeFn from a method definition and a function prototype.
7
7
  export function createInvokeFn(methodInfo, methodProto) {
8
8
  const requestDecode = buildDecodeMessageTransform(methodInfo.I);
9
- return async (dataSource, dataSink) => {
9
+ return async (dataSource, dataSink, invocation) => {
10
10
  // responseSink is a Sink for response messages.
11
11
  const responseSink = pushable({
12
12
  objectMode: true,
@@ -34,7 +34,7 @@ export function createInvokeFn(methodInfo, methodProto) {
34
34
  }
35
35
  // Call the implementation.
36
36
  try {
37
- const responseObj = methodProto(requestArg);
37
+ const responseObj = methodProto(requestArg, invocation);
38
38
  if (!responseObj) {
39
39
  throw new Error('return value was undefined');
40
40
  }
@@ -1,4 +1,4 @@
1
- import type { MessageType } from '@aptre/protobuf-es-lite';
1
+ import type { MessageType } from '@aptre/protobuf-es-lite/message';
2
2
  export declare const protobufPackage = "srpc";
3
3
  /**
4
4
  * CallStart requests starting a new RPC call.