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.
@@ -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
+ })
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ # Go<->TypeScript integration test: Go WebSocket server + TypeScript client.
3
+ # For cross-language tests across all 4 languages, see integration/cross-language/.
2
4
  set -eo pipefail
3
5
 
4
6
  unset GOOS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.46.0",
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
- "test:integration": "cd ./integration && bash ./integration.bash",
85
- "integration": "bun run test: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 ./",
@@ -69,7 +69,7 @@ func proxyStreamTo(src, dst Stream, errCh chan error) {
69
69
  if rerr == nil && srcErr != nil {
70
70
  rerr = srcErr
71
71
  }
72
- if rerr != nil {
72
+ if rerr != nil && rerr != io.EOF {
73
73
  if errCh != nil {
74
74
  errCh <- rerr
75
75
  }
package/srpc/lib.rs CHANGED
@@ -66,6 +66,7 @@ pub mod mux;
66
66
  pub mod packet;
67
67
  pub mod proto;
68
68
  pub mod rpc;
69
+ #[path = "../rpcstream/mod.rs"]
69
70
  pub mod rpcstream;
70
71
  pub mod server;
71
72
  pub mod stream;
@@ -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::*;