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.
- package/README.md +1 -3
- package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
- package/dist/echo/echo.pb.d.ts +1 -1
- package/dist/echo/echo.pb.js +3 -3
- package/dist/integration/cross-language/ts-client.js +84 -11
- package/dist/integration/cross-language/ts-server.js +84 -4
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
- package/dist/rpcstream/rpcstream.pb.js +14 -8
- package/dist/srpc/call-receipt.d.ts +17 -0
- package/dist/srpc/call-receipt.js +105 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +374 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -1
- package/dist/srpc/common-rpc.js +86 -6
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +2 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +1 -1
- package/dist/srpc/rpcproto.pb.js +7 -7
- package/dist/srpc/server-invocation.d.ts +17 -0
- package/dist/srpc/server-invocation.js +37 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/echo/echo.pb.go +1 -1
- package/echo/echo.pb.ts +6 -5
- package/echo/echo_srpc.pb.cpp +1 -1
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/echo_srpc.pb.hpp +1 -1
- package/echo/echo_srpc.pb.rs +1 -1
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/main.go +146 -3
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +94 -6
- package/mock/mock.pb.go +1 -1
- package/mock/mock.pb.ts +6 -5
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +1 -1
- package/package.json +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +438 -0
- package/srpc/call-receipt.ts +130 -0
- package/srpc/call-receipt_test.go +536 -0
- package/srpc/client-rpc.go +1 -8
- package/srpc/client.ts +29 -2
- package/srpc/common-rpc.go +134 -29
- package/srpc/common-rpc.ts +98 -8
- package/srpc/common-rpc_test.go +52 -0
- package/srpc/handler.ts +2 -0
- package/srpc/index.ts +4 -0
- package/srpc/invoker.ts +10 -5
- package/srpc/msg-stream.go +8 -0
- package/srpc/muxed-conn.go +8 -3
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/rpcproto.pb.go +1 -1
- package/srpc/rpcproto.pb.ts +28 -27
- package/srpc/server-invocation.go +58 -0
- package/srpc/server-invocation.ts +82 -0
- package/srpc/server-rpc-invoke.go +7 -0
- package/srpc/server-rpc-invoke_goscript.go +12 -0
- package/srpc/server-rpc-invoke_goscript_test.go +27 -0
- package/srpc/server-rpc.go +9 -2
- package/srpc/server-rpc.ts +6 -2
- 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
|
-
|
|
84
|
-
|
|
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' "$
|
|
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' "$
|
|
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
|
-
|
|
156
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
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
|
|
6
|
-
import { createMessageType
|
|
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
|
-
|
|
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
|
-
]
|
|
28
|
+
] satisfies readonly PartialFieldInfo[],
|
|
28
29
|
packedByDefault: true,
|
|
29
30
|
})
|
package/mock/mock_srpc.pb.cpp
CHANGED
package/mock/mock_srpc.pb.go
CHANGED
package/mock/mock_srpc.pb.hpp
CHANGED
package/mock/mock_srpc.pb.rs
CHANGED
package/package.json
CHANGED
|
@@ -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
|
+
}
|