starpc 0.51.0 → 0.52.1
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/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/echo/echo_srpc.pb.d.ts +44 -1
- package/dist/echo/server.d.ts +9 -8
- package/dist/echo/server.js +6 -6
- package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/mock/mock_srpc.pb.d.ts +14 -1
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/channel.js +6 -3
- package/dist/srpc/channel.test.js +20 -1
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/handler.d.ts +12 -3
- package/dist/srpc/index.d.ts +2 -0
- package/dist/srpc/index.js +1 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server-context.d.ts +11 -0
- package/dist/srpc/server-context.js +28 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +78 -6
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/srpc/watchdog.test.js +1 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.ts +74 -0
- package/echo/echo_srpc.py +306 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/server.ts +24 -5
- package/go.mod +2 -2
- package/go.sum +14 -0
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.pb.ts +19 -1
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +20 -6
- package/srpc/__init__.py +0 -0
- package/srpc/channel.test.ts +21 -1
- package/srpc/channel.ts +7 -3
- package/srpc/client.ts +20 -4
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/handler.ts +54 -4
- package/srpc/index.ts +7 -0
- package/srpc/invoker.ts +23 -6
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-context.ts +55 -0
- package/srpc/server-rpc.ts +4 -1
- package/srpc/server.test.ts +100 -5
- package/srpc/server.ts +22 -6
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
- package/srpc/watchdog.test.ts +1 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Cross-language integration tests for starpc.
|
|
3
|
-
# Runs all
|
|
3
|
+
# Runs all 21 server/client combinations across Go, TypeScript, Rust, C++, and Python.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# ./run.bash
|
|
7
|
-
# ./run.bash go:ts
|
|
8
|
-
# ./run.bash
|
|
6
|
+
# ./run.bash # Run all pairs
|
|
7
|
+
# ./run.bash go:ts # Run go-server+ts-client and ts-server+go-client
|
|
8
|
+
# ./run.bash python:go # Run python-server+go-client and go-server+python-client
|
|
9
|
+
# ./run.bash --nested ts:python # Run nested TypeScript/Python pairs
|
|
10
|
+
# ./run.bash --nested go:ts go:python ts:python # Run nested Go/TypeScript/Python pairs
|
|
9
11
|
set -eo pipefail
|
|
10
12
|
|
|
11
13
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -14,25 +16,79 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
14
16
|
# Fixes errors with the generated esm using require()
|
|
15
17
|
ESM_BANNER='import{fileURLToPath}from"node:url";import{dirname}from"node:path";import{createRequire as topLevelCreateRequire}from"node:module";const require=topLevelCreateRequire(import.meta.url);const __filename=fileURLToPath(import.meta.url);const __dirname=dirname(__filename);'
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
NESTED=false
|
|
20
|
+
FILTERS=()
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
if [ "$arg" = "--nested" ]; then
|
|
23
|
+
NESTED=true
|
|
24
|
+
else
|
|
25
|
+
FILTERS+=("$arg")
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
needs_language() {
|
|
30
|
+
local language="$1"
|
|
31
|
+
if [ ${#FILTERS[@]} -eq 0 ]; then
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
for filter in "${FILTERS[@]}"; do
|
|
35
|
+
if [[ ":${filter}:" == *":${language}:"* ]]; then
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
PASSED=0
|
|
20
43
|
FAILED=0
|
|
21
44
|
ERRORS=""
|
|
22
45
|
|
|
46
|
+
SERVER_PID=""
|
|
23
47
|
SERVER_LOG=""
|
|
48
|
+
SERVER_EXIT_STATUS=0
|
|
49
|
+
CLIENT_PID=""
|
|
50
|
+
CLIENT_OUT=""
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
cleanup() {
|
|
53
|
+
if [ -n "${CLIENT_PID:-}" ]; then
|
|
54
|
+
kill "$CLIENT_PID" 2>/dev/null || true
|
|
55
|
+
wait "$CLIENT_PID" 2>/dev/null || true
|
|
56
|
+
CLIENT_PID=""
|
|
57
|
+
fi
|
|
58
|
+
if [ -n "${SERVER_PID:-}" ]; then
|
|
59
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
60
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
61
|
+
SERVER_PID=""
|
|
62
|
+
fi
|
|
63
|
+
if [ -n "${SERVER_LOG:-}" ]; then
|
|
64
|
+
rm -f "$SERVER_LOG"
|
|
65
|
+
SERVER_LOG=""
|
|
66
|
+
fi
|
|
67
|
+
if [ -n "${CLIENT_OUT:-}" ]; then
|
|
68
|
+
rm -f "$CLIENT_OUT"
|
|
69
|
+
CLIENT_OUT=""
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
trap cleanup EXIT
|
|
74
|
+
trap 'exit 130' INT
|
|
75
|
+
trap 'exit 143' TERM
|
|
76
|
+
|
|
77
|
+
# should_run checks if a server/client pair matches the active filters.
|
|
26
78
|
# Returns 0 (true) if the test should run, 1 (false) otherwise.
|
|
27
79
|
should_run() {
|
|
28
80
|
local test_name="$1"
|
|
29
81
|
if [ ${#FILTERS[@]} -eq 0 ]; then
|
|
30
82
|
return 0
|
|
31
83
|
fi
|
|
84
|
+
local server_language="${test_name%%-server*}"
|
|
85
|
+
local client_language="${test_name#*+ }"
|
|
86
|
+
client_language="${client_language%%-client*}"
|
|
32
87
|
for filter in "${FILTERS[@]}"; do
|
|
33
88
|
local lang1="${filter%%:*}"
|
|
34
89
|
local lang2="${filter##*:}"
|
|
35
|
-
if [
|
|
90
|
+
if { [ "$server_language" = "$lang1" ] && [ "$client_language" = "$lang2" ]; } ||
|
|
91
|
+
{ [ "$server_language" = "$lang2" ] && [ "$client_language" = "$lang1" ]; }; then
|
|
36
92
|
return 0
|
|
37
93
|
fi
|
|
38
94
|
done
|
|
@@ -42,32 +98,39 @@ should_run() {
|
|
|
42
98
|
# Build all binaries.
|
|
43
99
|
echo "=== Building all integration binaries ==="
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
go build -o "$SCRIPT_DIR/go-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
echo
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
101
|
+
if needs_language go; then
|
|
102
|
+
echo "Building Go server/client..."
|
|
103
|
+
go build -o "$SCRIPT_DIR/go-server/go-server" "$SCRIPT_DIR/go-server/"
|
|
104
|
+
go build -o "$SCRIPT_DIR/go-client/go-client" "$SCRIPT_DIR/go-client/"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if needs_language ts; then
|
|
108
|
+
echo "Building TypeScript server/client..."
|
|
109
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-server.ts" \
|
|
110
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
111
|
+
--banner:js="$ESM_BANNER" \
|
|
112
|
+
--outfile="$SCRIPT_DIR/ts-server.mjs"
|
|
113
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-client.ts" \
|
|
114
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
115
|
+
--banner:js="$ESM_BANNER" \
|
|
116
|
+
--outfile="$SCRIPT_DIR/ts-client.mjs"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if needs_language rust; then
|
|
120
|
+
echo "Building Rust server/client..."
|
|
121
|
+
cargo build --release -p echo-example --bin integration-server --bin integration-client
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if needs_language cpp; then
|
|
125
|
+
echo "Vendoring Go dependencies (needed for C++ build)..."
|
|
126
|
+
go mod vendor
|
|
127
|
+
echo "Building C++ server/client..."
|
|
128
|
+
mkdir -p "$REPO_DIR/build"
|
|
129
|
+
pushd "$REPO_DIR/build" > /dev/null
|
|
130
|
+
cmake "$REPO_DIR" -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
|
|
131
|
+
cmake --build . --target cpp-integration-server cpp-integration-client --parallel > /dev/null 2>&1
|
|
132
|
+
popd > /dev/null
|
|
133
|
+
fi
|
|
71
134
|
|
|
72
135
|
# Binary paths.
|
|
73
136
|
GO_SERVER="$SCRIPT_DIR/go-server/go-server"
|
|
@@ -78,6 +141,7 @@ RUST_SERVER="$REPO_DIR/target/release/integration-server"
|
|
|
78
141
|
RUST_CLIENT="$REPO_DIR/target/release/integration-client"
|
|
79
142
|
CPP_SERVER="$REPO_DIR/build/cpp-integration-server"
|
|
80
143
|
CPP_CLIENT="$REPO_DIR/build/cpp-integration-client"
|
|
144
|
+
PYTHON=(uv run --project "$REPO_DIR" python)
|
|
81
145
|
|
|
82
146
|
start_server() {
|
|
83
147
|
SERVER_LOG=$(mktemp)
|
|
@@ -95,8 +159,9 @@ start_server() {
|
|
|
95
159
|
SERVER_ADDR=$(grep 'LISTENING' "$SERVER_LOG" 2>/dev/null | awk '{print $2}')
|
|
96
160
|
if [ -z "$SERVER_ADDR" ]; then
|
|
97
161
|
echo "FAILED: server did not start"
|
|
98
|
-
kill $SERVER_PID 2>/dev/null || true
|
|
99
|
-
wait $SERVER_PID 2>/dev/null || true
|
|
162
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
163
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
164
|
+
SERVER_PID=""
|
|
100
165
|
rm -f "$SERVER_LOG"
|
|
101
166
|
SERVER_LOG=""
|
|
102
167
|
return 1
|
|
@@ -105,8 +170,36 @@ start_server() {
|
|
|
105
170
|
}
|
|
106
171
|
|
|
107
172
|
stop_server() {
|
|
108
|
-
kill $SERVER_PID 2>/dev/null || true
|
|
109
|
-
wait $SERVER_PID 2>/dev/null
|
|
173
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
174
|
+
if wait "$SERVER_PID" 2>/dev/null; then
|
|
175
|
+
SERVER_EXIT_STATUS=0
|
|
176
|
+
else
|
|
177
|
+
SERVER_EXIT_STATUS=$?
|
|
178
|
+
fi
|
|
179
|
+
SERVER_PID=""
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
python_server_passed() {
|
|
183
|
+
local status="$1"
|
|
184
|
+
local log="$2"
|
|
185
|
+
[ "$status" -eq 0 ] && grep -qx "NESTED_CLEAN" "$log"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
verify_python_server_guard() {
|
|
189
|
+
local log
|
|
190
|
+
log="$(mktemp)"
|
|
191
|
+
printf '%s\n' "NESTED_CLEAN" >"$log"
|
|
192
|
+
if python_server_passed 1 "$log"; then
|
|
193
|
+
rm -f "$log"
|
|
194
|
+
echo "runner guard accepted a nonzero Python server" >&2
|
|
195
|
+
return 1
|
|
196
|
+
fi
|
|
197
|
+
if ! python_server_passed 0 "$log"; then
|
|
198
|
+
rm -f "$log"
|
|
199
|
+
echo "runner guard rejected a clean Python server" >&2
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
rm -f "$log"
|
|
110
203
|
}
|
|
111
204
|
|
|
112
205
|
# run_pair <test_name> <server_args...> -- <client_args...>
|
|
@@ -142,13 +235,25 @@ run_pair() {
|
|
|
142
235
|
return
|
|
143
236
|
fi
|
|
144
237
|
|
|
145
|
-
|
|
146
|
-
client_out=$(mktemp)
|
|
238
|
+
CLIENT_OUT=$(mktemp)
|
|
147
239
|
local client_ok=false
|
|
148
|
-
|
|
240
|
+
timeout 60 "${cli_args[@]}" "$SERVER_ADDR" > "$CLIENT_OUT" 2>&1 &
|
|
241
|
+
CLIENT_PID=$!
|
|
242
|
+
if wait "$CLIENT_PID"; then
|
|
149
243
|
client_ok=true
|
|
150
244
|
fi
|
|
245
|
+
CLIENT_PID=""
|
|
151
246
|
stop_server
|
|
247
|
+
if $NESTED && [[ "$test_name" == python-server* ]]; then
|
|
248
|
+
if ! verify_python_server_guard; then
|
|
249
|
+
client_ok=false
|
|
250
|
+
echo " Python server result guard regression failed"
|
|
251
|
+
elif ! python_server_passed "$SERVER_EXIT_STATUS" "$SERVER_LOG"; then
|
|
252
|
+
client_ok=false
|
|
253
|
+
echo " Python server failed nested shutdown (status $SERVER_EXIT_STATUS):"
|
|
254
|
+
sed 's/^/ /' "$SERVER_LOG"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
152
257
|
|
|
153
258
|
if $client_ok; then
|
|
154
259
|
echo "PASSED"
|
|
@@ -158,9 +263,10 @@ run_pair() {
|
|
|
158
263
|
FAILED=$((FAILED + 1))
|
|
159
264
|
ERRORS="${ERRORS}\n ${test_name}"
|
|
160
265
|
echo " client output:"
|
|
161
|
-
sed 's/^/ /' "$
|
|
266
|
+
sed 's/^/ /' "$CLIENT_OUT"
|
|
162
267
|
fi
|
|
163
|
-
rm -f "$
|
|
268
|
+
rm -f "$CLIENT_OUT" "$SERVER_LOG"
|
|
269
|
+
CLIENT_OUT=""
|
|
164
270
|
SERVER_LOG=""
|
|
165
271
|
}
|
|
166
272
|
|
|
@@ -168,29 +274,48 @@ echo ""
|
|
|
168
274
|
echo "=== Running cross-language integration tests ==="
|
|
169
275
|
echo ""
|
|
170
276
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
run_pair "go-server +
|
|
174
|
-
run_pair "go-server +
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
run_pair "
|
|
180
|
-
run_pair "
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
run_pair "
|
|
185
|
-
run_pair "
|
|
186
|
-
run_pair "
|
|
187
|
-
run_pair "
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
run_pair "
|
|
191
|
-
run_pair "
|
|
192
|
-
run_pair "
|
|
193
|
-
run_pair "
|
|
277
|
+
if $NESTED; then
|
|
278
|
+
# Nested streams have maintained Go, TypeScript, and Python endpoints.
|
|
279
|
+
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT" --nested
|
|
280
|
+
run_pair "go-server + python-client" "$GO_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py" --nested
|
|
281
|
+
|
|
282
|
+
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT" --nested
|
|
283
|
+
run_pair "ts-server + python-client" node "$TS_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py" --nested
|
|
284
|
+
|
|
285
|
+
run_pair "python-server + go-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "$GO_CLIENT" --nested-release
|
|
286
|
+
run_pair "python-server + ts-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- node "$TS_CLIENT" --nested-release
|
|
287
|
+
else
|
|
288
|
+
# Go server combinations
|
|
289
|
+
run_pair "go-server + go-client" "$GO_SERVER" -- "$GO_CLIENT"
|
|
290
|
+
run_pair "go-server + rust-client" "$GO_SERVER" -- "$RUST_CLIENT"
|
|
291
|
+
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT"
|
|
292
|
+
run_pair "go-server + cpp-client" "$GO_SERVER" -- "$CPP_CLIENT"
|
|
293
|
+
run_pair "go-server + python-client" "$GO_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
294
|
+
|
|
295
|
+
# Rust server combinations
|
|
296
|
+
run_pair "rust-server + go-client" "$RUST_SERVER" -- "$GO_CLIENT"
|
|
297
|
+
run_pair "rust-server + rust-client" "$RUST_SERVER" -- "$RUST_CLIENT"
|
|
298
|
+
run_pair "rust-server + ts-client" "$RUST_SERVER" -- node "$TS_CLIENT"
|
|
299
|
+
run_pair "rust-server + cpp-client" "$RUST_SERVER" -- "$CPP_CLIENT"
|
|
300
|
+
|
|
301
|
+
# TypeScript server combinations
|
|
302
|
+
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT"
|
|
303
|
+
run_pair "ts-server + rust-client" node "$TS_SERVER" -- "$RUST_CLIENT"
|
|
304
|
+
run_pair "ts-server + ts-client" node "$TS_SERVER" -- node "$TS_CLIENT"
|
|
305
|
+
run_pair "ts-server + cpp-client" node "$TS_SERVER" -- "$CPP_CLIENT"
|
|
306
|
+
run_pair "ts-server + python-client" node "$TS_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
307
|
+
|
|
308
|
+
# Python server combinations
|
|
309
|
+
run_pair "python-server + go-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "$GO_CLIENT"
|
|
310
|
+
run_pair "python-server + ts-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- node "$TS_CLIENT" lifecycle
|
|
311
|
+
run_pair "python-server + python-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
312
|
+
|
|
313
|
+
# C++ server combinations
|
|
314
|
+
run_pair "cpp-server + go-client" "$CPP_SERVER" -- "$GO_CLIENT"
|
|
315
|
+
run_pair "cpp-server + rust-client" "$CPP_SERVER" -- "$RUST_CLIENT"
|
|
316
|
+
run_pair "cpp-server + ts-client" "$CPP_SERVER" -- node "$TS_CLIENT"
|
|
317
|
+
run_pair "cpp-server + cpp-client" "$CPP_SERVER" -- "$CPP_CLIENT"
|
|
318
|
+
fi
|
|
194
319
|
|
|
195
320
|
echo ""
|
|
196
321
|
echo "=== Results: ${PASSED} passed, ${FAILED} failed ==="
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
|
|
5
|
+
import { tcpSocketToPacketStream } from './tcp-packet-stream.js'
|
|
6
|
+
|
|
7
|
+
const sockets: net.Socket[] = []
|
|
8
|
+
const servers: net.Server[] = []
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const socket of sockets) socket.destroy()
|
|
12
|
+
for (const server of servers) server.close()
|
|
13
|
+
sockets.length = 0
|
|
14
|
+
servers.length = 0
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('TCP PacketStream lifecycle', () => {
|
|
18
|
+
it('settles a blocked source when the peer ends cleanly', async () => {
|
|
19
|
+
const { local, peer } = await connectSockets()
|
|
20
|
+
const stream = tcpSocketToPacketStream(local)
|
|
21
|
+
const pending = stream.source.next()
|
|
22
|
+
|
|
23
|
+
peer.end()
|
|
24
|
+
|
|
25
|
+
await expect(settleBeforeTimeout(pending)).resolves.toEqual({
|
|
26
|
+
done: true,
|
|
27
|
+
value: undefined,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('reports a truncated frame when the peer ends', async () => {
|
|
32
|
+
const { local, peer } = await connectSockets()
|
|
33
|
+
const stream = tcpSocketToPacketStream(local)
|
|
34
|
+
const pending = stream.source.next()
|
|
35
|
+
|
|
36
|
+
peer.end(Buffer.from([4, 0, 0, 0, 10, 1]))
|
|
37
|
+
|
|
38
|
+
await expect(settleBeforeTimeout(pending)).rejects.toThrow(
|
|
39
|
+
'truncated packet frame',
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects a blocked source with the abort error', async () => {
|
|
44
|
+
const { local } = await connectSockets()
|
|
45
|
+
const stream = tcpSocketToPacketStream(local)
|
|
46
|
+
const pending = stream.source.next()
|
|
47
|
+
const error = new Error('stopped')
|
|
48
|
+
|
|
49
|
+
stream.abort(error)
|
|
50
|
+
|
|
51
|
+
await expect(settleBeforeTimeout(pending)).rejects.toBe(error)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('settles a blocked sink when closed', async () => {
|
|
55
|
+
const { local } = await connectSockets()
|
|
56
|
+
const stream = tcpSocketToPacketStream(local)
|
|
57
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
58
|
+
const pending = stream.sink(input)
|
|
59
|
+
|
|
60
|
+
await stream.close()
|
|
61
|
+
|
|
62
|
+
await expect(pending).resolves.toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('settles an in-flight socket write when closed', async () => {
|
|
66
|
+
const { local } = await connectSockets()
|
|
67
|
+
const stream = tcpSocketToPacketStream(local)
|
|
68
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
69
|
+
const writeStarted = Promise.withResolvers<void>()
|
|
70
|
+
vi.spyOn(local, 'write').mockImplementation(((
|
|
71
|
+
_data: Uint8Array,
|
|
72
|
+
_callback: (error?: Error) => void,
|
|
73
|
+
) => {
|
|
74
|
+
writeStarted.resolve()
|
|
75
|
+
return true
|
|
76
|
+
}) as typeof local.write)
|
|
77
|
+
input.push(new Uint8Array([1]))
|
|
78
|
+
const pending = stream.sink(input)
|
|
79
|
+
await writeStarted.promise
|
|
80
|
+
|
|
81
|
+
await stream.close()
|
|
82
|
+
|
|
83
|
+
await expect(settleBeforeTimeout(pending)).resolves.toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('rejects a blocked sink with the abort error', async () => {
|
|
87
|
+
const { local } = await connectSockets()
|
|
88
|
+
const stream = tcpSocketToPacketStream(local)
|
|
89
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
90
|
+
const pending = stream.sink(input)
|
|
91
|
+
const error = new Error('stopped')
|
|
92
|
+
|
|
93
|
+
stream.abort(error)
|
|
94
|
+
|
|
95
|
+
await expect(pending).rejects.toBe(error)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not write ready input after close', async () => {
|
|
99
|
+
const { local, peer } = await connectSockets()
|
|
100
|
+
const stream = tcpSocketToPacketStream(local)
|
|
101
|
+
const input = pushable<Uint8Array>({ objectMode: true })
|
|
102
|
+
const received: Buffer[] = []
|
|
103
|
+
peer.on('data', (data) => {
|
|
104
|
+
if (typeof data !== 'string') received.push(data)
|
|
105
|
+
})
|
|
106
|
+
input.push(new Uint8Array([1]))
|
|
107
|
+
|
|
108
|
+
const pending = stream.sink(input)
|
|
109
|
+
await stream.close()
|
|
110
|
+
await pending
|
|
111
|
+
await new Promise<void>((resolve) => peer.once('close', () => resolve()))
|
|
112
|
+
|
|
113
|
+
expect(received).toEqual([])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
async function connectSockets(): Promise<{
|
|
118
|
+
local: net.Socket
|
|
119
|
+
peer: net.Socket
|
|
120
|
+
}> {
|
|
121
|
+
const accepted = Promise.withResolvers<net.Socket>()
|
|
122
|
+
const server = net.createServer((socket) => accepted.resolve(socket))
|
|
123
|
+
servers.push(server)
|
|
124
|
+
await new Promise<void>((resolve, reject) => {
|
|
125
|
+
server.once('error', reject)
|
|
126
|
+
server.listen(0, '127.0.0.1', resolve)
|
|
127
|
+
})
|
|
128
|
+
const address = server.address() as net.AddressInfo
|
|
129
|
+
const local = net.connect(address.port, address.address)
|
|
130
|
+
await new Promise<void>((resolve, reject) => {
|
|
131
|
+
local.once('connect', resolve)
|
|
132
|
+
local.once('error', reject)
|
|
133
|
+
})
|
|
134
|
+
const peer = await accepted.promise
|
|
135
|
+
sockets.push(local, peer)
|
|
136
|
+
return { local, peer }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function settleBeforeTimeout<T>(promise: Promise<T>): Promise<T> {
|
|
140
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
141
|
+
try {
|
|
142
|
+
return await Promise.race([
|
|
143
|
+
promise,
|
|
144
|
+
new Promise<never>((_resolve, reject) => {
|
|
145
|
+
timeout = setTimeout(
|
|
146
|
+
() => reject(new Error('source stayed blocked')),
|
|
147
|
+
500,
|
|
148
|
+
)
|
|
149
|
+
}),
|
|
150
|
+
])
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(timeout)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import { pipe } from 'it-pipe'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
import type { Source } from 'it-stream-types'
|
|
5
|
+
|
|
6
|
+
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
|
|
7
|
+
import {
|
|
8
|
+
parseLengthPrefixTransform,
|
|
9
|
+
prependLengthPrefixTransform,
|
|
10
|
+
} from '../../srpc/packet.js'
|
|
11
|
+
import type { PacketStream } from '../../srpc/stream.js'
|
|
12
|
+
import {
|
|
13
|
+
closeIterator,
|
|
14
|
+
sourceIterator,
|
|
15
|
+
TerminationGate,
|
|
16
|
+
} from '../../srpc/termination.js'
|
|
17
|
+
|
|
18
|
+
// tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
|
|
19
|
+
export function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
20
|
+
const sourceTermination = new TerminationGate()
|
|
21
|
+
const sinkTermination = new TerminationGate()
|
|
22
|
+
const bytes = pushable<Uint8Array>({ objectMode: true })
|
|
23
|
+
let bytesEnded = false
|
|
24
|
+
|
|
25
|
+
const endBytes = (error?: Error) => {
|
|
26
|
+
if (bytesEnded) return
|
|
27
|
+
bytesEnded = true
|
|
28
|
+
bytes.end(error)
|
|
29
|
+
}
|
|
30
|
+
const terminate = (error?: Error): boolean => {
|
|
31
|
+
const first = sourceTermination.terminate(error)
|
|
32
|
+
sinkTermination.terminate(error)
|
|
33
|
+
endBytes(error)
|
|
34
|
+
return first
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
socket.on('data', (data: Buffer) => {
|
|
38
|
+
if (!bytesEnded) bytes.push(new Uint8Array(data))
|
|
39
|
+
})
|
|
40
|
+
socket.on('end', () => endBytes())
|
|
41
|
+
socket.on('error', (error) => terminate(error))
|
|
42
|
+
socket.on('close', () => {
|
|
43
|
+
endBytes()
|
|
44
|
+
sinkTermination.terminate()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const close = async (): Promise<void> => {
|
|
48
|
+
if (terminate()) socket.destroy()
|
|
49
|
+
}
|
|
50
|
+
const abort = (error: Error): void => {
|
|
51
|
+
if (terminate(error)) socket.destroy(error)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
close,
|
|
56
|
+
abort,
|
|
57
|
+
source: (async function* () {
|
|
58
|
+
const packets = pipe(
|
|
59
|
+
bytes,
|
|
60
|
+
parseLengthPrefixTransform(),
|
|
61
|
+
combineUint8ArrayListTransform(),
|
|
62
|
+
)[Symbol.asyncIterator]()
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const next = await sourceTermination.next(packets)
|
|
66
|
+
if ('terminated' in next) {
|
|
67
|
+
if (next.error) throw next.error
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if ('error' in next) throw next.error
|
|
71
|
+
if (next.result.done) return
|
|
72
|
+
yield next.result.value
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
closeIterator(packets)
|
|
76
|
+
}
|
|
77
|
+
})(),
|
|
78
|
+
sink: async (source: Source<Uint8Array>): Promise<void> => {
|
|
79
|
+
const iterator = sourceIterator(
|
|
80
|
+
pipe(source, prependLengthPrefixTransform()),
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
while (true) {
|
|
84
|
+
const next = await sinkTermination.next(iterator)
|
|
85
|
+
if ('terminated' in next) {
|
|
86
|
+
if (next.error) throw next.error
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if ('error' in next) throw next.error
|
|
90
|
+
if (next.result.done) {
|
|
91
|
+
socket.end()
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const data =
|
|
95
|
+
next.result.value instanceof Uint8Array
|
|
96
|
+
? next.result.value
|
|
97
|
+
: next.result.value.subarray()
|
|
98
|
+
const written = await sinkTermination.wait(
|
|
99
|
+
new Promise<void>((resolve, reject) => {
|
|
100
|
+
socket.write(data, (error) => {
|
|
101
|
+
if (error) reject(error)
|
|
102
|
+
else resolve()
|
|
103
|
+
})
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
if ('terminated' in written) {
|
|
107
|
+
if (written.error) throw written.error
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if ('error' in written) throw written.error
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
114
|
+
abort(error)
|
|
115
|
+
throw error
|
|
116
|
+
} finally {
|
|
117
|
+
closeIterator(iterator)
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|