starpc 0.49.18 → 0.50.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 (86) 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 +100 -4
  8. package/dist/mock/index.d.ts +2 -2
  9. package/dist/mock/index.js +2 -2
  10. package/dist/mock/mock.pb.d.ts +1 -1
  11. package/dist/mock/mock.pb.js +3 -3
  12. package/dist/rpcstream/receipt.test.d.ts +1 -0
  13. package/dist/rpcstream/receipt.test.js +41 -0
  14. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  15. package/dist/rpcstream/rpcstream.pb.js +14 -8
  16. package/dist/srpc/call-receipt.d.ts +17 -0
  17. package/dist/srpc/call-receipt.js +106 -0
  18. package/dist/srpc/call-receipt.test.d.ts +1 -0
  19. package/dist/srpc/call-receipt.test.js +375 -0
  20. package/dist/srpc/client.d.ts +3 -1
  21. package/dist/srpc/client.js +20 -0
  22. package/dist/srpc/common-rpc.d.ts +13 -3
  23. package/dist/srpc/common-rpc.js +87 -10
  24. package/dist/srpc/handler.d.ts +2 -1
  25. package/dist/srpc/index.d.ts +4 -0
  26. package/dist/srpc/index.js +3 -0
  27. package/dist/srpc/invoker.d.ts +2 -1
  28. package/dist/srpc/invoker.js +2 -2
  29. package/dist/srpc/rpcproto.pb.d.ts +45 -1
  30. package/dist/srpc/rpcproto.pb.js +60 -7
  31. package/dist/srpc/server-invocation.d.ts +17 -0
  32. package/dist/srpc/server-invocation.js +37 -0
  33. package/dist/srpc/server-rpc.js +3 -1
  34. package/echo/echo.pb.go +1 -1
  35. package/echo/echo.pb.ts +6 -5
  36. package/echo/echo_srpc.pb.cpp +1 -1
  37. package/echo/echo_srpc.pb.go +1 -1
  38. package/echo/echo_srpc.pb.hpp +1 -1
  39. package/echo/echo_srpc.pb.rs +1 -1
  40. package/go.mod +13 -12
  41. package/go.sum +24 -24
  42. package/integration/cross-language/go-client/main.go +137 -5
  43. package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
  44. package/integration/cross-language/go-server/main.go +169 -4
  45. package/integration/cross-language/run.bash +117 -13
  46. package/integration/cross-language/ts-client.ts +94 -13
  47. package/integration/cross-language/ts-server.ts +115 -7
  48. package/mock/index.ts +2 -2
  49. package/mock/mock.pb.go +1 -1
  50. package/mock/mock.pb.ts +6 -5
  51. package/mock/mock_srpc.pb.cpp +1 -1
  52. package/mock/mock_srpc.pb.go +1 -1
  53. package/mock/mock_srpc.pb.hpp +1 -1
  54. package/mock/mock_srpc.pb.rs +1 -1
  55. package/package.json +19 -26
  56. package/srpc/accept.go +1 -1
  57. package/srpc/call-receipt-e2e_test.go +111 -0
  58. package/srpc/call-receipt.go +112 -0
  59. package/srpc/call-receipt.test.ts +441 -0
  60. package/srpc/call-receipt.ts +131 -0
  61. package/srpc/call-receipt_test.go +536 -0
  62. package/srpc/client-rpc.go +1 -8
  63. package/srpc/client.ts +29 -2
  64. package/srpc/common-rpc.go +114 -29
  65. package/srpc/common-rpc.ts +104 -13
  66. package/srpc/handler.ts +2 -0
  67. package/srpc/index.ts +4 -0
  68. package/srpc/invoker.ts +10 -5
  69. package/srpc/msg-stream.go +8 -0
  70. package/srpc/muxed-conn.go +9 -4
  71. package/srpc/muxed-conn_test.go +79 -0
  72. package/srpc/muxed-yamux.go +1 -1
  73. package/srpc/rpcproto.pb.cc +15 -4
  74. package/srpc/rpcproto.pb.go +97 -1
  75. package/srpc/rpcproto.pb.h +59 -0
  76. package/srpc/rpcproto.pb.rs +45 -0
  77. package/srpc/rpcproto.pb.ts +90 -27
  78. package/srpc/rpcproto.proto +16 -0
  79. package/srpc/schema-ownership_test.go +115 -0
  80. package/srpc/server-invocation.go +40 -0
  81. package/srpc/server-invocation.ts +76 -0
  82. package/srpc/server-rpc.go +1 -1
  83. package/srpc/server-rpc.ts +6 -2
  84. package/srpc/stream-yamux.go +1 -1
  85. package/srpc/stream.go +20 -0
  86. package/srpc/websocket.go +1 -1
@@ -0,0 +1,441 @@
1
+ /// <reference lib="es2024.promise" />
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+ import { pushable } from 'it-pushable'
5
+ import { EchoMsg } from '../echo/echo.pb.js'
6
+ import type { Echoer } from '../echo/echo_srpc.pb.js'
7
+
8
+ import { Client } from './client.js'
9
+ import { ClientRPC } from './client-rpc.js'
10
+ import { Server } from './server.js'
11
+ import { CallReceipt } from './call-receipt.js'
12
+ import { Packet, TerminalKind } from './rpcproto.pb.js'
13
+ import { ServerRPC } from './server-rpc.js'
14
+ import { ServerInvocation } from './server-invocation.js'
15
+
16
+ const response = new Uint8Array([1, 2, 3])
17
+
18
+ describe('generated service compatibility', () => {
19
+ it('accepts plain abort signals and terminal-capable handlers', async () => {
20
+ const server: Pick<Echoer, 'Echo'> = {
21
+ Echo: async (request, invocation?: ServerInvocation) => {
22
+ void invocation
23
+ return request
24
+ },
25
+ }
26
+ const request = EchoMsg.create({ body: 'compatibility' })
27
+ const signal = new AbortController().signal
28
+ await expect(server.Echo(request, signal)).resolves.toEqual(request)
29
+ })
30
+ })
31
+
32
+ describe('held unary receipt', () => {
33
+ it('waits for a server completion acknowledgment', async () => {
34
+ const sent: Packet[] = []
35
+ const terminal = Promise.withResolvers<void>()
36
+ const client = new Client(async () => ({
37
+ source: responseSource(terminal.promise),
38
+ sink: async (source) => {
39
+ for await (const data of source) {
40
+ const packet = Packet.fromBinary(data)
41
+ sent.push(packet)
42
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
43
+ terminal.resolve()
44
+ }
45
+ }
46
+ },
47
+ }))
48
+
49
+ const held = await client.requestWithReceipt(
50
+ 'test.Service',
51
+ 'Unary',
52
+ response,
53
+ )
54
+ const commit = held.receipt.commit()
55
+ expect(held.receipt.settled).toBe(true)
56
+ await expect(commit).resolves.toBeUndefined()
57
+
58
+ const terminalPackets = sent.filter((packet) => {
59
+ const body = packet.body
60
+ return (
61
+ body?.case === 'callCancel' ||
62
+ (body?.case === 'callData' && body.value.complete)
63
+ )
64
+ })
65
+ expect(terminalPackets).toHaveLength(1)
66
+ expect(terminalPackets[0]?.body?.case).toBe('callData')
67
+ await expect(held.receipt.done).resolves.toBeUndefined()
68
+ })
69
+
70
+ it('commits through a real TypeScript server', async () => {
71
+ const terminal = Promise.withResolvers<TerminalKind>()
72
+ const server = new Server(async () => {
73
+ return async (_source, dataSink, invocation) => {
74
+ await dataSink(
75
+ (async function* () {
76
+ yield response
77
+ if (!invocation) {
78
+ throw new Error('missing invocation')
79
+ }
80
+ terminal.resolve(
81
+ await invocation.waitTerminal(new AbortController().signal),
82
+ )
83
+ })(),
84
+ )
85
+ }
86
+ })
87
+ const client = new Client(async () => {
88
+ const clientToServer = pushable<Uint8Array>({ objectMode: true })
89
+ const serverToClient = pushable<Uint8Array>({ objectMode: true })
90
+ server.handlePacketStream({
91
+ source: clientToServer,
92
+ sink: async (source) => {
93
+ for await (const packet of source) {
94
+ serverToClient.push(packet)
95
+ }
96
+ serverToClient.end()
97
+ },
98
+ })
99
+ return {
100
+ source: serverToClient,
101
+ sink: async (source) => {
102
+ for await (const packet of source) {
103
+ clientToServer.push(packet)
104
+ }
105
+ clientToServer.end()
106
+ },
107
+ }
108
+ })
109
+
110
+ const held = await client.requestWithReceipt(
111
+ 'test.Service',
112
+ 'Unary',
113
+ response,
114
+ )
115
+ await expect(held.receipt.commit()).resolves.toBeUndefined()
116
+ await expect(terminal.promise).resolves.toBe(TerminalKind.COMMITTED)
117
+ })
118
+
119
+ it('rejects commit after a bare remote close', async () => {
120
+ const call = new ClientRPC('test.Service', 'Unary')
121
+ const iterator = call.rpcDataSource[Symbol.asyncIterator]()
122
+ await call.handleCallData({
123
+ data: response,
124
+ dataIsZero: false,
125
+ complete: false,
126
+ error: '',
127
+ })
128
+ const first = await iterator.next()
129
+ expect(first.done).toBe(false)
130
+ const receipt = new CallReceipt(call, iterator)
131
+ await call.close()
132
+ await expect(receipt.commit()).rejects.toThrow()
133
+ await expect(receipt.done).rejects.toThrow('receipt closed before commit')
134
+ })
135
+
136
+ it('aborts a held receipt with one cancel packet', async () => {
137
+ const sent: Packet[] = []
138
+ const terminal = Promise.withResolvers<void>()
139
+ const cancelSeen = Promise.withResolvers<void>()
140
+ const client = new Client(async () => ({
141
+ source: responseSource(terminal.promise),
142
+ sink: async (source) => {
143
+ for await (const data of source) {
144
+ const packet = Packet.fromBinary(data)
145
+ sent.push(packet)
146
+ if (packet.body?.case === 'callCancel') {
147
+ cancelSeen.resolve()
148
+ }
149
+ }
150
+ },
151
+ }))
152
+
153
+ const held = await client.requestWithReceipt(
154
+ 'test.Service',
155
+ 'Unary',
156
+ response,
157
+ )
158
+ await held.receipt.abort()
159
+ await cancelSeen.promise
160
+ const terminalPackets = sent.filter(
161
+ (packet) => packet.body?.case === 'callCancel',
162
+ )
163
+ expect(terminalPackets).toHaveLength(1)
164
+ await expect(held.receipt.done).rejects.toThrow()
165
+ })
166
+
167
+ it('allows exactly one concurrent commit or abort terminal', async () => {
168
+ const sent: Packet[] = []
169
+ const terminal = Promise.withResolvers<void>()
170
+ const client = new Client(async () => ({
171
+ source: responseSource(terminal.promise),
172
+ sink: async (source) => {
173
+ for await (const data of source) {
174
+ const packet = Packet.fromBinary(data)
175
+ sent.push(packet)
176
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
177
+ terminal.resolve()
178
+ }
179
+ }
180
+ },
181
+ }))
182
+
183
+ const held = await client.requestWithReceipt(
184
+ 'test.Service',
185
+ 'Unary',
186
+ response,
187
+ )
188
+ const results = await Promise.allSettled([
189
+ held.receipt.commit(),
190
+ held.receipt.abort(),
191
+ ])
192
+ const terminalPackets = sent.filter((packet) => {
193
+ const body = packet.body
194
+ return (
195
+ body?.case === 'callCancel' ||
196
+ (body?.case === 'callData' && body.value.complete)
197
+ )
198
+ })
199
+ expect(terminalPackets).toHaveLength(1)
200
+ expect(results).toHaveLength(2)
201
+ })
202
+
203
+ it('rejects a trailing response datum instead of committing', async () => {
204
+ const terminal = Promise.withResolvers<void>()
205
+ const client = new Client(async () => ({
206
+ source: (async function* () {
207
+ yield Packet.toBinary({
208
+ body: {
209
+ case: 'callData',
210
+ value: {
211
+ data: response,
212
+ dataIsZero: false,
213
+ complete: false,
214
+ error: '',
215
+ },
216
+ },
217
+ })
218
+ await terminal.promise
219
+ yield Packet.toBinary({
220
+ body: {
221
+ case: 'callData',
222
+ value: {
223
+ data: new Uint8Array([9]),
224
+ dataIsZero: false,
225
+ complete: false,
226
+ error: '',
227
+ },
228
+ },
229
+ })
230
+ })(),
231
+ sink: async (source) => {
232
+ for await (const data of source) {
233
+ const packet = Packet.fromBinary(data)
234
+ if (packet.body?.case === 'callData' && packet.body.value.complete) {
235
+ terminal.resolve()
236
+ }
237
+ }
238
+ },
239
+ }))
240
+
241
+ const held = await client.requestWithReceipt(
242
+ 'test.Service',
243
+ 'Unary',
244
+ response,
245
+ )
246
+ await expect(held.receipt.commit()).rejects.toThrow(
247
+ 'unexpected trailing response data',
248
+ )
249
+ })
250
+
251
+ it('rejects done on a remote terminal error', async () => {
252
+ const client = new Client(async () => ({
253
+ source: (async function* () {
254
+ yield Packet.toBinary({
255
+ body: {
256
+ case: 'callData',
257
+ value: {
258
+ data: response,
259
+ dataIsZero: false,
260
+ complete: false,
261
+ error: '',
262
+ },
263
+ },
264
+ })
265
+ yield Packet.toBinary({
266
+ body: {
267
+ case: 'callData',
268
+ value: {
269
+ data: new Uint8Array(),
270
+ dataIsZero: false,
271
+ complete: true,
272
+ error: 'remote failure',
273
+ },
274
+ },
275
+ })
276
+ })(),
277
+ sink: async (source) => {
278
+ for await (const _data of source) {
279
+ void _data
280
+ }
281
+ },
282
+ }))
283
+
284
+ const held = await client.requestWithReceipt(
285
+ 'test.Service',
286
+ 'Unary',
287
+ response,
288
+ )
289
+ await expect(held.receipt.done).rejects.toThrow('remote failure')
290
+ })
291
+ })
292
+
293
+ describe('server invocation terminal', () => {
294
+ it.each([
295
+ ['explicit completion', TerminalKind.COMMITTED],
296
+ ['cancel', TerminalKind.CANCELED],
297
+ ['transport loss', TerminalKind.TRANSPORT_LOST],
298
+ ['remote error packet', TerminalKind.TRANSPORT_LOST],
299
+ ['bare close', TerminalKind.CLOSED],
300
+ ['remote error packet with completion', TerminalKind.TRANSPORT_LOST],
301
+ ] as const)('%s is classified distinctly', async (_name, expected) => {
302
+ const captured = Promise.withResolvers<ServerInvocation>()
303
+ const owner = new AbortController()
304
+ const observed = Promise.withResolvers<TerminalKind>()
305
+ const rpc = new ServerRPC(async () => {
306
+ return async (_source, _sink, invocation) => {
307
+ if (!invocation) {
308
+ throw new Error('missing invocation')
309
+ }
310
+ captured.resolve(invocation)
311
+ observed.resolve(await invocation.waitTerminal(owner.signal))
312
+ }
313
+ })
314
+ await rpc.handleCallStart({
315
+ rpcService: 'test.Service',
316
+ rpcMethod: 'Unary',
317
+ data: new Uint8Array(),
318
+ dataIsZero: true,
319
+ })
320
+ const invocation = await captured.promise
321
+ if (expected === TerminalKind.COMMITTED) {
322
+ await rpc.handleCallData({ complete: true })
323
+ } else if (expected === TerminalKind.CANCELED) {
324
+ await rpc.handleCallCancel()
325
+ } else if (expected === TerminalKind.TRANSPORT_LOST) {
326
+ if (
327
+ _name === 'remote error packet' ||
328
+ _name === 'remote error packet with completion'
329
+ ) {
330
+ await rpc.handleCallData({
331
+ complete: _name === 'remote error packet with completion',
332
+ error: 'remote failure',
333
+ })
334
+ } else {
335
+ await rpc.close(new Error('transport loss'))
336
+ }
337
+ } else {
338
+ await rpc.close()
339
+ }
340
+ await expect(observed.promise).resolves.toBe(expected)
341
+ owner.abort()
342
+ if (_name === 'remote error packet with completion') {
343
+ const rpcState: object = rpc
344
+ if (!('remoteCompleted' in rpcState)) {
345
+ throw new Error('remote completion discriminator is missing')
346
+ }
347
+ expect(rpcState.remoteCompleted).toBe(false)
348
+ }
349
+ if (
350
+ expected === TerminalKind.CLOSED ||
351
+ expected === TerminalKind.TRANSPORT_LOST
352
+ ) {
353
+ expect(invocation.signal.aborted).toBe(true)
354
+ }
355
+ })
356
+
357
+ it.each(['transport loss', 'cancel'] as const)(
358
+ 'keeps completion after %s',
359
+ async (followUp) => {
360
+ const captured = Promise.withResolvers<ServerInvocation>()
361
+ const owner = new AbortController()
362
+ const rpc = new ServerRPC(async () => {
363
+ return async (_source, _sink, invocation) => {
364
+ if (!invocation) {
365
+ throw new Error('missing invocation')
366
+ }
367
+ captured.resolve(invocation)
368
+ await invocation.waitTerminal(owner.signal)
369
+ }
370
+ })
371
+ await rpc.handleCallStart({
372
+ rpcService: 'test.Service',
373
+ rpcMethod: 'Unary',
374
+ data: new Uint8Array(),
375
+ dataIsZero: true,
376
+ })
377
+ const invocation = await captured.promise
378
+ await rpc.handleCallData({ complete: true })
379
+ if (followUp === 'transport loss') {
380
+ await rpc.close(new Error('transport loss'))
381
+ } else {
382
+ await rpc.handleCallCancel()
383
+ }
384
+ await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(
385
+ TerminalKind.COMMITTED,
386
+ )
387
+ owner.abort()
388
+ },
389
+ )
390
+
391
+ it('returns abandoned only for owner cancellation without a remote terminal', async () => {
392
+ const captured = Promise.withResolvers<ServerInvocation>()
393
+ const owner = new AbortController()
394
+ const rpc = new ServerRPC(async () => {
395
+ return async (_source, _sink, invocation) => {
396
+ if (!invocation) {
397
+ throw new Error('missing invocation')
398
+ }
399
+ captured.resolve(invocation)
400
+ await invocation.waitTerminal(owner.signal)
401
+ }
402
+ })
403
+ await rpc.handleCallStart({
404
+ rpcService: 'test.Service',
405
+ rpcMethod: 'Unary',
406
+ data: new Uint8Array(),
407
+ dataIsZero: true,
408
+ })
409
+ const invocation = await captured.promise
410
+ owner.abort()
411
+ await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(
412
+ TerminalKind.ABANDONED,
413
+ )
414
+ })
415
+ })
416
+
417
+ async function* responseSource(terminal: Promise<void>) {
418
+ yield Packet.toBinary({
419
+ body: {
420
+ case: 'callData',
421
+ value: {
422
+ data: response,
423
+ dataIsZero: false,
424
+ complete: false,
425
+ error: '',
426
+ },
427
+ },
428
+ })
429
+ await terminal
430
+ yield Packet.toBinary({
431
+ body: {
432
+ case: 'callData',
433
+ value: {
434
+ data: new Uint8Array(),
435
+ dataIsZero: false,
436
+ complete: true,
437
+ error: '',
438
+ },
439
+ },
440
+ })
441
+ }
@@ -0,0 +1,131 @@
1
+ /// <reference lib="es2024.promise" />
2
+ import { ERR_RPC_ABORT } from './errors.js'
3
+ import { ClientRPC } from './client-rpc.js'
4
+ import { TerminalKind } from './rpcproto.pb.js'
5
+
6
+ // ReceiptRpc exposes held unary calls without widening ProtoRpc.
7
+ export interface ReceiptRpc {
8
+ requestWithReceipt(
9
+ service: string,
10
+ method: string,
11
+ data: Uint8Array,
12
+ abortSignal?: AbortSignal,
13
+ ): Promise<HeldCall>
14
+ }
15
+
16
+ // HeldCall contains the first response and its held terminal receipt.
17
+ export interface HeldCall {
18
+ readonly response: Uint8Array
19
+ readonly receipt: CallReceipt
20
+ }
21
+
22
+ // CallReceipt holds a unary call until it is committed or aborted.
23
+ export class CallReceipt {
24
+ #call: ClientRPC
25
+ #iterator: AsyncIterator<Uint8Array>
26
+ #terminalPromise: Promise<IteratorResult<Uint8Array>>
27
+ #terminal?: 'committed' | 'aborted'
28
+ #done: Promise<void>
29
+ #requestCommitted = false
30
+ #resolveDone!: () => void
31
+ #rejectDone!: (reason?: unknown) => void
32
+
33
+ public constructor(call: ClientRPC, iterator: AsyncIterator<Uint8Array>) {
34
+ this.#call = call
35
+ this.#iterator = iterator
36
+ const { promise, resolve, reject } = Promise.withResolvers<void>()
37
+ this.#done = promise
38
+ this.#resolveDone = resolve
39
+ this.#rejectDone = reject
40
+ this.#done.catch(() => undefined)
41
+ this.#terminalPromise = this.#observeTerminal()
42
+ this.#terminalPromise.catch(() => undefined)
43
+ }
44
+
45
+ async #observeTerminal(): Promise<IteratorResult<Uint8Array>> {
46
+ try {
47
+ const result = await this.#iterator.next()
48
+ if (!result.done) {
49
+ throw new Error('unexpected trailing response data')
50
+ }
51
+ const terminal = this.#call.getTerminalKind()
52
+ if (
53
+ terminal !== TerminalKind.COMMITTED ||
54
+ !this.#requestCommitted ||
55
+ this.#terminal !== 'committed'
56
+ ) {
57
+ throw new Error('receipt closed before commit')
58
+ }
59
+ return result
60
+ } catch (err) {
61
+ this.#rejectDone(err)
62
+ const error =
63
+ err instanceof Error ? err : new Error('receipt terminal failed')
64
+ try {
65
+ await this.#call.close(error)
66
+ } catch {
67
+ // The primary terminal error is already recorded on the receipt.
68
+ }
69
+ throw err
70
+ }
71
+ }
72
+
73
+ // done resolves after committed close and rejects on terminal failure.
74
+ public get done(): Promise<void> {
75
+ return this.#done
76
+ }
77
+
78
+ // settled reports whether a terminal transition has been claimed.
79
+ public get settled(): boolean {
80
+ return this.#terminal !== undefined
81
+ }
82
+
83
+ // commit sends request completion and waits for server finalization.
84
+ public async commit(): Promise<void> {
85
+ if (this.#terminal === 'aborted') {
86
+ throw new Error(ERR_RPC_ABORT)
87
+ }
88
+ if (this.#terminal === 'committed') {
89
+ return this.#done
90
+ }
91
+ this.#terminal = 'committed'
92
+ this.#requestCommitted = true
93
+ try {
94
+ await this.#call.writeCallData(undefined, true)
95
+ await this.#terminalPromise
96
+ await this.#call.close()
97
+ this.#resolveDone()
98
+ } catch (err) {
99
+ await this.#call.close(
100
+ err instanceof Error ? err : new Error('receipt commit failed'),
101
+ )
102
+ this.#rejectDone(err)
103
+ throw err
104
+ }
105
+ }
106
+
107
+ // abort sends request cancellation and never rejects.
108
+ public async abort(reason?: Error): Promise<void> {
109
+ if (this.#terminal !== undefined) {
110
+ return
111
+ }
112
+ this.#terminal = 'aborted'
113
+ let terminalError: unknown = reason
114
+ try {
115
+ await this.#call.writeCallCancel(true)
116
+ } catch (err) {
117
+ terminalError ??= err
118
+ }
119
+ try {
120
+ await this.#call.close()
121
+ } catch (err) {
122
+ terminalError ??= err
123
+ }
124
+ this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT))
125
+ }
126
+
127
+ // asyncDispose aborts a receipt that has not reached a terminal.
128
+ public async [Symbol.asyncDispose](): Promise<void> {
129
+ await this.abort()
130
+ }
131
+ }