starpc 0.41.2 → 0.43.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/README.md +101 -20
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +4 -5
- package/dist/mock/mock_srpc.pb.d.ts +3 -3
- package/dist/mock/mock_srpc.pb.js +5 -5
- package/echo/Cargo.toml +21 -0
- package/echo/build.rs +15 -0
- package/echo/echo.pb.cc +405 -0
- package/echo/echo.pb.go +9 -27
- package/echo/echo.pb.h +364 -0
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/gen/mod.rs +3 -0
- package/echo/main.rs +162 -0
- package/go.mod +18 -10
- package/go.sum +28 -18
- package/mock/mock.pb.cc +394 -0
- package/mock/mock.pb.go +9 -27
- package/mock/mock.pb.h +366 -0
- package/mock/mock.pb.ts +11 -13
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.ts +12 -9
- package/package.json +27 -25
- package/srpc/Cargo.toml +26 -0
- package/srpc/build.rs +15 -0
- package/srpc/client.rs +356 -0
- package/srpc/codec.rs +225 -0
- package/srpc/error.rs +177 -0
- package/srpc/handler.rs +163 -0
- package/srpc/invoker.rs +192 -0
- package/srpc/lib.rs +107 -0
- package/srpc/message.rs +9 -0
- package/srpc/mux.rs +353 -0
- package/srpc/packet.rs +334 -0
- package/srpc/proto/mod.rs +10 -0
- package/srpc/rpc.rs +777 -0
- package/srpc/rpcproto.pb.cc +1381 -0
- package/srpc/rpcproto.pb.go +75 -183
- package/srpc/rpcproto.pb.h +1451 -0
- package/srpc/server.rs +337 -0
- package/srpc/stream.rs +304 -0
- package/srpc/testing.rs +290 -0
- package/srpc/tests/integration_test.rs +495 -0
- package/srpc/transport.rs +218 -0
- package/Makefile +0 -154
package/srpc/error.rs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//! Error types for starpc.
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides the error types used throughout the starpc library,
|
|
4
|
+
//! matching the error semantics of the Go and TypeScript implementations.
|
|
5
|
+
|
|
6
|
+
use thiserror::Error;
|
|
7
|
+
|
|
8
|
+
/// Errors that can occur in starpc operations.
|
|
9
|
+
#[derive(Error, Debug)]
|
|
10
|
+
pub enum Error {
|
|
11
|
+
/// The requested RPC method is not implemented.
|
|
12
|
+
#[error("method not implemented")]
|
|
13
|
+
Unimplemented,
|
|
14
|
+
|
|
15
|
+
/// A packet was received after the RPC was completed.
|
|
16
|
+
#[error("unexpected packet after rpc was completed")]
|
|
17
|
+
Completed,
|
|
18
|
+
|
|
19
|
+
/// An unrecognized packet type was received.
|
|
20
|
+
#[error("unrecognized packet type")]
|
|
21
|
+
UnrecognizedPacket,
|
|
22
|
+
|
|
23
|
+
/// An empty packet was received (no body or CallData with no content).
|
|
24
|
+
#[error("invalid empty packet")]
|
|
25
|
+
EmptyPacket,
|
|
26
|
+
|
|
27
|
+
/// Invalid message format (protobuf decode error).
|
|
28
|
+
#[error("invalid message: {0}")]
|
|
29
|
+
InvalidMessage(#[from] prost::DecodeError),
|
|
30
|
+
|
|
31
|
+
/// The method ID is empty.
|
|
32
|
+
#[error("method id empty")]
|
|
33
|
+
EmptyMethodId,
|
|
34
|
+
|
|
35
|
+
/// The service ID is empty.
|
|
36
|
+
#[error("service id empty")]
|
|
37
|
+
EmptyServiceId,
|
|
38
|
+
|
|
39
|
+
/// No RPC clients are available.
|
|
40
|
+
#[error("no available rpc clients")]
|
|
41
|
+
NoAvailableClients,
|
|
42
|
+
|
|
43
|
+
/// The writer is not initialized.
|
|
44
|
+
#[error("writer cannot be nil")]
|
|
45
|
+
NilWriter,
|
|
46
|
+
|
|
47
|
+
/// IO error during read/write operations.
|
|
48
|
+
#[error("io error: {0}")]
|
|
49
|
+
Io(#[from] std::io::Error),
|
|
50
|
+
|
|
51
|
+
/// The stream was closed.
|
|
52
|
+
#[error("stream closed")]
|
|
53
|
+
StreamClosed,
|
|
54
|
+
|
|
55
|
+
/// The RPC was aborted.
|
|
56
|
+
#[error("rpc aborted")]
|
|
57
|
+
Aborted,
|
|
58
|
+
|
|
59
|
+
/// The context was cancelled.
|
|
60
|
+
#[error("context cancelled")]
|
|
61
|
+
Cancelled,
|
|
62
|
+
|
|
63
|
+
/// The stream idle timeout was exceeded.
|
|
64
|
+
#[error("stream idle timeout exceeded")]
|
|
65
|
+
StreamIdle,
|
|
66
|
+
|
|
67
|
+
/// Remote error from the other end.
|
|
68
|
+
#[error("remote error: {0}")]
|
|
69
|
+
Remote(String),
|
|
70
|
+
|
|
71
|
+
/// Message size exceeds maximum allowed size.
|
|
72
|
+
#[error("message size {0} exceeds maximum {1}")]
|
|
73
|
+
MessageTooLarge(usize, usize),
|
|
74
|
+
|
|
75
|
+
/// Message size is zero but data_is_zero flag is not set.
|
|
76
|
+
#[error("unexpected zero length prefix")]
|
|
77
|
+
MessageSizeZero,
|
|
78
|
+
|
|
79
|
+
/// Expected CallStart packet but got a different packet type.
|
|
80
|
+
#[error("expected CallStart packet")]
|
|
81
|
+
ExpectedCallStart,
|
|
82
|
+
|
|
83
|
+
/// CallStart was sent more than once.
|
|
84
|
+
#[error("call start must be sent only once")]
|
|
85
|
+
DuplicateCallStart,
|
|
86
|
+
|
|
87
|
+
/// CallData received before CallStart.
|
|
88
|
+
#[error("call start must be sent before call data")]
|
|
89
|
+
CallDataBeforeStart,
|
|
90
|
+
|
|
91
|
+
/// Protocol encode error.
|
|
92
|
+
#[error("encode error: {0}")]
|
|
93
|
+
Encode(#[from] prost::EncodeError),
|
|
94
|
+
|
|
95
|
+
/// Channel send error (internal).
|
|
96
|
+
#[error("channel closed")]
|
|
97
|
+
ChannelClosed,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl Error {
|
|
101
|
+
/// Returns true if this error indicates the RPC was aborted.
|
|
102
|
+
pub fn is_abort(&self) -> bool {
|
|
103
|
+
matches!(self, Error::Aborted | Error::Cancelled)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Returns true if this error indicates the stream was closed.
|
|
107
|
+
pub fn is_closed(&self) -> bool {
|
|
108
|
+
matches!(self, Error::StreamClosed | Error::Cancelled)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Returns true if this error indicates a timeout.
|
|
112
|
+
pub fn is_timeout(&self) -> bool {
|
|
113
|
+
matches!(self, Error::StreamIdle)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Returns true if this error indicates the method is not implemented.
|
|
117
|
+
pub fn is_unimplemented(&self) -> bool {
|
|
118
|
+
matches!(self, Error::Unimplemented)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Creates a remote error from a string.
|
|
122
|
+
pub fn remote(msg: impl Into<String>) -> Self {
|
|
123
|
+
Error::Remote(msg.into())
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Result type alias using starpc's Error type.
|
|
128
|
+
pub type Result<T> = std::result::Result<T, Error>;
|
|
129
|
+
|
|
130
|
+
/// Error code constants matching the TypeScript implementation.
|
|
131
|
+
pub mod codes {
|
|
132
|
+
/// Error code for RPC abort.
|
|
133
|
+
pub const ERR_RPC_ABORT: &str = "ERR_RPC_ABORT";
|
|
134
|
+
|
|
135
|
+
/// Error code for stream idle timeout.
|
|
136
|
+
pub const ERR_STREAM_IDLE: &str = "ERR_STREAM_IDLE";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Checks if an error message indicates an abort.
|
|
140
|
+
pub fn is_abort_error_message(msg: &str) -> bool {
|
|
141
|
+
msg == codes::ERR_RPC_ABORT || msg == "rpc aborted" || msg == "context cancelled"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Checks if an error message indicates a stream idle timeout.
|
|
145
|
+
pub fn is_stream_idle_error_message(msg: &str) -> bool {
|
|
146
|
+
msg == codes::ERR_STREAM_IDLE || msg == "stream idle timeout exceeded"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[cfg(test)]
|
|
150
|
+
mod tests {
|
|
151
|
+
use super::*;
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn test_error_display() {
|
|
155
|
+
assert_eq!(Error::Unimplemented.to_string(), "method not implemented");
|
|
156
|
+
assert_eq!(Error::Completed.to_string(), "unexpected packet after rpc was completed");
|
|
157
|
+
assert_eq!(Error::EmptyMethodId.to_string(), "method id empty");
|
|
158
|
+
assert_eq!(Error::Remote("test error".into()).to_string(), "remote error: test error");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn test_error_predicates() {
|
|
163
|
+
assert!(Error::Aborted.is_abort());
|
|
164
|
+
assert!(Error::Cancelled.is_abort());
|
|
165
|
+
assert!(!Error::StreamClosed.is_abort());
|
|
166
|
+
|
|
167
|
+
assert!(Error::StreamClosed.is_closed());
|
|
168
|
+
assert!(Error::Cancelled.is_closed());
|
|
169
|
+
assert!(!Error::Aborted.is_closed());
|
|
170
|
+
|
|
171
|
+
assert!(Error::StreamIdle.is_timeout());
|
|
172
|
+
assert!(!Error::Cancelled.is_timeout());
|
|
173
|
+
|
|
174
|
+
assert!(Error::Unimplemented.is_unimplemented());
|
|
175
|
+
assert!(!Error::Cancelled.is_unimplemented());
|
|
176
|
+
}
|
|
177
|
+
}
|
package/srpc/handler.rs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
//! Handler trait for RPC service implementations.
|
|
2
|
+
//!
|
|
3
|
+
//! A Handler is an Invoker that also provides metadata about the service
|
|
4
|
+
//! it handles. This allows the Mux to register handlers and route calls
|
|
5
|
+
//! based on service and method IDs.
|
|
6
|
+
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use crate::invoker::Invoker;
|
|
10
|
+
|
|
11
|
+
/// Trait for RPC service handlers.
|
|
12
|
+
///
|
|
13
|
+
/// A Handler extends Invoker with metadata methods that describe the service
|
|
14
|
+
/// and methods it implements. This is the trait that generated service code
|
|
15
|
+
/// typically implements.
|
|
16
|
+
///
|
|
17
|
+
/// # Example
|
|
18
|
+
///
|
|
19
|
+
/// ```rust,ignore
|
|
20
|
+
/// struct MyServiceHandler {
|
|
21
|
+
/// // Handler state
|
|
22
|
+
/// }
|
|
23
|
+
///
|
|
24
|
+
/// #[async_trait]
|
|
25
|
+
/// impl Invoker for MyServiceHandler {
|
|
26
|
+
/// async fn invoke_method(
|
|
27
|
+
/// &self,
|
|
28
|
+
/// service_id: &str,
|
|
29
|
+
/// method_id: &str,
|
|
30
|
+
/// stream: Box<dyn Stream>,
|
|
31
|
+
/// ) -> (bool, Result<()>) {
|
|
32
|
+
/// match method_id {
|
|
33
|
+
/// "Method1" => (true, self.method1(stream).await),
|
|
34
|
+
/// "Method2" => (true, self.method2(stream).await),
|
|
35
|
+
/// _ => (false, Err(Error::Unimplemented)),
|
|
36
|
+
/// }
|
|
37
|
+
/// }
|
|
38
|
+
/// }
|
|
39
|
+
///
|
|
40
|
+
/// impl Handler for MyServiceHandler {
|
|
41
|
+
/// fn service_id(&self) -> &'static str {
|
|
42
|
+
/// "my.package.MyService"
|
|
43
|
+
/// }
|
|
44
|
+
///
|
|
45
|
+
/// fn method_ids(&self) -> &'static [&'static str] {
|
|
46
|
+
/// &["Method1", "Method2"]
|
|
47
|
+
/// }
|
|
48
|
+
/// }
|
|
49
|
+
/// ```
|
|
50
|
+
pub trait Handler: Invoker {
|
|
51
|
+
/// Returns the service ID that this handler implements.
|
|
52
|
+
///
|
|
53
|
+
/// The service ID is typically the fully-qualified protobuf service name,
|
|
54
|
+
/// e.g., "echo.Echoer" or "my.package.MyService".
|
|
55
|
+
fn service_id(&self) -> &'static str;
|
|
56
|
+
|
|
57
|
+
/// Returns the list of method IDs that this handler implements.
|
|
58
|
+
///
|
|
59
|
+
/// These are the method names as defined in the protobuf service definition.
|
|
60
|
+
fn method_ids(&self) -> &'static [&'static str];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Boxed Handler trait object.
|
|
64
|
+
pub type BoxHandler = Box<dyn Handler>;
|
|
65
|
+
|
|
66
|
+
/// Arc-wrapped Handler trait object.
|
|
67
|
+
pub type ArcHandler = Arc<dyn Handler>;
|
|
68
|
+
|
|
69
|
+
// Note: We can't provide blanket implementations for Arc<T> and Box<T>
|
|
70
|
+
// because Handler extends Invoker which already has these implementations.
|
|
71
|
+
// The Mux uses ArcHandler directly.
|
|
72
|
+
|
|
73
|
+
#[cfg(test)]
|
|
74
|
+
mod tests {
|
|
75
|
+
use super::*;
|
|
76
|
+
use crate::error::{Error, Result};
|
|
77
|
+
use crate::stream::{Context, Stream};
|
|
78
|
+
use async_trait::async_trait;
|
|
79
|
+
|
|
80
|
+
struct TestHandler;
|
|
81
|
+
|
|
82
|
+
#[async_trait]
|
|
83
|
+
impl Invoker for TestHandler {
|
|
84
|
+
async fn invoke_method(
|
|
85
|
+
&self,
|
|
86
|
+
_service_id: &str,
|
|
87
|
+
method_id: &str,
|
|
88
|
+
_stream: Box<dyn Stream>,
|
|
89
|
+
) -> (bool, Result<()>) {
|
|
90
|
+
match method_id {
|
|
91
|
+
"Method1" | "Method2" => (true, Ok(())),
|
|
92
|
+
_ => (false, Err(Error::Unimplemented)),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Handler for TestHandler {
|
|
98
|
+
fn service_id(&self) -> &'static str {
|
|
99
|
+
"test.Service"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn method_ids(&self) -> &'static [&'static str] {
|
|
103
|
+
&["Method1", "Method2"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn test_handler_metadata() {
|
|
109
|
+
let handler = TestHandler;
|
|
110
|
+
assert_eq!(handler.service_id(), "test.Service");
|
|
111
|
+
assert_eq!(handler.method_ids(), &["Method1", "Method2"]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn test_arc_handler() {
|
|
116
|
+
let handler: ArcHandler = Arc::new(TestHandler);
|
|
117
|
+
assert_eq!(handler.service_id(), "test.Service");
|
|
118
|
+
assert_eq!(handler.method_ids(), &["Method1", "Method2"]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
struct MockStream;
|
|
122
|
+
|
|
123
|
+
#[async_trait]
|
|
124
|
+
impl Stream for MockStream {
|
|
125
|
+
fn context(&self) -> &Context {
|
|
126
|
+
static CTX: std::sync::OnceLock<Context> = std::sync::OnceLock::new();
|
|
127
|
+
CTX.get_or_init(Context::new)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async fn send_bytes(&self, _data: bytes::Bytes) -> Result<()> {
|
|
131
|
+
Ok(())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async fn recv_bytes(&self) -> Result<bytes::Bytes> {
|
|
135
|
+
Err(Error::StreamClosed)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async fn close_send(&self) -> Result<()> {
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async fn close(&self) -> Result<()> {
|
|
143
|
+
Ok(())
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[tokio::test]
|
|
148
|
+
async fn test_handler_invoke() {
|
|
149
|
+
let handler: ArcHandler = Arc::new(TestHandler);
|
|
150
|
+
|
|
151
|
+
let (found, result) = handler
|
|
152
|
+
.invoke_method("test.Service", "Method1", Box::new(MockStream))
|
|
153
|
+
.await;
|
|
154
|
+
assert!(found);
|
|
155
|
+
assert!(result.is_ok());
|
|
156
|
+
|
|
157
|
+
let (found, result) = handler
|
|
158
|
+
.invoke_method("test.Service", "Unknown", Box::new(MockStream))
|
|
159
|
+
.await;
|
|
160
|
+
assert!(!found);
|
|
161
|
+
assert!(result.is_err());
|
|
162
|
+
}
|
|
163
|
+
}
|
package/srpc/invoker.rs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//! Invoker trait for RPC method invocation.
|
|
2
|
+
//!
|
|
3
|
+
//! The Invoker trait defines the interface for dispatching RPC calls to handlers.
|
|
4
|
+
//! This is the core abstraction that allows the Mux, Server, and generated code
|
|
5
|
+
//! to route calls appropriately.
|
|
6
|
+
|
|
7
|
+
use async_trait::async_trait;
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
use crate::error::Result;
|
|
11
|
+
use crate::stream::Stream;
|
|
12
|
+
|
|
13
|
+
/// Trait for invoking RPC methods.
|
|
14
|
+
///
|
|
15
|
+
/// An Invoker is responsible for dispatching incoming RPC calls to the
|
|
16
|
+
/// appropriate handler implementation. The Mux implements this trait to
|
|
17
|
+
/// route calls based on service and method IDs.
|
|
18
|
+
///
|
|
19
|
+
/// # Return Value
|
|
20
|
+
///
|
|
21
|
+
/// The `invoke_method` method returns a tuple of `(found, result)`:
|
|
22
|
+
/// - `found`: Whether the method was found and handled
|
|
23
|
+
/// - `result`: The result of the invocation, or an error
|
|
24
|
+
///
|
|
25
|
+
/// This design allows callers to distinguish between:
|
|
26
|
+
/// - Method not found: `(false, Err(Error::Unimplemented))`
|
|
27
|
+
/// - Method found but failed: `(true, Err(...))`
|
|
28
|
+
/// - Method found and succeeded: `(true, Ok(()))`
|
|
29
|
+
///
|
|
30
|
+
/// # Example
|
|
31
|
+
///
|
|
32
|
+
/// ```rust,ignore
|
|
33
|
+
/// #[async_trait]
|
|
34
|
+
/// impl Invoker for MyHandler {
|
|
35
|
+
/// async fn invoke_method(
|
|
36
|
+
/// &self,
|
|
37
|
+
/// service_id: &str,
|
|
38
|
+
/// method_id: &str,
|
|
39
|
+
/// stream: Box<dyn Stream>,
|
|
40
|
+
/// ) -> (bool, Result<()>) {
|
|
41
|
+
/// match method_id {
|
|
42
|
+
/// "MyMethod" => {
|
|
43
|
+
/// // Handle the method
|
|
44
|
+
/// (true, self.my_method(stream).await)
|
|
45
|
+
/// }
|
|
46
|
+
/// _ => (false, Err(Error::Unimplemented)),
|
|
47
|
+
/// }
|
|
48
|
+
/// }
|
|
49
|
+
/// }
|
|
50
|
+
/// ```
|
|
51
|
+
#[async_trait]
|
|
52
|
+
pub trait Invoker: Send + Sync {
|
|
53
|
+
/// Invokes an RPC method.
|
|
54
|
+
///
|
|
55
|
+
/// # Arguments
|
|
56
|
+
///
|
|
57
|
+
/// * `service_id` - The service identifier (e.g., "echo.Echoer")
|
|
58
|
+
/// * `method_id` - The method identifier (e.g., "Echo")
|
|
59
|
+
/// * `stream` - The bidirectional stream for this RPC
|
|
60
|
+
///
|
|
61
|
+
/// # Returns
|
|
62
|
+
///
|
|
63
|
+
/// A tuple of (found, result) where:
|
|
64
|
+
/// * `found` - Whether the method was found and handled
|
|
65
|
+
/// * `result` - The result of the invocation
|
|
66
|
+
async fn invoke_method(
|
|
67
|
+
&self,
|
|
68
|
+
service_id: &str,
|
|
69
|
+
method_id: &str,
|
|
70
|
+
stream: Box<dyn Stream>,
|
|
71
|
+
) -> (bool, Result<()>);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Boxed Invoker trait object.
|
|
75
|
+
pub type BoxInvoker = Box<dyn Invoker>;
|
|
76
|
+
|
|
77
|
+
/// Arc-wrapped Invoker trait object.
|
|
78
|
+
pub type ArcInvoker = Arc<dyn Invoker>;
|
|
79
|
+
|
|
80
|
+
// Blanket implementation for Arc<T> where T: Invoker
|
|
81
|
+
#[async_trait]
|
|
82
|
+
impl<T: Invoker + ?Sized> Invoker for Arc<T> {
|
|
83
|
+
async fn invoke_method(
|
|
84
|
+
&self,
|
|
85
|
+
service_id: &str,
|
|
86
|
+
method_id: &str,
|
|
87
|
+
stream: Box<dyn Stream>,
|
|
88
|
+
) -> (bool, Result<()>) {
|
|
89
|
+
(**self).invoke_method(service_id, method_id, stream).await
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Blanket implementation for Box<T> where T: Invoker
|
|
94
|
+
#[async_trait]
|
|
95
|
+
impl<T: Invoker + ?Sized> Invoker for Box<T> {
|
|
96
|
+
async fn invoke_method(
|
|
97
|
+
&self,
|
|
98
|
+
service_id: &str,
|
|
99
|
+
method_id: &str,
|
|
100
|
+
stream: Box<dyn Stream>,
|
|
101
|
+
) -> (bool, Result<()>) {
|
|
102
|
+
(**self).invoke_method(service_id, method_id, stream).await
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[cfg(test)]
|
|
107
|
+
mod tests {
|
|
108
|
+
use super::*;
|
|
109
|
+
use crate::error::Error;
|
|
110
|
+
use crate::stream::Context;
|
|
111
|
+
|
|
112
|
+
struct TestInvoker {
|
|
113
|
+
should_handle: bool,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[async_trait]
|
|
117
|
+
impl Invoker for TestInvoker {
|
|
118
|
+
async fn invoke_method(
|
|
119
|
+
&self,
|
|
120
|
+
_service_id: &str,
|
|
121
|
+
_method_id: &str,
|
|
122
|
+
_stream: Box<dyn Stream>,
|
|
123
|
+
) -> (bool, Result<()>) {
|
|
124
|
+
if self.should_handle {
|
|
125
|
+
(true, Ok(()))
|
|
126
|
+
} else {
|
|
127
|
+
(false, Err(Error::Unimplemented))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
struct MockStream;
|
|
133
|
+
|
|
134
|
+
#[async_trait]
|
|
135
|
+
impl Stream for MockStream {
|
|
136
|
+
fn context(&self) -> &Context {
|
|
137
|
+
static CTX: std::sync::OnceLock<Context> = std::sync::OnceLock::new();
|
|
138
|
+
CTX.get_or_init(Context::new)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async fn send_bytes(&self, _data: bytes::Bytes) -> Result<()> {
|
|
142
|
+
Ok(())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async fn recv_bytes(&self) -> Result<bytes::Bytes> {
|
|
146
|
+
Err(Error::StreamClosed)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async fn close_send(&self) -> Result<()> {
|
|
150
|
+
Ok(())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async fn close(&self) -> Result<()> {
|
|
154
|
+
Ok(())
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[tokio::test]
|
|
159
|
+
async fn test_invoker_found() {
|
|
160
|
+
let invoker = TestInvoker { should_handle: true };
|
|
161
|
+
let (found, result) = invoker
|
|
162
|
+
.invoke_method("svc", "method", Box::new(MockStream))
|
|
163
|
+
.await;
|
|
164
|
+
|
|
165
|
+
assert!(found);
|
|
166
|
+
assert!(result.is_ok());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[tokio::test]
|
|
170
|
+
async fn test_invoker_not_found() {
|
|
171
|
+
let invoker = TestInvoker {
|
|
172
|
+
should_handle: false,
|
|
173
|
+
};
|
|
174
|
+
let (found, result) = invoker
|
|
175
|
+
.invoke_method("svc", "method", Box::new(MockStream))
|
|
176
|
+
.await;
|
|
177
|
+
|
|
178
|
+
assert!(!found);
|
|
179
|
+
assert!(result.is_err());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[tokio::test]
|
|
183
|
+
async fn test_arc_invoker() {
|
|
184
|
+
let invoker: Arc<dyn Invoker> = Arc::new(TestInvoker { should_handle: true });
|
|
185
|
+
let (found, result) = invoker
|
|
186
|
+
.invoke_method("svc", "method", Box::new(MockStream))
|
|
187
|
+
.await;
|
|
188
|
+
|
|
189
|
+
assert!(found);
|
|
190
|
+
assert!(result.is_ok());
|
|
191
|
+
}
|
|
192
|
+
}
|
package/srpc/lib.rs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//! Starpc - Streaming Protobuf RPC Framework
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides a streaming RPC framework built on protobuf, offering
|
|
4
|
+
//! full-duplex bidirectional streaming with support for unary, server-streaming,
|
|
5
|
+
//! client-streaming, and bidirectional streaming RPC patterns.
|
|
6
|
+
//!
|
|
7
|
+
//! # Features
|
|
8
|
+
//!
|
|
9
|
+
//! - **Wire-compatible** with the Go and TypeScript implementations
|
|
10
|
+
//! - **Streaming support** for all RPC patterns
|
|
11
|
+
//! - **Transport agnostic** - works with TCP, WebSocket, or any AsyncRead/AsyncWrite
|
|
12
|
+
//! - **Code generation** via starpc-build crate
|
|
13
|
+
//!
|
|
14
|
+
//! # Quick Start
|
|
15
|
+
//!
|
|
16
|
+
//! ## Client
|
|
17
|
+
//!
|
|
18
|
+
//! ```rust,ignore
|
|
19
|
+
//! use starpc::{Client, SrpcClient};
|
|
20
|
+
//! use starpc::client::transport::SingleStreamOpener;
|
|
21
|
+
//! use tokio::net::TcpStream;
|
|
22
|
+
//!
|
|
23
|
+
//! // Connect to a server
|
|
24
|
+
//! let stream = TcpStream::connect("127.0.0.1:8080").await?;
|
|
25
|
+
//! let opener = SingleStreamOpener::new(stream);
|
|
26
|
+
//! let client = SrpcClient::new(opener);
|
|
27
|
+
//!
|
|
28
|
+
//! // Make a unary call
|
|
29
|
+
//! let response: MyResponse = client
|
|
30
|
+
//! .exec_call("my.Service", "MyMethod", &request)
|
|
31
|
+
//! .await?;
|
|
32
|
+
//! ```
|
|
33
|
+
//!
|
|
34
|
+
//! ## Server
|
|
35
|
+
//!
|
|
36
|
+
//! ```rust,ignore
|
|
37
|
+
//! use starpc::{Server, Mux, Handler};
|
|
38
|
+
//! use std::sync::Arc;
|
|
39
|
+
//!
|
|
40
|
+
//! // Create a mux and register handlers
|
|
41
|
+
//! let mux = Arc::new(Mux::new());
|
|
42
|
+
//! mux.register(Arc::new(MyServiceHandler))?;
|
|
43
|
+
//!
|
|
44
|
+
//! // Create the server
|
|
45
|
+
//! let server = Server::with_arc(mux);
|
|
46
|
+
//!
|
|
47
|
+
//! // Handle a connection
|
|
48
|
+
//! server.handle_stream(tcp_stream).await?;
|
|
49
|
+
//! ```
|
|
50
|
+
//!
|
|
51
|
+
//! # Wire Format
|
|
52
|
+
//!
|
|
53
|
+
//! Starpc uses a simple length-prefixed framing:
|
|
54
|
+
//! - 4-byte little-endian u32 length prefix
|
|
55
|
+
//! - Protobuf-encoded Packet message
|
|
56
|
+
//!
|
|
57
|
+
//! This format is compatible with the Go and TypeScript implementations.
|
|
58
|
+
|
|
59
|
+
pub mod client;
|
|
60
|
+
pub mod codec;
|
|
61
|
+
pub mod error;
|
|
62
|
+
pub mod handler;
|
|
63
|
+
pub mod invoker;
|
|
64
|
+
pub mod message;
|
|
65
|
+
pub mod mux;
|
|
66
|
+
pub mod packet;
|
|
67
|
+
pub mod proto;
|
|
68
|
+
pub mod rpc;
|
|
69
|
+
pub mod server;
|
|
70
|
+
pub mod stream;
|
|
71
|
+
pub mod testing;
|
|
72
|
+
pub mod transport;
|
|
73
|
+
|
|
74
|
+
// Re-exports for convenience.
|
|
75
|
+
pub use client::{BoxClient, Client, OpenStream, SrpcClient};
|
|
76
|
+
pub use codec::{PacketCodec, MAX_MESSAGE_SIZE};
|
|
77
|
+
pub use error::{Error, Result};
|
|
78
|
+
pub use handler::{BoxHandler, Handler};
|
|
79
|
+
pub use invoker::{BoxInvoker, Invoker};
|
|
80
|
+
pub use message::Message;
|
|
81
|
+
pub use mux::{Mux, QueryableInvoker};
|
|
82
|
+
pub use packet::Validate;
|
|
83
|
+
pub use rpc::{ClientRpc, PacketWriter, ServerRpc};
|
|
84
|
+
pub use server::{Server, ServerConfig};
|
|
85
|
+
pub use stream::{ArcStream, BoxStream, Context, Stream, StreamExt};
|
|
86
|
+
pub use transport::{
|
|
87
|
+
create_packet_channel, decode_optional_data, encode_optional_data, TransportPacketWriter,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Re-export async_trait for use in generated code.
|
|
91
|
+
pub use async_trait::async_trait;
|
|
92
|
+
pub use prost::Message as ProstMessage;
|
|
93
|
+
|
|
94
|
+
/// Prelude module for convenient imports.
|
|
95
|
+
pub mod prelude {
|
|
96
|
+
pub use crate::client::{Client, OpenStream, SrpcClient};
|
|
97
|
+
pub use crate::error::{Error, Result};
|
|
98
|
+
pub use crate::handler::Handler;
|
|
99
|
+
pub use crate::invoker::Invoker;
|
|
100
|
+
pub use crate::mux::Mux;
|
|
101
|
+
pub use crate::packet::Validate;
|
|
102
|
+
pub use crate::server::Server;
|
|
103
|
+
pub use crate::stream::{Context, Stream, StreamExt};
|
|
104
|
+
|
|
105
|
+
pub use async_trait::async_trait;
|
|
106
|
+
pub use prost::Message as ProstMessage;
|
|
107
|
+
}
|
package/srpc/message.rs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//! Message trait for protobuf messages.
|
|
2
|
+
|
|
3
|
+
use prost::Message as ProstMessage;
|
|
4
|
+
|
|
5
|
+
/// Trait for protobuf messages that can be sent over starpc.
|
|
6
|
+
pub trait Message: ProstMessage + Default + Send + Sync + 'static {}
|
|
7
|
+
|
|
8
|
+
// Blanket implementation for all prost messages.
|
|
9
|
+
impl<T: ProstMessage + Default + Send + Sync + 'static> Message for T {}
|