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
@@ -20,6 +20,8 @@ PASSED=0
20
20
  FAILED=0
21
21
  ERRORS=""
22
22
 
23
+ SERVER_LOG=""
24
+
23
25
  # should_run checks if a test name matches the active filters.
24
26
  # Returns 0 (true) if the test should run, 1 (false) otherwise.
25
27
  should_run() {
@@ -77,27 +79,26 @@ RUST_CLIENT="$REPO_DIR/target/release/integration-client"
77
79
  CPP_SERVER="$REPO_DIR/build/cpp-integration-server"
78
80
  CPP_CLIENT="$REPO_DIR/build/cpp-integration-client"
79
81
 
80
- # Start a server and capture its address.
81
- # Sets SERVER_PID and SERVER_ADDR.
82
82
  start_server() {
83
- local addr_file
84
- addr_file=$(mktemp)
85
- "$@" > "$addr_file" 2>&1 &
83
+ SERVER_LOG=$(mktemp)
84
+ "$@" > "$SERVER_LOG" 2>&1 &
86
85
  SERVER_PID=$!
87
86
  # Wait for LISTENING output (up to 3 seconds).
88
87
  local waited=0
89
88
  while [ $waited -lt 30 ]; do
90
- if grep -q 'LISTENING' "$addr_file" 2>/dev/null; then
89
+ if grep -q 'LISTENING' "$SERVER_LOG" 2>/dev/null; then
91
90
  break
92
91
  fi
93
92
  sleep 0.1
94
93
  waited=$((waited + 1))
95
94
  done
96
- SERVER_ADDR=$(grep 'LISTENING' "$addr_file" 2>/dev/null | awk '{print $2}')
97
- rm -f "$addr_file"
95
+ SERVER_ADDR=$(grep 'LISTENING' "$SERVER_LOG" 2>/dev/null | awk '{print $2}')
98
96
  if [ -z "$SERVER_ADDR" ]; then
99
97
  echo "FAILED: server did not start"
100
98
  kill $SERVER_PID 2>/dev/null || true
99
+ wait $SERVER_PID 2>/dev/null || true
100
+ rm -f "$SERVER_LOG"
101
+ SERVER_LOG=""
101
102
  return 1
102
103
  fi
103
104
  return 0
@@ -110,6 +111,27 @@ stop_server() {
110
111
 
111
112
  # run_pair <test_name> <server_args...> -- <client_args...>
112
113
  # The client receives $SERVER_ADDR as its last argument.
114
+ # check_receipt_events verifies terminal markers after the receipt server
115
+ # naturally completes, so receipt synchronization is event/process-backed.
116
+ check_receipt_events() {
117
+ local expected="$1"
118
+ local event_log="$2"
119
+ awk -v expected="$expected" '
120
+ $0 == "SERVER_RECEIPT_TERMINAL " expected { terminal = NR; next }
121
+ $0 == "SERVER_RECEIPT_ACK " expected { ack = NR; next }
122
+ $0 == "CLIENT_RECEIPT_RESOLVED " expected { client = NR; next }
123
+ END {
124
+ if (!terminal || !client) {
125
+ exit 1
126
+ }
127
+ if (expected == "committed" &&
128
+ (!ack || terminal >= ack || ack >= client)) {
129
+ exit 1
130
+ }
131
+ }
132
+ ' "$event_log"
133
+ }
134
+
113
135
  run_pair() {
114
136
  local test_name="$1"
115
137
  shift
@@ -134,30 +156,104 @@ run_pair() {
134
156
  fi
135
157
  done
136
158
 
159
+ local expected_terminal=""
160
+ case "$test_name" in
161
+ *"receipt commit") expected_terminal="committed" ;;
162
+ *"receipt abort") expected_terminal="canceled" ;;
163
+ *"receipt loss") expected_terminal="transportLost" ;;
164
+ *"receipt bare-close") expected_terminal="closed" ;;
165
+ esac
166
+
137
167
  echo -n " ${test_name}... "
138
168
 
169
+ local event_fifo=""
170
+ local event_log=""
171
+ local event_reader=""
172
+ if [ -n "$expected_terminal" ]; then
173
+ event_fifo=$(mktemp)
174
+ rm -f "$event_fifo"
175
+ mkfifo "$event_fifo"
176
+ event_log=$(mktemp)
177
+ exec 3<>"$event_fifo"
178
+ (
179
+ exec 3>&-
180
+ cat "$event_fifo" > "$event_log"
181
+ ) &
182
+ event_reader=$!
183
+ export RECEIPT_EVENT_FIFO="$event_fifo"
184
+ else
185
+ unset RECEIPT_EVENT_FIFO
186
+ fi
187
+
139
188
  if ! start_server "${srv_args[@]}"; then
140
189
  echo "FAILED (server start)"
141
190
  FAILED=$((FAILED + 1))
142
191
  ERRORS="${ERRORS}\n ${test_name} (server start failed)"
192
+ if [ -n "$event_reader" ]; then
193
+ exec 3>&-
194
+ kill "$event_reader" 2>/dev/null || true
195
+ wait "$event_reader" 2>/dev/null || true
196
+ rm -f "$event_fifo" "$event_log"
197
+ fi
198
+ unset RECEIPT_EVENT_FIFO
143
199
  return
144
200
  fi
145
201
 
146
202
  local client_out
147
203
  client_out=$(mktemp)
204
+ local client_ok=false
205
+ local server_ok=true
148
206
  if "${cli_args[@]}" "$SERVER_ADDR" > "$client_out" 2>&1; then
207
+ client_ok=true
208
+ fi
209
+
210
+ if [ -n "$event_reader" ]; then
211
+ if $client_ok; then
212
+ if ! wait "$SERVER_PID"; then
213
+ server_ok=false
214
+ fi
215
+ else
216
+ stop_server
217
+ wait "$SERVER_PID" 2>/dev/null || true
218
+ server_ok=false
219
+ fi
220
+ exec 3>&-
221
+ wait "$event_reader" 2>/dev/null || true
222
+ else
223
+ stop_server
224
+ fi
225
+ unset RECEIPT_EVENT_FIFO
226
+
227
+ local terminal_ok=true
228
+ if [ -n "$expected_terminal" ] &&
229
+ ! check_receipt_events "$expected_terminal" "$event_log"; then
230
+ terminal_ok=false
231
+ fi
232
+
233
+ if $client_ok && $server_ok && $terminal_ok; then
149
234
  echo "PASSED"
235
+ if [ -n "$expected_terminal" ]; then
236
+ echo " receipt events:"
237
+ sed 's/^/ /' "$event_log"
238
+ fi
150
239
  PASSED=$((PASSED + 1))
151
240
  else
152
241
  echo "FAILED"
153
242
  FAILED=$((FAILED + 1))
154
243
  ERRORS="${ERRORS}\n ${test_name}"
155
- echo " client output:"
156
- sed 's/^/ /' "$client_out"
244
+ if ! $client_ok; then
245
+ echo " client output:"
246
+ sed 's/^/ /' "$client_out"
247
+ fi
248
+ if ! $terminal_ok; then
249
+ echo " receipt events:"
250
+ sed 's/^/ /' "$event_log"
251
+ echo " server output:"
252
+ sed 's/^/ /' "$SERVER_LOG"
253
+ fi
157
254
  fi
158
- rm -f "$client_out"
159
-
160
- stop_server
255
+ rm -f "$client_out" "$SERVER_LOG" "$event_fifo" "$event_log"
256
+ SERVER_LOG=""
161
257
  }
162
258
 
163
259
  echo ""
@@ -168,6 +264,10 @@ echo ""
168
264
  run_pair "go-server + go-client" "$GO_SERVER" -- "$GO_CLIENT"
169
265
  run_pair "go-server + rust-client" "$GO_SERVER" -- "$RUST_CLIENT"
170
266
  run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT"
267
+ run_pair "go-server + ts-client receipt commit" "$GO_SERVER" receipt commit -- node "$TS_CLIENT" receipt commit
268
+ run_pair "go-server + ts-client receipt abort" "$GO_SERVER" receipt abort -- node "$TS_CLIENT" receipt abort
269
+ run_pair "go-server + ts-client receipt loss" "$GO_SERVER" receipt loss -- node "$TS_CLIENT" receipt loss
270
+ run_pair "go-server + ts-client receipt bare-close" "$GO_SERVER" receipt bare-close -- node "$TS_CLIENT" receipt bare-close
171
271
  run_pair "go-server + cpp-client" "$GO_SERVER" -- "$CPP_CLIENT"
172
272
 
173
273
  # Rust server combinations
@@ -178,6 +278,10 @@ run_pair "rust-server + cpp-client" "$RUST_SERVER" -- "$CPP_CLIENT"
178
278
 
179
279
  # TypeScript server combinations
180
280
  run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT"
281
+ run_pair "ts-server + go-client receipt commit" node "$TS_SERVER" receipt commit -- "$GO_CLIENT" receipt commit
282
+ run_pair "ts-server + go-client receipt abort" node "$TS_SERVER" receipt abort -- "$GO_CLIENT" receipt abort
283
+ run_pair "ts-server + go-client receipt loss" node "$TS_SERVER" receipt loss -- "$GO_CLIENT" receipt loss
284
+ run_pair "ts-server + go-client receipt bare-close" node "$TS_SERVER" receipt bare-close -- "$GO_CLIENT" receipt bare-close
181
285
  run_pair "ts-server + rust-client" node "$TS_SERVER" -- "$RUST_CLIENT"
182
286
  run_pair "ts-server + ts-client" node "$TS_SERVER" -- node "$TS_CLIENT"
183
287
  run_pair "ts-server + cpp-client" node "$TS_SERVER" -- "$CPP_CLIENT"
@@ -1,4 +1,5 @@
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'
@@ -7,9 +8,22 @@ import {
7
8
  prependLengthPrefixTransform,
8
9
  } from '../../srpc/packet.js'
9
10
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
10
- import { runClientTest } from '../../echo/client-test.js'
11
+ import { EchoMsg, runClientTest } from '../../echo/index.js'
11
12
  import type { OpenStreamFunc, PacketStream } from '../../srpc/stream.js'
12
13
  import type { Source } from 'it-stream-types'
14
+ function emitReceiptEvent(line: string): void {
15
+ console.log(line)
16
+ const fifo = process.env.RECEIPT_EVENT_FIFO
17
+ if (!fifo) {
18
+ return
19
+ }
20
+ const fd = openSync(fifo, 'w')
21
+ try {
22
+ writeSync(fd, `${line}\n`)
23
+ } finally {
24
+ closeSync(fd)
25
+ }
26
+ }
13
27
 
14
28
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
15
29
  function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
@@ -32,8 +46,7 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
32
46
  source: socketSource(),
33
47
  sink: async (source: Source<Uint8Array>): Promise<void> => {
34
48
  for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
35
- const data =
36
- chunk instanceof Uint8Array ? chunk : (chunk as any).subarray()
49
+ const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
37
50
  await new Promise<void>((resolve, reject) => {
38
51
  socket.write(data, (err) => {
39
52
  if (err) reject(err)
@@ -47,30 +60,98 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
47
60
  }
48
61
 
49
62
  async function main() {
50
- const addr = process.argv[2]
63
+ const receiptMode = process.argv[2] === 'receipt'
64
+ const receiptCase = receiptMode ? process.argv[3] : undefined
65
+ const addr = receiptMode ? process.argv[4] : process.argv[2]
51
66
  if (!addr) {
52
- console.error('usage: ts-client <host:port>')
67
+ console.error('usage: ts-client [receipt <case>] <host:port>')
53
68
  process.exit(1)
54
69
  }
70
+ if (
71
+ receiptMode &&
72
+ !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')
73
+ ) {
74
+ throw new Error(`unknown receipt case: ${receiptCase}`)
75
+ }
55
76
 
56
77
  const [host, portStr] = addr.split(':')
57
78
  const port = parseInt(portStr, 10)
79
+ let activeSocket: net.Socket | undefined
58
80
 
59
81
  const openStream: OpenStreamFunc = async (): Promise<PacketStream> => {
60
- return new Promise((resolve, reject) => {
61
- const socket = net.connect(port, host, () => {
62
- resolve(tcpSocketToPacketStream(socket))
63
- })
64
- socket.on('error', reject)
82
+ const { promise, resolve, reject } = Promise.withResolvers<PacketStream>()
83
+ const socket = net.connect(port, host, () => {
84
+ activeSocket = socket
85
+ resolve(tcpSocketToPacketStream(socket))
65
86
  })
87
+ socket.on('error', reject)
88
+ return promise
66
89
  }
67
90
 
68
91
  const client = new Client(openStream)
69
92
 
70
- console.log('Running client test via TCP...')
71
- await runClientTest(client)
93
+ if (receiptMode) {
94
+ console.log(`Running held receipt test via TCP (${receiptCase})...`)
95
+ const request = EchoMsg.create({ body: 'held receipt' })
96
+ const held = await client.requestWithReceipt(
97
+ 'echo.Echoer',
98
+ 'Echo',
99
+ EchoMsg.toBinary(request),
100
+ )
101
+ const result = EchoMsg.fromBinary(held.response)
102
+ if (result.body !== request.body) {
103
+ throw new Error(`expected ${request.body}, got ${result.body}`)
104
+ }
105
+ switch (receiptCase) {
106
+ case 'commit':
107
+ await held.receipt.commit()
108
+ break
109
+ case 'abort':
110
+ await held.receipt.abort()
111
+ break
112
+ case 'loss':
113
+ activeSocket?.resetAndDestroy()
114
+ await expectReceiptFailure(held.receipt)
115
+ break
116
+ case 'bare-close':
117
+ activeSocket?.end()
118
+ await expectReceiptFailure(held.receipt)
119
+ break
120
+ }
121
+ emitReceiptEvent(
122
+ `CLIENT_RECEIPT_RESOLVED ${receiptTerminalName(receiptCase ?? '')}`,
123
+ )
124
+ } else {
125
+ console.log('Running client test via TCP...')
126
+ await runClientTest(client)
127
+ }
72
128
  console.log('All tests passed.')
73
- process.exit(0)
129
+ }
130
+
131
+ function receiptTerminalName(receiptCase: string): string {
132
+ switch (receiptCase) {
133
+ case 'commit':
134
+ return 'committed'
135
+ case 'abort':
136
+ return 'canceled'
137
+ case 'loss':
138
+ return 'transportLost'
139
+ case 'bare-close':
140
+ return 'closed'
141
+ default:
142
+ return 'unknown'
143
+ }
144
+ }
145
+
146
+ async function expectReceiptFailure(receipt: {
147
+ commit(): Promise<void>
148
+ }): Promise<void> {
149
+ try {
150
+ await receipt.commit()
151
+ } catch {
152
+ return
153
+ }
154
+ throw new Error('receipt commit unexpectedly succeeded')
74
155
  }
75
156
 
76
157
  process.on('unhandledRejection', (ev) => {
@@ -1,4 +1,5 @@
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'
@@ -7,10 +8,24 @@ import {
7
8
  prependLengthPrefixTransform,
8
9
  } from '../../srpc/packet.js'
9
10
  import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
10
- import { EchoerServer } from '../../echo/server.js'
11
+ import { EchoerServer, EchoMsg } from '../../echo/index.js'
11
12
  import { EchoerDefinition } from '../../echo/echo_srpc.pb.js'
13
+ import { Packet } from '../../srpc/rpcproto.pb.js'
12
14
  import type { PacketStream } from '../../srpc/stream.js'
13
15
  import type { Source } from 'it-stream-types'
16
+ function emitReceiptEvent(line: string): void {
17
+ console.log(line)
18
+ const fifo = process.env.RECEIPT_EVENT_FIFO
19
+ if (!fifo) {
20
+ return
21
+ }
22
+ const fd = openSync(fifo, 'w')
23
+ try {
24
+ writeSync(fd, `${line}\n`)
25
+ } finally {
26
+ closeSync(fd)
27
+ }
28
+ }
14
29
 
15
30
  // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
16
31
  // Each Uint8Array in source/sink is one packet (no length prefix).
@@ -35,14 +50,26 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
35
50
  source: socketSource(),
36
51
  sink: async (source: Source<Uint8Array>): Promise<void> => {
37
52
  for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
38
- const data =
39
- chunk instanceof Uint8Array ? chunk : (chunk as any).subarray()
40
- await new Promise<void>((resolve, reject) => {
53
+ const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
54
+ const packet = Packet.fromBinary(data.subarray(4))
55
+ const receiptCompletion =
56
+ receiptMode &&
57
+ packet.body?.case === 'callData' &&
58
+ packet.body.value.complete &&
59
+ !packet.body.value.error
60
+ const writeDone = new Promise<void>((resolve, reject) => {
41
61
  socket.write(data, (err) => {
42
62
  if (err) reject(err)
43
63
  else resolve()
44
64
  })
45
65
  })
66
+ if (receiptCompletion) {
67
+ emitReceiptEvent('SERVER_RECEIPT_ACK committed')
68
+ }
69
+ await writeDone
70
+ if (receiptCompletion) {
71
+ finishReceiptServer()
72
+ }
46
73
  }
47
74
  socket.end()
48
75
  },
@@ -50,9 +77,64 @@ function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
50
77
  }
51
78
 
52
79
  const mux = createMux()
80
+ const receiptMode = process.argv[2] === 'receipt'
81
+ const receiptCase = receiptMode ? process.argv[3] : undefined
82
+ if (
83
+ receiptMode &&
84
+ !['commit', 'abort', 'loss', 'bare-close'].includes(receiptCase ?? '')
85
+ ) {
86
+ console.error(`unknown receipt case: ${receiptCase}`)
87
+ process.exit(1)
88
+ }
89
+
90
+ const receiptServerDone = Promise.withResolvers<void>()
91
+ let receiptServerFinished = false
92
+ function finishReceiptServer(): void {
93
+ if (!receiptServerFinished) {
94
+ receiptServerFinished = true
95
+ receiptServerDone.resolve()
96
+ }
97
+ }
98
+
99
+ if (receiptMode) {
100
+ mux.registerLookupMethod(async (serviceID, methodID) => {
101
+ if (serviceID !== 'echo.Echoer' || methodID !== 'Echo') {
102
+ return null
103
+ }
104
+ return async (dataSource, dataSink, invocation) => {
105
+ let requestData: Uint8Array | undefined
106
+ for await (const data of dataSource) {
107
+ requestData = data
108
+ break
109
+ }
110
+ if (!requestData) {
111
+ throw new Error('receipt request was empty')
112
+ }
113
+ const requestMsg = EchoMsg.fromBinary(requestData)
114
+ await dataSink(
115
+ (async function* () {
116
+ yield EchoMsg.toBinary(requestMsg)
117
+ if (!invocation) {
118
+ throw new Error('receipt invocation was missing')
119
+ }
120
+ emitReceiptEvent('SERVER_RECEIPT_WAITING')
121
+ const terminal = await invocation.waitTerminal(
122
+ new AbortController().signal,
123
+ )
124
+ emitReceiptEvent(`SERVER_RECEIPT_TERMINAL ${terminal}`)
125
+ if (terminal !== 'committed') {
126
+ finishReceiptServer()
127
+ }
128
+ })(),
129
+ )
130
+ }
131
+ })
132
+ }
53
133
  const server = new Server(mux.lookupMethod)
54
- const echoer = new EchoerServer(server)
55
- mux.register(createHandler(EchoerDefinition, echoer))
134
+ if (!receiptMode) {
135
+ const echoer = new EchoerServer(server)
136
+ mux.register(createHandler(EchoerDefinition, echoer))
137
+ }
56
138
 
57
139
  const tcpServer = net.createServer((socket) => {
58
140
  const stream = tcpSocketToPacketStream(socket)
@@ -64,6 +146,12 @@ tcpServer.listen(0, '127.0.0.1', () => {
64
146
  console.log(`LISTENING ${addr.address}:${addr.port}`)
65
147
  })
66
148
 
149
+ if (receiptMode) {
150
+ void receiptServerDone.promise.then(() => {
151
+ tcpServer.close()
152
+ })
153
+ }
154
+
67
155
  process.on('SIGINT', () => {
68
156
  tcpServer.close()
69
157
  process.exit(0)
package/mock/mock.pb.go CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-go-lite. DO NOT EDIT.
2
- // protoc-gen-go-lite version: v0.12.1
2
+ // protoc-gen-go-lite version: v0.13.0
3
3
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
4
4
 
5
5
  package e2e_mock
package/mock/mock.pb.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  // @generated from file github.com/aperturerobotics/starpc/mock/mock.proto (package e2e.mock, syntax proto3)
3
3
  /* eslint-disable */
4
4
 
5
- import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite'
6
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite'
5
+ import type { MessageType } from '@aptre/protobuf-es-lite/message'
6
+ import { createMessageType } from '@aptre/protobuf-es-lite/message'
7
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar'
8
+ import type { PartialFieldInfo } from '@aptre/protobuf-es-lite/field'
7
9
 
8
10
  export const protobufPackage = 'e2e.mock'
9
11
 
@@ -19,11 +21,10 @@ export interface MockMsg {
19
21
  body?: string
20
22
  }
21
23
 
22
- // MockMsg contains the message type declaration for MockMsg.
23
- export const MockMsg: MessageType<MockMsg> = createMessageType({
24
+ export const MockMsg: MessageType<MockMsg> = /* @__PURE__ */ createMessageType({
24
25
  typeName: 'e2e.mock.MockMsg',
25
26
  fields: [
26
27
  { no: 1, name: 'body', kind: 'scalar', T: ScalarType.STRING },
27
- ] as readonly PartialFieldInfo[],
28
+ ] satisfies readonly PartialFieldInfo[],
28
29
  packedByDefault: true,
29
30
  })
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.46.1
4
+ // protoc-gen-starpc-cpp version: v0.49.7
5
5
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
6
6
 
7
7
  #include "mock_srpc.pb.hpp"
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-srpc. DO NOT EDIT.
2
- // protoc-gen-srpc version: v0.46.1
2
+ // protoc-gen-srpc version: v0.49.7
3
3
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
4
4
 
5
5
  package e2e_mock
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only && cgo
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.46.1
4
+ // protoc-gen-starpc-cpp version: v0.49.7
5
5
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
6
6
 
7
7
  #pragma once
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-starpc-rust. DO NOT EDIT.
2
- // protoc-gen-starpc-rust version: v0.46.1
2
+ // protoc-gen-starpc-rust version: v0.49.7
3
3
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
4
4
 
5
5
  #[allow(unused_imports)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.49.17",
3
+ "version": "0.49.20",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -0,0 +1,111 @@
1
+ package srpc_test
2
+
3
+ import (
4
+ "context"
5
+ "net"
6
+ "testing"
7
+
8
+ yamux "github.com/libp2p/go-yamux/v4"
9
+
10
+ "github.com/aperturerobotics/starpc/echo"
11
+ "github.com/aperturerobotics/starpc/srpc"
12
+ )
13
+
14
+ type receiptEchoServer struct {
15
+ echo.SRPCEchoerServer
16
+ invocationFound chan bool
17
+ }
18
+
19
+ func (s *receiptEchoServer) Echo(
20
+ ctx context.Context,
21
+ msg *echo.EchoMsg,
22
+ ) (*echo.EchoMsg, error) {
23
+ _, found := srpc.GetServerInvocation(ctx)
24
+ s.invocationFound <- found
25
+ return msg.CloneVT(), nil
26
+ }
27
+
28
+ func TestCallReceiptGoGo(t *testing.T) {
29
+ clientConn, serverConn := net.Pipe()
30
+ clientMux, err := srpc.NewMuxedConn(clientConn, true, nil)
31
+ if err != nil {
32
+ t.Fatalf("client mux: %v", err)
33
+ }
34
+ serverMux, err := srpc.NewMuxedConn(serverConn, false, nil)
35
+ if err != nil {
36
+ _ = clientMux.Close()
37
+ t.Fatalf("server mux: %v", err)
38
+ }
39
+
40
+ mux := srpc.NewMux()
41
+ echoServer := &receiptEchoServer{
42
+ SRPCEchoerServer: echo.NewEchoServer(mux),
43
+ invocationFound: make(chan bool, 1),
44
+ }
45
+ if err := echo.SRPCRegisterEchoer(mux, echoServer); err != nil {
46
+ t.Fatalf("register echo: %v", err)
47
+ }
48
+
49
+ terminal := make(chan srpc.TerminalKind, 1)
50
+ invoker := srpc.InvokerFunc(func(
51
+ serviceID, methodID string,
52
+ strm srpc.Stream,
53
+ ) (bool, error) {
54
+ handled, err := mux.InvokeMethod(serviceID, methodID, strm)
55
+ if err != nil || !handled {
56
+ return handled, err
57
+ }
58
+ invocation, ok := srpc.GetServerInvocation(strm.Context())
59
+ if !ok {
60
+ return true, context.Canceled
61
+ }
62
+ kind, err := invocation.WaitTerminal(context.Background())
63
+ if err == nil {
64
+ terminal <- kind
65
+ }
66
+ return true, err
67
+ })
68
+ server := srpc.NewServer(invoker)
69
+ serverCtx, serverCancel := context.WithCancel(context.Background())
70
+ serverErr := make(chan error, 1)
71
+ go func() {
72
+ serverErr <- server.AcceptMuxedConn(serverCtx, serverMux)
73
+ }()
74
+ t.Cleanup(func() {
75
+ if err := serverMux.Close(); err != nil {
76
+ t.Errorf("close server mux: %v", err)
77
+ }
78
+ acceptErr := <-serverErr
79
+ serverCancel()
80
+ if acceptErr != yamux.ErrSessionShutdown {
81
+ t.Errorf("server accept: %v, want session shutdown", acceptErr)
82
+ }
83
+ if err := clientMux.Close(); err != nil {
84
+ t.Errorf("close client mux: %v", err)
85
+ }
86
+ })
87
+
88
+ client := srpc.NewClientWithMuxedConn(clientMux)
89
+ out := new(echo.EchoMsg)
90
+ receipt, err := srpc.ExecCallReceipt(
91
+ context.Background(), client, echo.SRPCEchoerServiceID, "Echo",
92
+ &echo.EchoMsg{Body: "held"}, out,
93
+ )
94
+ if err != nil {
95
+ t.Fatalf("exec receipt: %v", err)
96
+ }
97
+ if err := receipt.Commit(); err != nil {
98
+ t.Fatalf("commit: %v", err)
99
+ }
100
+ if out.GetBody() != "held" {
101
+ t.Fatalf("response body = %q, want held", out.GetBody())
102
+ }
103
+ found := <-echoServer.invocationFound
104
+ if !found {
105
+ t.Fatal("generated handler context lost server invocation")
106
+ }
107
+ kind := <-terminal
108
+ if kind != srpc.TerminalCommitted {
109
+ t.Fatalf("terminal = %v, want committed", kind)
110
+ }
111
+ }