starpc 0.49.18 → 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 (69) 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 +114 -29
  56. package/srpc/common-rpc.ts +98 -8
  57. package/srpc/handler.ts +2 -0
  58. package/srpc/index.ts +4 -0
  59. package/srpc/invoker.ts +10 -5
  60. package/srpc/msg-stream.go +8 -0
  61. package/srpc/muxed-conn.go +8 -3
  62. package/srpc/muxed-conn_test.go +79 -0
  63. package/srpc/rpcproto.pb.go +1 -1
  64. package/srpc/rpcproto.pb.ts +28 -27
  65. package/srpc/server-invocation.go +58 -0
  66. package/srpc/server-invocation.ts +82 -0
  67. package/srpc/server-rpc.go +1 -1
  68. package/srpc/server-rpc.ts +6 -2
  69. package/srpc/stream.go +20 -0
package/README.md CHANGED
@@ -3,14 +3,12 @@
3
3
  [![npm](https://img.shields.io/npm/v/starpc?style=flat-square)](https://www.npmjs.com/package/starpc)
4
4
  [![crates.io](https://img.shields.io/crates/v/starpc.svg?style=flat-square)](https://crates.io/crates/starpc)
5
5
  [![Build status](https://img.shields.io/github/actions/workflow/status/aperturerobotics/starpc/tests.yml?style=flat-square&branch=master)](https://github.com/aperturerobotics/starpc/actions)
6
- [![GoDoc Widget]][GoDoc] [![Go Report Card Widget]][Go Report Card]
6
+ [![GoDoc Widget]][GoDoc]
7
7
 
8
8
  > Streaming Protobuf RPC with bidirectional streaming over any multiplexed transport.
9
9
 
10
10
  [GoDoc]: https://godoc.org/github.com/aperturerobotics/starpc
11
11
  [GoDoc Widget]: https://godoc.org/github.com/aperturerobotics/starpc?status.svg
12
- [Go Report Card Widget]: https://goreportcard.com/badge/github.com/aperturerobotics/starpc
13
- [Go Report Card]: https://goreportcard.com/report/github.com/aperturerobotics/starpc
14
12
 
15
13
  ## Table of Contents
16
14
 
@@ -26,6 +26,14 @@ import {
26
26
  } from '@aptre/protobuf-es-lite/protoplugin/ecmascript'
27
27
 
28
28
  const MessageStream = createImportSymbol('MessageStream', 'starpc')
29
+ const RuntimeMethodKind = createImportSymbol(
30
+ 'MethodKind',
31
+ '@aptre/protobuf-es-lite',
32
+ )
33
+ const RuntimeMethodIdempotency = createImportSymbol(
34
+ 'MethodIdempotency',
35
+ '@aptre/protobuf-es-lite',
36
+ )
29
37
  // const Message = createImportSymbol('Message', '@aptre/protobuf-es-lite')
30
38
 
31
39
  export function generateTs(schema: Schema) {
@@ -33,19 +41,16 @@ export function generateTs(schema: Schema) {
33
41
  const file = schema.generateFile(protoFile.name + '_srpc.pb.ts')
34
42
  file.preamble(protoFile)
35
43
  for (const service of protoFile.services) {
36
- generateService(schema, file, service)
44
+ generateService(file, service)
37
45
  }
38
46
  }
39
47
  }
40
48
 
41
49
  // prettier-ignore
42
50
  function generateService(
43
- schema: Schema,
44
51
  f: GeneratedFile,
45
52
  service: DescService
46
53
  ) {
47
- const { MethodKind: rtMethodKind, MethodIdempotency: rtMethodIdempotency } =
48
- schema.runtime;
49
54
 
50
55
  // NOTE: This matches generateService from @connectrpc/protoc-gen-connect-es.
51
56
  f.print(f.jsDoc(service));
@@ -60,7 +65,7 @@ function generateService(
60
65
  f.print(" O: ", method.output, ",");
61
66
  f.print(
62
67
  " kind: ",
63
- rtMethodKind,
68
+ RuntimeMethodKind,
64
69
  ".",
65
70
  MethodKind[method.methodKind],
66
71
  ","
@@ -68,7 +73,7 @@ function generateService(
68
73
  if (method.idempotency !== undefined) {
69
74
  f.print(
70
75
  " idempotency: ",
71
- rtMethodIdempotency,
76
+ RuntimeMethodIdempotency,
72
77
  ".",
73
78
  MethodIdempotency[method.idempotency],
74
79
  ","
@@ -15,19 +15,20 @@
15
15
  import { MethodIdempotency, MethodKind, localName, } from '@aptre/protobuf-es-lite';
16
16
  import { createImportSymbol, } from '@aptre/protobuf-es-lite/protoplugin/ecmascript';
17
17
  const MessageStream = createImportSymbol('MessageStream', 'starpc');
18
+ const RuntimeMethodKind = createImportSymbol('MethodKind', '@aptre/protobuf-es-lite');
19
+ const RuntimeMethodIdempotency = createImportSymbol('MethodIdempotency', '@aptre/protobuf-es-lite');
18
20
  // const Message = createImportSymbol('Message', '@aptre/protobuf-es-lite')
19
21
  export function generateTs(schema) {
20
22
  for (const protoFile of schema.files) {
21
23
  const file = schema.generateFile(protoFile.name + '_srpc.pb.ts');
22
24
  file.preamble(protoFile);
23
25
  for (const service of protoFile.services) {
24
- generateService(schema, file, service);
26
+ generateService(file, service);
25
27
  }
26
28
  }
27
29
  }
28
30
  // prettier-ignore
29
- function generateService(schema, f, service) {
30
- const { MethodKind: rtMethodKind, MethodIdempotency: rtMethodIdempotency } = schema.runtime;
31
+ function generateService(f, service) {
31
32
  // NOTE: This matches generateService from @connectrpc/protoc-gen-connect-es.
32
33
  f.print(f.jsDoc(service));
33
34
  f.print(f.exportDecl("const", localName(service)), "Definition = {");
@@ -39,9 +40,9 @@ function generateService(schema, f, service) {
39
40
  f.print(` name: `, f.string(method.name), `,`);
40
41
  f.print(" I: ", method.input, ",");
41
42
  f.print(" O: ", method.output, ",");
42
- f.print(" kind: ", rtMethodKind, ".", MethodKind[method.methodKind], ",");
43
+ f.print(" kind: ", RuntimeMethodKind, ".", MethodKind[method.methodKind], ",");
43
44
  if (method.idempotency !== undefined) {
44
- f.print(" idempotency: ", rtMethodIdempotency, ".", MethodIdempotency[method.idempotency], ",");
45
+ f.print(" idempotency: ", RuntimeMethodIdempotency, ".", MethodIdempotency[method.idempotency], ",");
45
46
  }
46
47
  // In case we start supporting options, we have to surface them here
47
48
  f.print(" },");
@@ -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 = "echo";
3
3
  /**
4
4
  * EchoMsg is the message body for Echo.
@@ -1,10 +1,10 @@
1
1
  // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false"
2
2
  // @generated from file github.com/aperturerobotics/starpc/echo/echo.proto (package echo, syntax proto3)
3
3
  /* eslint-disable */
4
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite';
4
+ import { createMessageType } from '@aptre/protobuf-es-lite/message';
5
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar';
5
6
  export const protobufPackage = 'echo';
6
- // EchoMsg contains the message type declaration for EchoMsg.
7
- export const EchoMsg = createMessageType({
7
+ export const EchoMsg = /* @__PURE__ */ createMessageType({
8
8
  typeName: 'echo.EchoMsg',
9
9
  fields: [
10
10
  { no: 1, name: 'body', kind: 'scalar', T: ScalarType.STRING },
@@ -1,10 +1,25 @@
1
1
  import net from 'net';
2
+ import { closeSync, openSync, writeSync } from 'node:fs';
2
3
  import { pipe } from 'it-pipe';
3
4
  import { pushable } from 'it-pushable';
4
5
  import { Client } from '../../srpc/client.js';
5
6
  import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
6
7
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
7
- import { runClientTest } from '../../echo/client-test.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
+ }
8
23
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
9
24
  function tcpSocketToPacketStream(socket) {
10
25
  const socketSource = async function* () {
@@ -36,26 +51,84 @@ function tcpSocketToPacketStream(socket) {
36
51
  };
37
52
  }
38
53
  async function main() {
39
- const addr = process.argv[2];
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];
40
57
  if (!addr) {
41
- console.error('usage: ts-client <host:port>');
58
+ console.error('usage: ts-client [receipt <case>] <host:port>');
42
59
  process.exit(1);
43
60
  }
61
+ if (receiptMode &&
62
+ !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')) {
63
+ throw new Error(`unknown receipt case: ${receiptCase}`);
64
+ }
44
65
  const [host, portStr] = addr.split(':');
45
66
  const port = parseInt(portStr, 10);
67
+ let activeSocket;
46
68
  const openStream = async () => {
47
- return new Promise((resolve, reject) => {
48
- const socket = net.connect(port, host, () => {
49
- resolve(tcpSocketToPacketStream(socket));
50
- });
51
- socket.on('error', reject);
69
+ const { promise, resolve, reject } = Promise.withResolvers();
70
+ const socket = net.connect(port, host, () => {
71
+ activeSocket = socket;
72
+ resolve(tcpSocketToPacketStream(socket));
52
73
  });
74
+ socket.on('error', reject);
75
+ return promise;
53
76
  };
54
77
  const client = new Client(openStream);
55
- console.log('Running client test via TCP...');
56
- await runClientTest(client);
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
+ }
57
108
  console.log('All tests passed.');
58
- process.exit(0);
109
+ }
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');
59
132
  }
60
133
  process.on('unhandledRejection', (ev) => {
61
134
  console.error('Unhandled rejection', ev);
@@ -1,11 +1,27 @@
1
1
  import net from 'net';
2
+ import { closeSync, openSync, writeSync } from 'node:fs';
2
3
  import { pipe } from 'it-pipe';
3
4
  import { pushable } from 'it-pushable';
4
5
  import { createMux, createHandler, Server } from '../../srpc/index.js';
5
6
  import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
6
7
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
7
- import { EchoerServer } from '../../echo/server.js';
8
+ import { EchoerServer, EchoMsg } from '../../echo/index.js';
8
9
  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
+ }
9
25
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
10
26
  // Each Uint8Array in source/sink is one packet (no length prefix).
11
27
  function tcpSocketToPacketStream(socket) {
@@ -25,7 +41,12 @@ function tcpSocketToPacketStream(socket) {
25
41
  sink: async (source) => {
26
42
  for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
27
43
  const data = chunk instanceof Uint8Array ? chunk : chunk.subarray();
28
- await new Promise((resolve, reject) => {
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) => {
29
50
  socket.write(data, (err) => {
30
51
  if (err)
31
52
  reject(err);
@@ -33,15 +54,69 @@ function tcpSocketToPacketStream(socket) {
33
54
  resolve();
34
55
  });
35
56
  });
57
+ if (receiptCompletion) {
58
+ emitReceiptEvent('SERVER_RECEIPT_ACK committed');
59
+ }
60
+ await writeDone;
61
+ if (receiptCompletion) {
62
+ finishReceiptServer();
63
+ }
36
64
  }
37
65
  socket.end();
38
66
  },
39
67
  };
40
68
  }
41
69
  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
+ }
42
115
  const server = new Server(mux.lookupMethod);
43
- const echoer = new EchoerServer(server);
44
- mux.register(createHandler(EchoerDefinition, echoer));
116
+ if (!receiptMode) {
117
+ const echoer = new EchoerServer(server);
118
+ mux.register(createHandler(EchoerDefinition, echoer));
119
+ }
45
120
  const tcpServer = net.createServer((socket) => {
46
121
  const stream = tcpSocketToPacketStream(socket);
47
122
  server.handlePacketStream(stream);
@@ -50,6 +125,11 @@ tcpServer.listen(0, '127.0.0.1', () => {
50
125
  const addr = tcpServer.address();
51
126
  console.log(`LISTENING ${addr.address}:${addr.port}`);
52
127
  });
128
+ if (receiptMode) {
129
+ void receiptServerDone.promise.then(() => {
130
+ tcpServer.close();
131
+ });
132
+ }
53
133
  process.on('SIGINT', () => {
54
134
  tcpServer.close();
55
135
  process.exit(0);
@@ -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 = "e2e.mock";
3
3
  /**
4
4
  * MockMsg is the mock message body.
@@ -1,10 +1,10 @@
1
1
  // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false"
2
2
  // @generated from file github.com/aperturerobotics/starpc/mock/mock.proto (package e2e.mock, syntax proto3)
3
3
  /* eslint-disable */
4
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite';
4
+ import { createMessageType } from '@aptre/protobuf-es-lite/message';
5
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar';
5
6
  export const protobufPackage = 'e2e.mock';
6
- // MockMsg contains the message type declaration for MockMsg.
7
- export const MockMsg = createMessageType({
7
+ export const MockMsg = /* @__PURE__ */ createMessageType({
8
8
  typeName: 'e2e.mock.MockMsg',
9
9
  fields: [
10
10
  { no: 1, name: 'body', kind: 'scalar', T: ScalarType.STRING },
@@ -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 = "rpcstream";
3
3
  /**
4
4
  * RpcStreamInit is the first message in a RPC stream.
@@ -1,26 +1,26 @@
1
1
  // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false"
2
2
  // @generated from file github.com/aperturerobotics/starpc/rpcstream/rpcstream.proto (package rpcstream, syntax proto3)
3
3
  /* eslint-disable */
4
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite';
4
+ import { createMessageType } from '@aptre/protobuf-es-lite/message';
5
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar';
5
6
  export const protobufPackage = 'rpcstream';
6
- // RpcStreamInit contains the message type declaration for RpcStreamInit.
7
- export const RpcStreamInit = createMessageType({
7
+ export const RpcStreamInit =
8
+ /* @__PURE__ */ createMessageType({
8
9
  typeName: 'rpcstream.RpcStreamInit',
9
10
  fields: [
10
11
  { no: 1, name: 'component_id', kind: 'scalar', T: ScalarType.STRING },
11
12
  ],
12
13
  packedByDefault: true,
13
14
  });
14
- // RpcAck contains the message type declaration for RpcAck.
15
- export const RpcAck = createMessageType({
15
+ export const RpcAck = /* @__PURE__ */ createMessageType({
16
16
  typeName: 'rpcstream.RpcAck',
17
17
  fields: [
18
18
  { no: 1, name: 'error', kind: 'scalar', T: ScalarType.STRING },
19
19
  ],
20
20
  packedByDefault: true,
21
21
  });
22
- // RpcStreamPacket contains the message type declaration for RpcStreamPacket.
23
- export const RpcStreamPacket = createMessageType({
22
+ export const RpcStreamPacket =
23
+ /* @__PURE__ */ createMessageType({
24
24
  typeName: 'rpcstream.RpcStreamPacket',
25
25
  fields: [
26
26
  {
@@ -31,7 +31,13 @@ export const RpcStreamPacket = createMessageType({
31
31
  oneof: 'body',
32
32
  },
33
33
  { no: 2, name: 'ack', kind: 'message', T: () => RpcAck, oneof: 'body' },
34
- { no: 3, name: 'data', kind: 'scalar', T: ScalarType.BYTES, oneof: 'body' },
34
+ {
35
+ no: 3,
36
+ name: 'data',
37
+ kind: 'scalar',
38
+ T: ScalarType.BYTES,
39
+ oneof: 'body',
40
+ },
35
41
  ],
36
42
  packedByDefault: true,
37
43
  });
@@ -0,0 +1,17 @@
1
+ import { ClientRPC } from './client-rpc.js';
2
+ export interface ReceiptRpc {
3
+ requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
4
+ }
5
+ export interface HeldCall {
6
+ readonly response: Uint8Array;
7
+ readonly receipt: CallReceipt;
8
+ }
9
+ export declare class CallReceipt {
10
+ #private;
11
+ constructor(call: ClientRPC, iterator: AsyncIterator<Uint8Array>);
12
+ get done(): Promise<void>;
13
+ get settled(): boolean;
14
+ commit(): Promise<void>;
15
+ abort(reason?: Error): Promise<void>;
16
+ [Symbol.asyncDispose](): Promise<void>;
17
+ }
@@ -0,0 +1,105 @@
1
+ /// <reference lib="es2024.promise" />
2
+ import { ERR_RPC_ABORT } from './errors.js';
3
+ // CallReceipt holds a unary call until it is committed or aborted.
4
+ export class CallReceipt {
5
+ #call;
6
+ #iterator;
7
+ #terminalPromise;
8
+ #terminal;
9
+ #done;
10
+ #requestCommitted = false;
11
+ #resolveDone;
12
+ #rejectDone;
13
+ constructor(call, iterator) {
14
+ this.#call = call;
15
+ this.#iterator = iterator;
16
+ const { promise, resolve, reject } = Promise.withResolvers();
17
+ this.#done = promise;
18
+ this.#resolveDone = resolve;
19
+ this.#rejectDone = reject;
20
+ this.#done.catch(() => undefined);
21
+ this.#terminalPromise = this.#observeTerminal();
22
+ this.#terminalPromise.catch(() => undefined);
23
+ }
24
+ async #observeTerminal() {
25
+ try {
26
+ const result = await this.#iterator.next();
27
+ if (!result.done) {
28
+ throw new Error('unexpected trailing response data');
29
+ }
30
+ const terminal = this.#call.getTerminalKind();
31
+ if (terminal !== 'committed' ||
32
+ !this.#requestCommitted ||
33
+ this.#terminal !== 'committed') {
34
+ throw new Error('receipt closed before commit');
35
+ }
36
+ return result;
37
+ }
38
+ catch (err) {
39
+ this.#rejectDone(err);
40
+ const error = err instanceof Error ? err : new Error('receipt terminal failed');
41
+ try {
42
+ await this.#call.close(error);
43
+ }
44
+ catch {
45
+ // The primary terminal error is already recorded on the receipt.
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+ // done resolves after committed close and rejects on terminal failure.
51
+ get done() {
52
+ return this.#done;
53
+ }
54
+ // settled reports whether a terminal transition has been claimed.
55
+ get settled() {
56
+ return this.#terminal !== undefined;
57
+ }
58
+ // commit sends request completion and waits for server finalization.
59
+ async commit() {
60
+ if (this.#terminal === 'aborted') {
61
+ throw new Error(ERR_RPC_ABORT);
62
+ }
63
+ if (this.#terminal === 'committed') {
64
+ return this.#done;
65
+ }
66
+ this.#terminal = 'committed';
67
+ this.#requestCommitted = true;
68
+ try {
69
+ await this.#call.writeCallData(undefined, true);
70
+ await this.#terminalPromise;
71
+ await this.#call.close();
72
+ this.#resolveDone();
73
+ }
74
+ catch (err) {
75
+ await this.#call.close(err instanceof Error ? err : new Error('receipt commit failed'));
76
+ this.#rejectDone(err);
77
+ throw err;
78
+ }
79
+ }
80
+ // abort sends request cancellation and never rejects.
81
+ async abort(reason) {
82
+ if (this.#terminal !== undefined) {
83
+ return;
84
+ }
85
+ this.#terminal = 'aborted';
86
+ let terminalError = reason;
87
+ try {
88
+ await this.#call.writeCallCancel(true);
89
+ }
90
+ catch (err) {
91
+ terminalError ??= err;
92
+ }
93
+ try {
94
+ await this.#call.close();
95
+ }
96
+ catch (err) {
97
+ terminalError ??= err;
98
+ }
99
+ this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT));
100
+ }
101
+ // asyncDispose aborts a receipt that has not reached a terminal.
102
+ async [Symbol.asyncDispose]() {
103
+ await this.abort();
104
+ }
105
+ }
@@ -0,0 +1 @@
1
+ export {};