starpc 0.52.1 → 0.52.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/README.md +37 -8
- package/dist/srpc/client-rpc.d.ts +1 -0
- package/dist/srpc/client-rpc.js +9 -0
- package/dist/srpc/client-rpc.test.d.ts +1 -0
- package/dist/srpc/client-rpc.test.js +50 -0
- package/dist/srpc/common-rpc.d.ts +5 -0
- package/dist/srpc/common-rpc.js +37 -8
- package/dist/srpc/errors.d.ts +2 -0
- package/dist/srpc/errors.js +14 -0
- package/dist/srpc/index.d.ts +1 -1
- package/dist/srpc/index.js +1 -1
- package/echo/Cargo.toml +8 -0
- package/echo/echo_e2e_test.cpp +542 -985
- package/echo/echo_srpc.pb.cpp +3 -5
- package/echo/echo_srpc.pb.go +6 -9
- package/echo/echo_srpc.pb.hpp +23 -3
- package/echo/echo_srpc.pb.rs +61 -26
- package/echo/echo_srpc.py +4 -1
- package/echo/half_close_test.rs +178 -0
- package/echo/integration_server.rs +14 -21
- package/echo/main.rs +28 -48
- package/go.mod +1 -1
- package/go.sum +2 -18
- package/integration/cross-language/cpp-client.cpp +446 -402
- package/integration/cross-language/cpp-pipe-server.cpp +139 -0
- package/integration/cross-language/cpp-pipe_test.go +114 -0
- package/integration/cross-language/cpp-server.cpp +222 -186
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +10 -9
- package/package.json +1 -1
- package/srpc/CMakeLists.txt +3 -58
- package/srpc/README-cpp.md +69 -0
- package/srpc/accept.go +10 -4
- package/srpc/accept_test.go +25 -0
- package/srpc/build.rs +129 -34
- package/srpc/client-half-close_test.go +152 -0
- package/srpc/client-invoker.go +32 -42
- package/srpc/client-invoker_test.go +53 -0
- package/srpc/client-rpc.cpp +65 -80
- package/srpc/client-rpc.go +32 -33
- package/srpc/client-rpc.hpp +52 -38
- package/srpc/client-rpc.test.ts +67 -0
- package/srpc/client-rpc.ts +10 -0
- package/srpc/client-rpc_test.go +76 -0
- package/srpc/client.cpp +99 -87
- package/srpc/client.hpp +46 -35
- package/srpc/client.rs +78 -38
- package/srpc/client_half_close_test.cc +331 -0
- package/srpc/client_invoker.rs +75 -0
- package/srpc/common-rpc.cpp +186 -146
- package/srpc/common-rpc.go +5 -4
- package/srpc/common-rpc.hpp +119 -89
- package/srpc/common-rpc.ts +43 -8
- package/srpc/common-rpc_test.go +23 -0
- package/srpc/error.rs +19 -2
- package/srpc/errors.hpp +80 -50
- package/srpc/errors.ts +16 -0
- package/srpc/handler.hpp +9 -7
- package/srpc/handler.rs +7 -45
- package/srpc/index.ts +2 -0
- package/srpc/invoker.hpp +82 -55
- package/srpc/invoker.rs +14 -52
- package/srpc/lib.rs +2 -0
- package/srpc/message.hpp +17 -12
- package/srpc/msg-stream.go +1 -0
- package/srpc/msg-stream.hpp +108 -61
- package/srpc/mux.cpp +84 -84
- package/srpc/mux.hpp +48 -38
- package/srpc/mux.rs +4 -4
- package/srpc/net.go +12 -3
- package/srpc/packet.cpp +60 -58
- package/srpc/packet.hpp +35 -20
- package/srpc/packet.rs +8 -27
- package/srpc/rpc.rs +332 -115
- package/srpc/rpc_lifecycle_test.cc +333 -0
- package/srpc/server-cancel_test.go +39 -0
- package/srpc/server-rpc.cpp +83 -73
- package/srpc/server-rpc.go +21 -19
- package/srpc/server-rpc.hpp +56 -37
- package/srpc/server.rs +76 -131
- package/srpc/starpc.hpp +4 -2
- package/srpc/stream.go +4 -0
- package/srpc/stream.hpp +95 -34
- package/srpc/stream.rs +7 -2
- package/srpc/testing.rs +11 -66
- package/srpc/transport.rs +21 -71
- package/srpc/websocket.rs +106 -57
- package/srpc/writer.hpp +46 -32
- package/srpc/yamux.rs +82 -26
- package/srpc/tests/integration_test.rs +0 -495
package/README.md
CHANGED
|
@@ -145,16 +145,18 @@ For TypeScript <-> TypeScript, see the [e2e] test.
|
|
|
145
145
|
|
|
146
146
|
### Rust
|
|
147
147
|
|
|
148
|
+
These examples use the repository's current Rust API. Unary handlers receive the transport's cancellation context; streaming handlers obtain it from their stream.
|
|
149
|
+
|
|
148
150
|
Add the dependencies to your `Cargo.toml`:
|
|
149
151
|
|
|
150
152
|
```toml
|
|
151
153
|
[dependencies]
|
|
152
|
-
starpc = "
|
|
154
|
+
starpc = { git = "https://github.com/aperturerobotics/starpc" }
|
|
153
155
|
prost = "0.14"
|
|
154
|
-
tokio = { version = "1", features = ["rt", "macros"] }
|
|
156
|
+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] }
|
|
155
157
|
|
|
156
158
|
[build-dependencies]
|
|
157
|
-
starpc = {
|
|
159
|
+
starpc = { git = "https://github.com/aperturerobotics/starpc", features = ["build"] }
|
|
158
160
|
prost-build = "0.14"
|
|
159
161
|
```
|
|
160
162
|
|
|
@@ -171,7 +173,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
171
173
|
Implement and use your service:
|
|
172
174
|
|
|
173
175
|
```rust
|
|
174
|
-
use starpc::{
|
|
176
|
+
use starpc::{Error, Mux, Server, SrpcClient, Stream, StreamExt};
|
|
175
177
|
use std::sync::Arc;
|
|
176
178
|
|
|
177
179
|
// Include generated code
|
|
@@ -186,8 +188,34 @@ struct EchoServer;
|
|
|
186
188
|
|
|
187
189
|
#[starpc::async_trait]
|
|
188
190
|
impl EchoerServer for EchoServer {
|
|
189
|
-
async fn echo(&self, request: EchoMsg) -> starpc::Result<EchoMsg> {
|
|
190
|
-
Ok(request)
|
|
191
|
+
async fn echo(&self, _context: &starpc::Context, request: EchoMsg) -> starpc::Result<EchoMsg> {
|
|
192
|
+
Ok(request)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async fn echo_server_stream(&self, request: EchoMsg, stream: Box<dyn Stream>) -> starpc::Result<()> {
|
|
196
|
+
stream.msg_send(&request).await
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async fn echo_client_stream(&self, stream: &dyn Stream) -> starpc::Result<EchoMsg> {
|
|
200
|
+
let mut messages = Vec::new();
|
|
201
|
+
loop {
|
|
202
|
+
match stream.msg_recv::<EchoMsg>().await {
|
|
203
|
+
Ok(message) => messages.push(message.body),
|
|
204
|
+
Err(Error::StreamClosed) => break,
|
|
205
|
+
Err(error) => return Err(error),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
Ok(EchoMsg { body: messages.join(", ") })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async fn echo_bidi_stream(&self, stream: Box<dyn Stream>) -> starpc::Result<()> {
|
|
212
|
+
loop {
|
|
213
|
+
match stream.msg_recv::<EchoMsg>().await {
|
|
214
|
+
Ok(message) => stream.msg_send(&message).await?,
|
|
215
|
+
Err(Error::StreamClosed) => return Ok(()),
|
|
216
|
+
Err(error) => return Err(error),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
191
219
|
}
|
|
192
220
|
}
|
|
193
221
|
|
|
@@ -202,8 +230,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
202
230
|
let (client_stream, server_stream) = tokio::io::duplex(64 * 1024);
|
|
203
231
|
|
|
204
232
|
// Spawn server handler
|
|
205
|
-
tokio::spawn(async move {
|
|
206
|
-
|
|
233
|
+
let server = tokio::spawn(async move {
|
|
234
|
+
server.handle_stream(server_stream).await
|
|
207
235
|
});
|
|
208
236
|
|
|
209
237
|
// Create client
|
|
@@ -214,6 +242,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
214
242
|
// Make RPC call
|
|
215
243
|
let response = echoer.echo(&EchoMsg { body: "Hello!".into() }).await?;
|
|
216
244
|
println!("Response: {}", response.body);
|
|
245
|
+
server.await??;
|
|
217
246
|
|
|
218
247
|
Ok(())
|
|
219
248
|
}
|
package/dist/srpc/client-rpc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CommonRPC } from './common-rpc.js';
|
|
2
|
+
import { ERR_CLOSED_BEFORE_COMPLETION } from './errors.js';
|
|
2
3
|
// ClientRPC is an ongoing RPC from the client side.
|
|
3
4
|
export class ClientRPC extends CommonRPC {
|
|
4
5
|
constructor(service, method) {
|
|
@@ -30,4 +31,12 @@ export class ClientRPC extends CommonRPC {
|
|
|
30
31
|
// we do not implement server -> client RPCs.
|
|
31
32
|
throw new Error(`unexpected server to client rpc: ${packet.rpcService || '<empty>'}/${packet.rpcMethod || '<empty>'}`);
|
|
32
33
|
}
|
|
34
|
+
// handleSourceEnd fails the call when the transport ends before the remote
|
|
35
|
+
// sent a completion or an error, so a truncated stream does not read as a
|
|
36
|
+
// complete one.
|
|
37
|
+
async handleSourceEnd() {
|
|
38
|
+
if (!this.isRemoteCompleted) {
|
|
39
|
+
await this.close(new Error(ERR_CLOSED_BEFORE_COMPLETION));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ClientRPC } from './client-rpc.js';
|
|
3
|
+
import { isClosedBeforeCompletionError } from './errors.js';
|
|
4
|
+
import { Packet } from './rpcproto.pb.js';
|
|
5
|
+
describe('ClientRPC', () => {
|
|
6
|
+
it('fails a call whose transport ends before the remote completes it', async () => {
|
|
7
|
+
const call = new ClientRPC('svc', 'method');
|
|
8
|
+
await call.sink(packetSource(callDataPacket(new Uint8Array([1]))));
|
|
9
|
+
const received = new Array();
|
|
10
|
+
const read = async () => {
|
|
11
|
+
for await (const data of call.rpcDataSource) {
|
|
12
|
+
received.push(data[0]);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const err = await read().then(() => undefined, (err) => err);
|
|
16
|
+
expect(received).toEqual([1]);
|
|
17
|
+
expect(isClosedBeforeCompletionError(err)).toBe(true);
|
|
18
|
+
expect(isClosedBeforeCompletionError(call.isClosed)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('ends a call cleanly when the remote completes it before the transport ends', async () => {
|
|
21
|
+
const call = new ClientRPC('svc', 'method');
|
|
22
|
+
await call.sink(packetSource(callDataPacket(new Uint8Array([1])), callDataPacket(undefined, true)));
|
|
23
|
+
const received = new Array();
|
|
24
|
+
for await (const data of call.rpcDataSource) {
|
|
25
|
+
received.push(data[0]);
|
|
26
|
+
}
|
|
27
|
+
expect(received).toEqual([1]);
|
|
28
|
+
expect(call.isClosed).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
// callDataPacket builds an incoming call-data packet.
|
|
32
|
+
function callDataPacket(data, complete = false) {
|
|
33
|
+
return Packet.create({
|
|
34
|
+
body: {
|
|
35
|
+
case: 'callData',
|
|
36
|
+
value: {
|
|
37
|
+
data: data ?? new Uint8Array(0),
|
|
38
|
+
dataIsZero: false,
|
|
39
|
+
complete,
|
|
40
|
+
error: '',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// packetSource yields packets and then ends like a closed transport.
|
|
46
|
+
async function* packetSource(...packets) {
|
|
47
|
+
for (const packet of packets) {
|
|
48
|
+
yield packet;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -10,10 +10,12 @@ export declare class CommonRPC {
|
|
|
10
10
|
protected method?: string;
|
|
11
11
|
private closed?;
|
|
12
12
|
private remoteError?;
|
|
13
|
+
private remoteCompleted;
|
|
13
14
|
private readonly invocationController;
|
|
14
15
|
private readonly writeDrainAbort;
|
|
15
16
|
constructor();
|
|
16
17
|
get isClosed(): boolean | Error;
|
|
18
|
+
protected get isRemoteCompleted(): boolean;
|
|
17
19
|
protected get invocationSignal(): AbortSignal;
|
|
18
20
|
writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
|
|
19
21
|
private writeCallDataPacket;
|
|
@@ -27,6 +29,9 @@ export declare class CommonRPC {
|
|
|
27
29
|
handleCallData(packet: Partial<CallData>): Promise<void>;
|
|
28
30
|
handleCallCancel(): Promise<void>;
|
|
29
31
|
close(err?: Error): Promise<void>;
|
|
32
|
+
private endRpcData;
|
|
33
|
+
private readRpcData;
|
|
34
|
+
protected handleSourceEnd(): Promise<void>;
|
|
30
35
|
private _createSink;
|
|
31
36
|
}
|
|
32
37
|
interface WritePacketOptions {
|
package/dist/srpc/common-rpc.js
CHANGED
|
@@ -14,7 +14,9 @@ export class CommonRPC {
|
|
|
14
14
|
_source = pushable({
|
|
15
15
|
objectMode: true,
|
|
16
16
|
});
|
|
17
|
-
// _rpcDataSource
|
|
17
|
+
// _rpcDataSource queues incoming rpc messages and the terminal error.
|
|
18
|
+
// The error is queued as an item because ending a pushable with an error
|
|
19
|
+
// discards the messages it buffered.
|
|
18
20
|
_rpcDataSource = pushable({
|
|
19
21
|
objectMode: true,
|
|
20
22
|
});
|
|
@@ -26,6 +28,8 @@ export class CommonRPC {
|
|
|
26
28
|
closed;
|
|
27
29
|
// remoteError records a remote error or transport failure.
|
|
28
30
|
remoteError;
|
|
31
|
+
// remoteCompleted records that the remote sent a completion or an error.
|
|
32
|
+
remoteCompleted = false;
|
|
29
33
|
// invocationController cancels the server invocation when the RPC closes.
|
|
30
34
|
invocationController = new AbortController();
|
|
31
35
|
// writeDrainAbort wakes writers waiting for outbound stream drain on close.
|
|
@@ -33,12 +37,16 @@ export class CommonRPC {
|
|
|
33
37
|
constructor() {
|
|
34
38
|
this.sink = this._createSink();
|
|
35
39
|
this.source = this._source;
|
|
36
|
-
this.rpcDataSource = this.
|
|
40
|
+
this.rpcDataSource = this.readRpcData();
|
|
37
41
|
}
|
|
38
42
|
// isClosed returns one of: true (closed w/o error), Error (closed w/ error), or false (not closed).
|
|
39
43
|
get isClosed() {
|
|
40
44
|
return this.closed ?? false;
|
|
41
45
|
}
|
|
46
|
+
// isRemoteCompleted reports whether the remote sent a completion or an error.
|
|
47
|
+
get isRemoteCompleted() {
|
|
48
|
+
return this.remoteCompleted;
|
|
49
|
+
}
|
|
42
50
|
// invocationSignal is canceled when the RPC closes.
|
|
43
51
|
get invocationSignal() {
|
|
44
52
|
return this.invocationController.signal;
|
|
@@ -171,11 +179,9 @@ export class CommonRPC {
|
|
|
171
179
|
this.remoteError ??= remoteError;
|
|
172
180
|
this.invocationController.abort();
|
|
173
181
|
}
|
|
174
|
-
if (packet.complete
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
else if (remoteError) {
|
|
178
|
-
this._rpcDataSource.end(remoteError);
|
|
182
|
+
if (packet.complete || remoteError) {
|
|
183
|
+
this.remoteCompleted = true;
|
|
184
|
+
this.endRpcData(remoteError);
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
// handleCallCancel aborts the invocation and closes the call.
|
|
@@ -201,7 +207,29 @@ export class CommonRPC {
|
|
|
201
207
|
}
|
|
202
208
|
this.writeDrainAbort.abort();
|
|
203
209
|
this._source.end();
|
|
204
|
-
this.
|
|
210
|
+
this.endRpcData(err);
|
|
211
|
+
}
|
|
212
|
+
// endRpcData ends the rpc message source after its buffered messages,
|
|
213
|
+
// optionally with an error.
|
|
214
|
+
endRpcData(err) {
|
|
215
|
+
if (err) {
|
|
216
|
+
this._rpcDataSource.push(err);
|
|
217
|
+
}
|
|
218
|
+
this._rpcDataSource.end();
|
|
219
|
+
}
|
|
220
|
+
// readRpcData yields the buffered rpc messages, then throws the terminal
|
|
221
|
+
// error, if any.
|
|
222
|
+
async *readRpcData() {
|
|
223
|
+
for await (const item of this._rpcDataSource) {
|
|
224
|
+
if (item instanceof Error) {
|
|
225
|
+
throw item;
|
|
226
|
+
}
|
|
227
|
+
yield item;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// handleSourceEnd handles the end of the incoming packet source.
|
|
231
|
+
async handleSourceEnd() {
|
|
232
|
+
// no-op
|
|
205
233
|
}
|
|
206
234
|
_createSink() {
|
|
207
235
|
return async (source) => {
|
|
@@ -216,6 +244,7 @@ export class CommonRPC {
|
|
|
216
244
|
await this.handlePacket(msg);
|
|
217
245
|
}
|
|
218
246
|
}
|
|
247
|
+
await this.handleSourceEnd();
|
|
219
248
|
}
|
|
220
249
|
catch (err) {
|
|
221
250
|
this.close(err);
|
package/dist/srpc/errors.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const ERR_RPC_ABORT = "ERR_RPC_ABORT";
|
|
2
2
|
export declare function isAbortError(err: unknown): boolean;
|
|
3
|
+
export declare const ERR_CLOSED_BEFORE_COMPLETION = "ERR_CLOSED_BEFORE_COMPLETION";
|
|
4
|
+
export declare function isClosedBeforeCompletionError(err: unknown): boolean;
|
|
3
5
|
export declare const ERR_STREAM_IDLE = "ERR_STREAM_IDLE";
|
|
4
6
|
export declare function isStreamIdleError(err: unknown): boolean;
|
|
5
7
|
export declare class RemoteRPCError extends Error {
|
package/dist/srpc/errors.js
CHANGED
|
@@ -8,6 +8,20 @@ export function isAbortError(err) {
|
|
|
8
8
|
const message = err.message;
|
|
9
9
|
return message === ERR_RPC_ABORT;
|
|
10
10
|
}
|
|
11
|
+
// ERR_CLOSED_BEFORE_COMPLETION is returned if the stream closed before the
|
|
12
|
+
// remote sent a completion or an error. The call has no verdict: the handler
|
|
13
|
+
// may have finished with its answer lost in the transport, or it may never
|
|
14
|
+
// have run.
|
|
15
|
+
export const ERR_CLOSED_BEFORE_COMPLETION = 'ERR_CLOSED_BEFORE_COMPLETION';
|
|
16
|
+
// isClosedBeforeCompletionError checks if the error object is
|
|
17
|
+
// ERR_CLOSED_BEFORE_COMPLETION.
|
|
18
|
+
export function isClosedBeforeCompletionError(err) {
|
|
19
|
+
if (typeof err !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const message = err.message;
|
|
23
|
+
return message === ERR_CLOSED_BEFORE_COMPLETION;
|
|
24
|
+
}
|
|
11
25
|
// ERR_STREAM_IDLE is returned if the stream idle timeout was exceeded.
|
|
12
26
|
export const ERR_STREAM_IDLE = 'ERR_STREAM_IDLE';
|
|
13
27
|
// isStreamIdleError checks if the error object is ERR_STREAM_IDLE.
|
package/dist/srpc/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
1
|
+
export { ERR_RPC_ABORT, isAbortError, ERR_CLOSED_BEFORE_COMPLETION, isClosedBeforeCompletionError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
2
2
|
export { Client } from './client.js';
|
|
3
3
|
export { Server } from './server.js';
|
|
4
4
|
export { StreamConn } from './conn.js';
|
package/dist/srpc/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
1
|
+
export { ERR_RPC_ABORT, isAbortError, ERR_CLOSED_BEFORE_COMPLETION, isClosedBeforeCompletionError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
2
2
|
export { Client } from './client.js';
|
|
3
3
|
export { Server } from './server.js';
|
|
4
4
|
export { StreamConn } from './conn.js';
|
package/echo/Cargo.toml
CHANGED
|
@@ -23,3 +23,11 @@ starpc = { workspace = true }
|
|
|
23
23
|
prost = { workspace = true }
|
|
24
24
|
tokio = { version = "1", features = ["full"] }
|
|
25
25
|
async-trait = { workspace = true }
|
|
26
|
+
futures = { workspace = true }
|
|
27
|
+
|
|
28
|
+
[[test]]
|
|
29
|
+
name = "half_close"
|
|
30
|
+
path = "half_close_test.rs"
|
|
31
|
+
|
|
32
|
+
[dev-dependencies]
|
|
33
|
+
tokio-util = { workspace = true }
|