starpc 0.46.0 → 0.46.2
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/dist/integration/cross-language/ts-client.d.ts +1 -0
- package/dist/integration/cross-language/ts-client.js +67 -0
- package/dist/integration/cross-language/ts-server.d.ts +1 -0
- package/dist/integration/cross-language/ts-server.js +60 -0
- package/echo/Cargo.toml +8 -0
- package/echo/echo_e2e_test.cpp +2 -2
- package/echo/integration_client.rs +168 -0
- package/echo/integration_server.rs +87 -0
- package/integration/cross-language/cpp-client.cpp +448 -0
- package/integration/cross-language/cpp-server.cpp +221 -0
- package/integration/cross-language/go-client/main.go +151 -0
- package/integration/cross-language/go-server/main.go +47 -0
- package/integration/cross-language/run.bash +193 -0
- package/integration/cross-language/ts-client.ts +84 -0
- package/integration/cross-language/ts-server.ts +75 -0
- package/integration/integration.bash +2 -0
- package/package.json +8 -3
- package/srpc/client-invoker.go +1 -1
- package/srpc/lib.rs +1 -0
- package/srpc/rpcstream/mod.rs +0 -38
- package/srpc/rpcstream/proto.rs +0 -286
- package/srpc/rpcstream/stream.rs +0 -517
- package/srpc/rpcstream/writer.rs +0 -150
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"net"
|
|
8
|
+
"os"
|
|
9
|
+
|
|
10
|
+
"github.com/aperturerobotics/starpc/echo"
|
|
11
|
+
"github.com/aperturerobotics/starpc/srpc"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const bodyTxt = "hello world via starpc cross-language e2e test"
|
|
15
|
+
|
|
16
|
+
func main() {
|
|
17
|
+
if len(os.Args) < 2 {
|
|
18
|
+
fmt.Fprintf(os.Stderr, "usage: go-client <addr>\n")
|
|
19
|
+
os.Exit(1)
|
|
20
|
+
}
|
|
21
|
+
addr := os.Args[1]
|
|
22
|
+
|
|
23
|
+
openStream := func(ctx context.Context, msgHandler srpc.PacketDataHandler, closeHandler srpc.CloseHandler) (srpc.PacketWriter, error) {
|
|
24
|
+
conn, err := net.Dial("tcp", addr)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return nil, err
|
|
27
|
+
}
|
|
28
|
+
prw := srpc.NewPacketReadWriter(conn)
|
|
29
|
+
go prw.ReadPump(msgHandler, closeHandler)
|
|
30
|
+
return prw, nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
client := srpc.NewClient(openStream)
|
|
34
|
+
echoClient := echo.NewSRPCEchoerClient(client)
|
|
35
|
+
ctx := context.Background()
|
|
36
|
+
|
|
37
|
+
if err := testUnary(ctx, echoClient); err != nil {
|
|
38
|
+
fmt.Fprintf(os.Stderr, "unary test failed: %v\n", err)
|
|
39
|
+
os.Exit(1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if err := testServerStream(ctx, echoClient); err != nil {
|
|
43
|
+
fmt.Fprintf(os.Stderr, "server stream test failed: %v\n", err)
|
|
44
|
+
os.Exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if err := testClientStream(ctx, echoClient); err != nil {
|
|
48
|
+
fmt.Fprintf(os.Stderr, "client stream test failed: %v\n", err)
|
|
49
|
+
os.Exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if err := testBidiStream(ctx, echoClient); err != nil {
|
|
53
|
+
fmt.Fprintf(os.Stderr, "bidi stream test failed: %v\n", err)
|
|
54
|
+
os.Exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fmt.Println("All tests passed.")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func testUnary(ctx context.Context, client echo.SRPCEchoerClient) error {
|
|
61
|
+
fmt.Println("Testing Unary RPC...")
|
|
62
|
+
out, err := client.Echo(ctx, &echo.EchoMsg{Body: bodyTxt})
|
|
63
|
+
if err != nil {
|
|
64
|
+
return fmt.Errorf("echo call: %w", err)
|
|
65
|
+
}
|
|
66
|
+
if out.GetBody() != bodyTxt {
|
|
67
|
+
return fmt.Errorf("expected %q got %q", bodyTxt, out.GetBody())
|
|
68
|
+
}
|
|
69
|
+
fmt.Println(" PASSED")
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func testServerStream(ctx context.Context, client echo.SRPCEchoerClient) error {
|
|
74
|
+
fmt.Println("Testing ServerStream RPC...")
|
|
75
|
+
strm, err := client.EchoServerStream(ctx, &echo.EchoMsg{Body: bodyTxt})
|
|
76
|
+
if err != nil {
|
|
77
|
+
return fmt.Errorf("echo server stream call: %w", err)
|
|
78
|
+
}
|
|
79
|
+
received := 0
|
|
80
|
+
for {
|
|
81
|
+
msg, err := strm.Recv()
|
|
82
|
+
if err != nil {
|
|
83
|
+
if err == io.EOF {
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
return fmt.Errorf("recv: %w", err)
|
|
87
|
+
}
|
|
88
|
+
if msg.GetBody() != bodyTxt {
|
|
89
|
+
return fmt.Errorf("expected %q got %q", bodyTxt, msg.GetBody())
|
|
90
|
+
}
|
|
91
|
+
received++
|
|
92
|
+
}
|
|
93
|
+
if received != 5 {
|
|
94
|
+
return fmt.Errorf("expected 5 messages, got %d", received)
|
|
95
|
+
}
|
|
96
|
+
fmt.Println(" PASSED")
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func testClientStream(ctx context.Context, client echo.SRPCEchoerClient) error {
|
|
101
|
+
fmt.Println("Testing ClientStream RPC...")
|
|
102
|
+
strm, err := client.EchoClientStream(ctx)
|
|
103
|
+
if err != nil {
|
|
104
|
+
return fmt.Errorf("echo client stream call: %w", err)
|
|
105
|
+
}
|
|
106
|
+
if err := strm.MsgSend(&echo.EchoMsg{Body: bodyTxt}); err != nil {
|
|
107
|
+
return fmt.Errorf("send: %w", err)
|
|
108
|
+
}
|
|
109
|
+
resp := &echo.EchoMsg{}
|
|
110
|
+
if err := strm.MsgRecv(resp); err != nil {
|
|
111
|
+
return fmt.Errorf("recv: %w", err)
|
|
112
|
+
}
|
|
113
|
+
if resp.GetBody() != bodyTxt {
|
|
114
|
+
return fmt.Errorf("expected %q got %q", bodyTxt, resp.GetBody())
|
|
115
|
+
}
|
|
116
|
+
_ = strm.Close()
|
|
117
|
+
fmt.Println(" PASSED")
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func testBidiStream(ctx context.Context, client echo.SRPCEchoerClient) error {
|
|
122
|
+
fmt.Println("Testing BidiStream RPC...")
|
|
123
|
+
strm, err := client.EchoBidiStream(ctx)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return fmt.Errorf("echo bidi stream call: %w", err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// server sends initial message
|
|
129
|
+
msg, err := strm.Recv()
|
|
130
|
+
if err != nil {
|
|
131
|
+
return fmt.Errorf("recv initial: %w", err)
|
|
132
|
+
}
|
|
133
|
+
if msg.GetBody() != "hello from server" {
|
|
134
|
+
return fmt.Errorf("expected %q got %q", "hello from server", msg.GetBody())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// send a message and expect echo
|
|
138
|
+
if err := strm.MsgSend(&echo.EchoMsg{Body: bodyTxt}); err != nil {
|
|
139
|
+
return fmt.Errorf("send: %w", err)
|
|
140
|
+
}
|
|
141
|
+
msg, err = strm.Recv()
|
|
142
|
+
if err != nil {
|
|
143
|
+
return fmt.Errorf("recv echo: %w", err)
|
|
144
|
+
}
|
|
145
|
+
if msg.GetBody() != bodyTxt {
|
|
146
|
+
return fmt.Errorf("expected %q got %q", bodyTxt, msg.GetBody())
|
|
147
|
+
}
|
|
148
|
+
_ = strm.Close()
|
|
149
|
+
fmt.Println(" PASSED")
|
|
150
|
+
return nil
|
|
151
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net"
|
|
7
|
+
"os"
|
|
8
|
+
"os/signal"
|
|
9
|
+
|
|
10
|
+
"github.com/aperturerobotics/starpc/echo"
|
|
11
|
+
"github.com/aperturerobotics/starpc/srpc"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func main() {
|
|
15
|
+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
16
|
+
defer cancel()
|
|
17
|
+
|
|
18
|
+
mux := srpc.NewMux()
|
|
19
|
+
echoServer := echo.NewEchoServer(mux)
|
|
20
|
+
if err := echo.SRPCRegisterEchoer(mux, echoServer); err != nil {
|
|
21
|
+
fmt.Fprintf(os.Stderr, "register error: %v\n", err)
|
|
22
|
+
os.Exit(1)
|
|
23
|
+
}
|
|
24
|
+
server := srpc.NewServer(mux)
|
|
25
|
+
|
|
26
|
+
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
27
|
+
if err != nil {
|
|
28
|
+
fmt.Fprintf(os.Stderr, "listen error: %v\n", err)
|
|
29
|
+
os.Exit(1)
|
|
30
|
+
}
|
|
31
|
+
defer ln.Close()
|
|
32
|
+
|
|
33
|
+
fmt.Printf("LISTENING %s\n", ln.Addr().String())
|
|
34
|
+
|
|
35
|
+
go func() {
|
|
36
|
+
<-ctx.Done()
|
|
37
|
+
ln.Close()
|
|
38
|
+
}()
|
|
39
|
+
|
|
40
|
+
for {
|
|
41
|
+
conn, err := ln.Accept()
|
|
42
|
+
if err != nil {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
go server.HandleStream(ctx, conn)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Cross-language integration tests for starpc.
|
|
3
|
+
# Runs all 12 server/client combinations across Go, TypeScript, Rust, and C++.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ./run.bash # Run all pairs
|
|
7
|
+
# ./run.bash go:ts # Run go-server+ts-client and ts-server+go-client
|
|
8
|
+
# ./run.bash go:ts go:rust # Run multiple pair filters
|
|
9
|
+
set -eo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
13
|
+
|
|
14
|
+
# Fixes errors with the generated esm using require()
|
|
15
|
+
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
|
+
|
|
17
|
+
FILTERS=("$@")
|
|
18
|
+
|
|
19
|
+
PASSED=0
|
|
20
|
+
FAILED=0
|
|
21
|
+
ERRORS=""
|
|
22
|
+
|
|
23
|
+
# should_run checks if a test name matches the active filters.
|
|
24
|
+
# Returns 0 (true) if the test should run, 1 (false) otherwise.
|
|
25
|
+
should_run() {
|
|
26
|
+
local test_name="$1"
|
|
27
|
+
if [ ${#FILTERS[@]} -eq 0 ]; then
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
for filter in "${FILTERS[@]}"; do
|
|
31
|
+
local lang1="${filter%%:*}"
|
|
32
|
+
local lang2="${filter##*:}"
|
|
33
|
+
if [[ "$test_name" == *"${lang1}-"* && "$test_name" == *"${lang2}-"* ]]; then
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Build all binaries.
|
|
41
|
+
echo "=== Building all integration binaries ==="
|
|
42
|
+
|
|
43
|
+
echo "Building Go server/client..."
|
|
44
|
+
go build -o "$SCRIPT_DIR/go-server/go-server" "$SCRIPT_DIR/go-server/"
|
|
45
|
+
go build -o "$SCRIPT_DIR/go-client/go-client" "$SCRIPT_DIR/go-client/"
|
|
46
|
+
|
|
47
|
+
echo "Building TypeScript server/client..."
|
|
48
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-server.ts" \
|
|
49
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
50
|
+
--banner:js="$ESM_BANNER" \
|
|
51
|
+
--outfile="$SCRIPT_DIR/ts-server.mjs"
|
|
52
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-client.ts" \
|
|
53
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
54
|
+
--banner:js="$ESM_BANNER" \
|
|
55
|
+
--outfile="$SCRIPT_DIR/ts-client.mjs"
|
|
56
|
+
|
|
57
|
+
echo "Building Rust server/client..."
|
|
58
|
+
cargo build --release --bin integration-server --bin integration-client 2>&1 | grep -v "^warning:" || true
|
|
59
|
+
|
|
60
|
+
echo "Building C++ server/client..."
|
|
61
|
+
mkdir -p "$REPO_DIR/build"
|
|
62
|
+
pushd "$REPO_DIR/build" > /dev/null
|
|
63
|
+
cmake "$REPO_DIR" -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
|
|
64
|
+
cmake --build . --target cpp-integration-server cpp-integration-client --parallel > /dev/null 2>&1
|
|
65
|
+
popd > /dev/null
|
|
66
|
+
|
|
67
|
+
# Binary paths.
|
|
68
|
+
GO_SERVER="$SCRIPT_DIR/go-server/go-server"
|
|
69
|
+
GO_CLIENT="$SCRIPT_DIR/go-client/go-client"
|
|
70
|
+
TS_SERVER="$SCRIPT_DIR/ts-server.mjs"
|
|
71
|
+
TS_CLIENT="$SCRIPT_DIR/ts-client.mjs"
|
|
72
|
+
RUST_SERVER="$REPO_DIR/target/release/integration-server"
|
|
73
|
+
RUST_CLIENT="$REPO_DIR/target/release/integration-client"
|
|
74
|
+
CPP_SERVER="$REPO_DIR/build/cpp-integration-server"
|
|
75
|
+
CPP_CLIENT="$REPO_DIR/build/cpp-integration-client"
|
|
76
|
+
|
|
77
|
+
# Start a server and capture its address.
|
|
78
|
+
# Sets SERVER_PID and SERVER_ADDR.
|
|
79
|
+
start_server() {
|
|
80
|
+
local addr_file
|
|
81
|
+
addr_file=$(mktemp)
|
|
82
|
+
"$@" > "$addr_file" 2>&1 &
|
|
83
|
+
SERVER_PID=$!
|
|
84
|
+
# Wait for LISTENING output (up to 3 seconds).
|
|
85
|
+
local waited=0
|
|
86
|
+
while [ $waited -lt 30 ]; do
|
|
87
|
+
if grep -q 'LISTENING' "$addr_file" 2>/dev/null; then
|
|
88
|
+
break
|
|
89
|
+
fi
|
|
90
|
+
sleep 0.1
|
|
91
|
+
waited=$((waited + 1))
|
|
92
|
+
done
|
|
93
|
+
SERVER_ADDR=$(grep 'LISTENING' "$addr_file" 2>/dev/null | awk '{print $2}')
|
|
94
|
+
rm -f "$addr_file"
|
|
95
|
+
if [ -z "$SERVER_ADDR" ]; then
|
|
96
|
+
echo "FAILED: server did not start"
|
|
97
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
98
|
+
return 1
|
|
99
|
+
fi
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
stop_server() {
|
|
104
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
105
|
+
wait $SERVER_PID 2>/dev/null || true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# run_pair <test_name> <server_args...> -- <client_args...>
|
|
109
|
+
# The client receives $SERVER_ADDR as its last argument.
|
|
110
|
+
run_pair() {
|
|
111
|
+
local test_name="$1"
|
|
112
|
+
shift
|
|
113
|
+
|
|
114
|
+
if ! should_run "$test_name"; then
|
|
115
|
+
return
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Split args on "--".
|
|
119
|
+
local srv_args=()
|
|
120
|
+
local cli_args=()
|
|
121
|
+
local in_client=false
|
|
122
|
+
for arg in "$@"; do
|
|
123
|
+
if [ "$arg" = "--" ]; then
|
|
124
|
+
in_client=true
|
|
125
|
+
continue
|
|
126
|
+
fi
|
|
127
|
+
if $in_client; then
|
|
128
|
+
cli_args+=("$arg")
|
|
129
|
+
else
|
|
130
|
+
srv_args+=("$arg")
|
|
131
|
+
fi
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
echo -n " ${test_name}... "
|
|
135
|
+
|
|
136
|
+
if ! start_server "${srv_args[@]}"; then
|
|
137
|
+
echo "FAILED (server start)"
|
|
138
|
+
FAILED=$((FAILED + 1))
|
|
139
|
+
ERRORS="${ERRORS}\n ${test_name} (server start failed)"
|
|
140
|
+
return
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local client_out
|
|
144
|
+
client_out=$(mktemp)
|
|
145
|
+
if "${cli_args[@]}" "$SERVER_ADDR" > "$client_out" 2>&1; then
|
|
146
|
+
echo "PASSED"
|
|
147
|
+
PASSED=$((PASSED + 1))
|
|
148
|
+
else
|
|
149
|
+
echo "FAILED"
|
|
150
|
+
FAILED=$((FAILED + 1))
|
|
151
|
+
ERRORS="${ERRORS}\n ${test_name}"
|
|
152
|
+
echo " client output:"
|
|
153
|
+
sed 's/^/ /' "$client_out"
|
|
154
|
+
fi
|
|
155
|
+
rm -f "$client_out"
|
|
156
|
+
|
|
157
|
+
stop_server
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
echo ""
|
|
161
|
+
echo "=== Running cross-language integration tests ==="
|
|
162
|
+
echo ""
|
|
163
|
+
|
|
164
|
+
# Go server combinations
|
|
165
|
+
run_pair "go-server + go-client" "$GO_SERVER" -- "$GO_CLIENT"
|
|
166
|
+
run_pair "go-server + rust-client" "$GO_SERVER" -- "$RUST_CLIENT"
|
|
167
|
+
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT"
|
|
168
|
+
run_pair "go-server + cpp-client" "$GO_SERVER" -- "$CPP_CLIENT"
|
|
169
|
+
|
|
170
|
+
# Rust server combinations
|
|
171
|
+
run_pair "rust-server + go-client" "$RUST_SERVER" -- "$GO_CLIENT"
|
|
172
|
+
run_pair "rust-server + rust-client" "$RUST_SERVER" -- "$RUST_CLIENT"
|
|
173
|
+
run_pair "rust-server + ts-client" "$RUST_SERVER" -- node "$TS_CLIENT"
|
|
174
|
+
run_pair "rust-server + cpp-client" "$RUST_SERVER" -- "$CPP_CLIENT"
|
|
175
|
+
|
|
176
|
+
# TypeScript server combinations
|
|
177
|
+
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT"
|
|
178
|
+
run_pair "ts-server + rust-client" node "$TS_SERVER" -- "$RUST_CLIENT"
|
|
179
|
+
run_pair "ts-server + ts-client" node "$TS_SERVER" -- node "$TS_CLIENT"
|
|
180
|
+
run_pair "ts-server + cpp-client" node "$TS_SERVER" -- "$CPP_CLIENT"
|
|
181
|
+
|
|
182
|
+
# C++ server combinations
|
|
183
|
+
run_pair "cpp-server + go-client" "$CPP_SERVER" -- "$GO_CLIENT"
|
|
184
|
+
run_pair "cpp-server + rust-client" "$CPP_SERVER" -- "$RUST_CLIENT"
|
|
185
|
+
run_pair "cpp-server + ts-client" "$CPP_SERVER" -- node "$TS_CLIENT"
|
|
186
|
+
run_pair "cpp-server + cpp-client" "$CPP_SERVER" -- "$CPP_CLIENT"
|
|
187
|
+
|
|
188
|
+
echo ""
|
|
189
|
+
echo "=== Results: ${PASSED} passed, ${FAILED} failed ==="
|
|
190
|
+
if [ $FAILED -gt 0 ]; then
|
|
191
|
+
echo -e "Failed tests:${ERRORS}"
|
|
192
|
+
exit 1
|
|
193
|
+
fi
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import { pipe } from 'it-pipe'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
import { Client } from '../../srpc/client.js'
|
|
5
|
+
import {
|
|
6
|
+
parseLengthPrefixTransform,
|
|
7
|
+
prependLengthPrefixTransform,
|
|
8
|
+
} from '../../srpc/packet.js'
|
|
9
|
+
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
|
|
10
|
+
import { runClientTest } from '../../echo/client-test.js'
|
|
11
|
+
import type { OpenStreamFunc, PacketStream } from '../../srpc/stream.js'
|
|
12
|
+
import type { Source } from 'it-stream-types'
|
|
13
|
+
|
|
14
|
+
// tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
|
|
15
|
+
function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
16
|
+
const socketSource = async function* (): AsyncGenerator<Uint8Array> {
|
|
17
|
+
const source = pushable<Uint8Array>({ objectMode: true })
|
|
18
|
+
socket.on('data', (data: Buffer) => {
|
|
19
|
+
source.push(new Uint8Array(data))
|
|
20
|
+
})
|
|
21
|
+
socket.on('end', () => source.end())
|
|
22
|
+
socket.on('error', (err) => source.end(err))
|
|
23
|
+
socket.on('close', () => source.end())
|
|
24
|
+
yield* pipe(
|
|
25
|
+
source,
|
|
26
|
+
parseLengthPrefixTransform(),
|
|
27
|
+
combineUint8ArrayListTransform(),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
source: socketSource(),
|
|
33
|
+
sink: async (source: Source<Uint8Array>): Promise<void> => {
|
|
34
|
+
for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
|
|
35
|
+
const data =
|
|
36
|
+
chunk instanceof Uint8Array ? chunk : (chunk as any).subarray()
|
|
37
|
+
await new Promise<void>((resolve, reject) => {
|
|
38
|
+
socket.write(data, (err) => {
|
|
39
|
+
if (err) reject(err)
|
|
40
|
+
else resolve()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
socket.end()
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const addr = process.argv[2]
|
|
51
|
+
if (!addr) {
|
|
52
|
+
console.error('usage: ts-client <host:port>')
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [host, portStr] = addr.split(':')
|
|
57
|
+
const port = parseInt(portStr, 10)
|
|
58
|
+
|
|
59
|
+
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)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const client = new Client(openStream)
|
|
69
|
+
|
|
70
|
+
console.log('Running client test via TCP...')
|
|
71
|
+
await runClientTest(client)
|
|
72
|
+
console.log('All tests passed.')
|
|
73
|
+
process.exit(0)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process.on('unhandledRejection', (ev) => {
|
|
77
|
+
console.error('Unhandled rejection', ev)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
main().catch((err) => {
|
|
82
|
+
console.error('Error:', err)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import net from 'net'
|
|
2
|
+
import { pipe } from 'it-pipe'
|
|
3
|
+
import { pushable } from 'it-pushable'
|
|
4
|
+
import { createMux, createHandler, Server } from '../../srpc/index.js'
|
|
5
|
+
import {
|
|
6
|
+
parseLengthPrefixTransform,
|
|
7
|
+
prependLengthPrefixTransform,
|
|
8
|
+
} from '../../srpc/packet.js'
|
|
9
|
+
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
|
|
10
|
+
import { EchoerServer } from '../../echo/server.js'
|
|
11
|
+
import { EchoerDefinition } from '../../echo/echo_srpc.pb.js'
|
|
12
|
+
import type { PacketStream } from '../../srpc/stream.js'
|
|
13
|
+
import type { Source } from 'it-stream-types'
|
|
14
|
+
|
|
15
|
+
// tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
|
|
16
|
+
// Each Uint8Array in source/sink is one packet (no length prefix).
|
|
17
|
+
function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
|
|
18
|
+
// Source: read from socket, strip length prefix, yield individual packets.
|
|
19
|
+
const socketSource = async function* (): AsyncGenerator<Uint8Array> {
|
|
20
|
+
const source = pushable<Uint8Array>({ objectMode: true })
|
|
21
|
+
socket.on('data', (data: Buffer) => {
|
|
22
|
+
source.push(new Uint8Array(data))
|
|
23
|
+
})
|
|
24
|
+
socket.on('end', () => source.end())
|
|
25
|
+
socket.on('error', (err) => source.end(err))
|
|
26
|
+
socket.on('close', () => source.end())
|
|
27
|
+
yield* pipe(
|
|
28
|
+
source,
|
|
29
|
+
parseLengthPrefixTransform(),
|
|
30
|
+
combineUint8ArrayListTransform(),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
source: socketSource(),
|
|
36
|
+
sink: async (source: Source<Uint8Array>): Promise<void> => {
|
|
37
|
+
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) => {
|
|
41
|
+
socket.write(data, (err) => {
|
|
42
|
+
if (err) reject(err)
|
|
43
|
+
else resolve()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
socket.end()
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const mux = createMux()
|
|
53
|
+
const server = new Server(mux.lookupMethod)
|
|
54
|
+
const echoer = new EchoerServer(server)
|
|
55
|
+
mux.register(createHandler(EchoerDefinition, echoer))
|
|
56
|
+
|
|
57
|
+
const tcpServer = net.createServer((socket) => {
|
|
58
|
+
const stream = tcpSocketToPacketStream(socket)
|
|
59
|
+
server.handlePacketStream(stream)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
tcpServer.listen(0, '127.0.0.1', () => {
|
|
63
|
+
const addr = tcpServer.address() as net.AddressInfo
|
|
64
|
+
console.log(`LISTENING ${addr.address}:${addr.port}`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
process.on('SIGINT', () => {
|
|
68
|
+
tcpServer.close()
|
|
69
|
+
process.exit(0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
process.on('SIGTERM', () => {
|
|
73
|
+
tcpServer.close()
|
|
74
|
+
process.exit(0)
|
|
75
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starpc",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2",
|
|
4
4
|
"description": "Streaming protobuf RPC service protocol over any two-way channel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -81,8 +81,13 @@
|
|
|
81
81
|
"test:js": "vitest run",
|
|
82
82
|
"test:js:watch": "vitest",
|
|
83
83
|
"debug:js": "bun run build:e2e && cd e2e && node --inspect --inspect-brk ./e2e.cjs",
|
|
84
|
-
"
|
|
85
|
-
"integration": "
|
|
84
|
+
"integration": "bash ./integration/cross-language/run.bash",
|
|
85
|
+
"integration:go:ts": "bash ./integration/cross-language/run.bash go:ts",
|
|
86
|
+
"integration:go:rust": "bash ./integration/cross-language/run.bash go:rust",
|
|
87
|
+
"integration:go:cpp": "bash ./integration/cross-language/run.bash go:cpp",
|
|
88
|
+
"integration:ts:rust": "bash ./integration/cross-language/run.bash ts:rust",
|
|
89
|
+
"integration:ts:cpp": "bash ./integration/cross-language/run.bash ts:cpp",
|
|
90
|
+
"integration:rust:cpp": "bash ./integration/cross-language/run.bash rust:cpp",
|
|
86
91
|
"lint": "bun run lint:go && bun run lint:js",
|
|
87
92
|
"lint:go": "bun run go:aptre -- lint",
|
|
88
93
|
"lint:js": "ESLINT_USE_FLAT_CONFIG=false eslint -c .eslintrc.cjs --ignore-pattern *.js --ignore-pattern *.d.ts ./",
|
package/srpc/client-invoker.go
CHANGED
package/srpc/lib.rs
CHANGED
package/srpc/rpcstream/mod.rs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
//! RpcStream module for nested RPC calls.
|
|
2
|
-
//!
|
|
3
|
-
//! This module enables nesting RPC calls within RPC calls, supporting
|
|
4
|
-
//! component-based architectures where different components expose different
|
|
5
|
-
//! services via sub-streams.
|
|
6
|
-
//!
|
|
7
|
-
//! # Overview
|
|
8
|
-
//!
|
|
9
|
-
//! The rpcstream protocol works as follows:
|
|
10
|
-
//! 1. Client opens a bidirectional stream to the server
|
|
11
|
-
//! 2. Client sends `RpcStreamInit` with the target component ID
|
|
12
|
-
//! 3. Server looks up the component and sends `RpcAck`
|
|
13
|
-
//! 4. Both sides exchange `RpcStreamPacket::Data` containing nested RPC packets
|
|
14
|
-
//!
|
|
15
|
-
//! # Example
|
|
16
|
-
//!
|
|
17
|
-
//! ```rust,ignore
|
|
18
|
-
//! use starpc::rpcstream::{open_rpc_stream, RpcStreamGetter};
|
|
19
|
-
//!
|
|
20
|
-
//! // Client side: open a stream to a component
|
|
21
|
-
//! let stream = my_service.rpc_stream().await?;
|
|
22
|
-
//! let rpc_stream = open_rpc_stream(stream, "my-component", true).await?;
|
|
23
|
-
//!
|
|
24
|
-
//! // Server side: handle incoming rpc stream
|
|
25
|
-
//! let getter: RpcStreamGetter = Arc::new(|ctx, component_id, released| {
|
|
26
|
-
//! // Look up the invoker for this component
|
|
27
|
-
//! Some((invoker, release_fn))
|
|
28
|
-
//! });
|
|
29
|
-
//! handle_rpc_stream(stream, getter).await?;
|
|
30
|
-
//! ```
|
|
31
|
-
|
|
32
|
-
mod proto;
|
|
33
|
-
mod stream;
|
|
34
|
-
mod writer;
|
|
35
|
-
|
|
36
|
-
pub use proto::*;
|
|
37
|
-
pub use stream::*;
|
|
38
|
-
pub use writer::*;
|