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
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, TerminalKind } 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,15 @@ 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
+ if (receiptCompletion) {
50
+ emitReceiptEvent('SERVER_RECEIPT_ACK_WRITE committed');
51
+ }
52
+ const writeDone = new Promise((resolve, reject) => {
29
53
  socket.write(data, (err) => {
30
54
  if (err)
31
55
  reject(err);
@@ -33,15 +57,82 @@ function tcpSocketToPacketStream(socket) {
33
57
  resolve();
34
58
  });
35
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 ${terminalName(terminal)}`);
108
+ if (terminal !== TerminalKind.COMMITTED) {
109
+ finishReceiptServer();
110
+ }
111
+ })());
112
+ };
113
+ });
114
+ }
115
+ function terminalName(terminal) {
116
+ switch (terminal) {
117
+ case TerminalKind.COMMITTED:
118
+ return 'committed';
119
+ case TerminalKind.CANCELED:
120
+ return 'canceled';
121
+ case TerminalKind.TRANSPORT_LOST:
122
+ return 'transportLost';
123
+ case TerminalKind.CLOSED:
124
+ return 'closed';
125
+ case TerminalKind.ABANDONED:
126
+ return 'abandoned';
127
+ default:
128
+ return 'unknown';
129
+ }
130
+ }
42
131
  const server = new Server(mux.lookupMethod);
43
- const echoer = new EchoerServer(server);
44
- mux.register(createHandler(EchoerDefinition, echoer));
132
+ if (!receiptMode) {
133
+ const echoer = new EchoerServer(server);
134
+ mux.register(createHandler(EchoerDefinition, echoer));
135
+ }
45
136
  const tcpServer = net.createServer((socket) => {
46
137
  const stream = tcpSocketToPacketStream(socket);
47
138
  server.handlePacketStream(stream);
@@ -50,6 +141,11 @@ tcpServer.listen(0, '127.0.0.1', () => {
50
141
  const addr = tcpServer.address();
51
142
  console.log(`LISTENING ${addr.address}:${addr.port}`);
52
143
  });
144
+ if (receiptMode) {
145
+ void receiptServerDone.promise.then(() => {
146
+ tcpServer.close();
147
+ });
148
+ }
53
149
  process.on('SIGINT', () => {
54
150
  tcpServer.close();
55
151
  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,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 },
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ /// <reference lib="es2024.promise" />
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Client } from '../srpc/client.js';
4
+ import { Server } from '../srpc/server.js';
5
+ import { TerminalKind } from '../srpc/rpcproto.pb.js';
6
+ import { buildRpcStreamOpenStream, handleRpcStream } from './rpcstream.js';
7
+ const response = new Uint8Array([4, 5, 6]);
8
+ // This guard mirrors the Go rpcstream/receipt_test.go nested tunnel: an
9
+ // ExecCallReceipt-equivalent held unary call runs through a real handleRpcStream
10
+ // tunnel. The inner generated handler sends its single response, then the
11
+ // wrapping invocation synchronously holds on waitTerminal. The first response
12
+ // must be client-visible while the exact invocation stays open for commit.
13
+ describe('rpcstream held receipt tunnel', () => {
14
+ it('delivers the first inner response while the invocation stays held', async () => {
15
+ const terminal = Promise.withResolvers();
16
+ const innerServer = new Server(async () => {
17
+ return async (_source, dataSink, invocation) => {
18
+ await dataSink((async function* () {
19
+ yield response;
20
+ if (!invocation) {
21
+ throw new Error('missing invocation');
22
+ }
23
+ terminal.resolve(await invocation.waitTerminal(new AbortController().signal));
24
+ })());
25
+ };
26
+ });
27
+ const caller = (packetTx) => handleRpcStream(packetTx[Symbol.asyncIterator](), async () => innerServer.rpcStreamHandler);
28
+ const client = new Client(buildRpcStreamOpenStream('component-a', caller));
29
+ const held = await client.requestWithReceipt('test.Service', 'Unary', response);
30
+ // The first inner response is client-visible while the invocation is held.
31
+ expect(held.response).toEqual(response);
32
+ // The inner invocation must still be open: no terminal observed yet.
33
+ const raced = await Promise.race([
34
+ terminal.promise.then(() => 'terminal'),
35
+ Promise.resolve('pending'),
36
+ ]);
37
+ expect(raced).toBe('pending');
38
+ await expect(held.receipt.commit()).resolves.toBeUndefined();
39
+ await expect(terminal.promise).resolves.toBe(TerminalKind.COMMITTED);
40
+ });
41
+ });
@@ -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,106 @@
1
+ /// <reference lib="es2024.promise" />
2
+ import { ERR_RPC_ABORT } from './errors.js';
3
+ import { TerminalKind } from './rpcproto.pb.js';
4
+ // CallReceipt holds a unary call until it is committed or aborted.
5
+ export class CallReceipt {
6
+ #call;
7
+ #iterator;
8
+ #terminalPromise;
9
+ #terminal;
10
+ #done;
11
+ #requestCommitted = false;
12
+ #resolveDone;
13
+ #rejectDone;
14
+ constructor(call, iterator) {
15
+ this.#call = call;
16
+ this.#iterator = iterator;
17
+ const { promise, resolve, reject } = Promise.withResolvers();
18
+ this.#done = promise;
19
+ this.#resolveDone = resolve;
20
+ this.#rejectDone = reject;
21
+ this.#done.catch(() => undefined);
22
+ this.#terminalPromise = this.#observeTerminal();
23
+ this.#terminalPromise.catch(() => undefined);
24
+ }
25
+ async #observeTerminal() {
26
+ try {
27
+ const result = await this.#iterator.next();
28
+ if (!result.done) {
29
+ throw new Error('unexpected trailing response data');
30
+ }
31
+ const terminal = this.#call.getTerminalKind();
32
+ if (terminal !== TerminalKind.COMMITTED ||
33
+ !this.#requestCommitted ||
34
+ this.#terminal !== 'committed') {
35
+ throw new Error('receipt closed before commit');
36
+ }
37
+ return result;
38
+ }
39
+ catch (err) {
40
+ this.#rejectDone(err);
41
+ const error = err instanceof Error ? err : new Error('receipt terminal failed');
42
+ try {
43
+ await this.#call.close(error);
44
+ }
45
+ catch {
46
+ // The primary terminal error is already recorded on the receipt.
47
+ }
48
+ throw err;
49
+ }
50
+ }
51
+ // done resolves after committed close and rejects on terminal failure.
52
+ get done() {
53
+ return this.#done;
54
+ }
55
+ // settled reports whether a terminal transition has been claimed.
56
+ get settled() {
57
+ return this.#terminal !== undefined;
58
+ }
59
+ // commit sends request completion and waits for server finalization.
60
+ async commit() {
61
+ if (this.#terminal === 'aborted') {
62
+ throw new Error(ERR_RPC_ABORT);
63
+ }
64
+ if (this.#terminal === 'committed') {
65
+ return this.#done;
66
+ }
67
+ this.#terminal = 'committed';
68
+ this.#requestCommitted = true;
69
+ try {
70
+ await this.#call.writeCallData(undefined, true);
71
+ await this.#terminalPromise;
72
+ await this.#call.close();
73
+ this.#resolveDone();
74
+ }
75
+ catch (err) {
76
+ await this.#call.close(err instanceof Error ? err : new Error('receipt commit failed'));
77
+ this.#rejectDone(err);
78
+ throw err;
79
+ }
80
+ }
81
+ // abort sends request cancellation and never rejects.
82
+ async abort(reason) {
83
+ if (this.#terminal !== undefined) {
84
+ return;
85
+ }
86
+ this.#terminal = 'aborted';
87
+ let terminalError = reason;
88
+ try {
89
+ await this.#call.writeCallCancel(true);
90
+ }
91
+ catch (err) {
92
+ terminalError ??= err;
93
+ }
94
+ try {
95
+ await this.#call.close();
96
+ }
97
+ catch (err) {
98
+ terminalError ??= err;
99
+ }
100
+ this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT));
101
+ }
102
+ // asyncDispose aborts a receipt that has not reached a terminal.
103
+ async [Symbol.asyncDispose]() {
104
+ await this.abort();
105
+ }
106
+ }
@@ -0,0 +1 @@
1
+ export {};