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/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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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 {}