starpc 0.52.0 → 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.
Files changed (152) hide show
  1. package/README.md +37 -8
  2. package/dist/echo/client-test.d.ts +1 -1
  3. package/dist/echo/client-test.js +110 -2
  4. package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
  5. package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
  6. package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
  7. package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
  8. package/dist/integration/cross-language/ts-client.js +50 -37
  9. package/dist/integration/cross-language/ts-server.js +1 -36
  10. package/dist/rpcstream/rpcstream.d.ts +5 -1
  11. package/dist/rpcstream/rpcstream.js +75 -28
  12. package/dist/rpcstream/rpcstream.test.d.ts +1 -0
  13. package/dist/rpcstream/rpcstream.test.js +92 -0
  14. package/dist/srpc/client-rpc.d.ts +1 -0
  15. package/dist/srpc/client-rpc.js +9 -0
  16. package/dist/srpc/client-rpc.test.d.ts +1 -0
  17. package/dist/srpc/client-rpc.test.js +50 -0
  18. package/dist/srpc/client.js +18 -4
  19. package/dist/srpc/common-rpc.d.ts +5 -0
  20. package/dist/srpc/common-rpc.js +37 -8
  21. package/dist/srpc/common-rpc.test.js +2 -0
  22. package/dist/srpc/errors.d.ts +2 -0
  23. package/dist/srpc/errors.js +14 -0
  24. package/dist/srpc/index.d.ts +1 -1
  25. package/dist/srpc/index.js +1 -1
  26. package/dist/srpc/packet-codec.test.d.ts +1 -0
  27. package/dist/srpc/packet-codec.test.js +75 -0
  28. package/dist/srpc/packet.d.ts +1 -1
  29. package/dist/srpc/packet.js +11 -1
  30. package/dist/srpc/server.js +19 -6
  31. package/dist/srpc/server.test.js +43 -0
  32. package/dist/srpc/stream.d.ts +4 -1
  33. package/dist/srpc/stream.js +62 -3
  34. package/dist/srpc/stream.test.js +110 -1
  35. package/dist/srpc/termination.d.ts +27 -0
  36. package/dist/srpc/termination.js +56 -0
  37. package/dist/srpc/termination.test.d.ts +1 -0
  38. package/dist/srpc/termination.test.js +24 -0
  39. package/dist/testdata/packet-codec-vectors.json +64 -0
  40. package/echo/Cargo.toml +8 -0
  41. package/echo/client-test.ts +124 -2
  42. package/echo/echo_e2e_test.cpp +542 -985
  43. package/echo/echo_pb2.py +40 -0
  44. package/echo/echo_pb2.pyi +13 -0
  45. package/echo/echo_srpc.pb.cpp +3 -5
  46. package/echo/echo_srpc.pb.go +6 -9
  47. package/echo/echo_srpc.pb.hpp +23 -3
  48. package/echo/echo_srpc.pb.rs +61 -26
  49. package/echo/echo_srpc.py +309 -0
  50. package/echo/echo_srpc.pyi +85 -0
  51. package/echo/half_close_test.rs +178 -0
  52. package/echo/integration_server.rs +14 -21
  53. package/echo/main.rs +28 -48
  54. package/go.mod +3 -3
  55. package/go.sum +6 -8
  56. package/integration/cross-language/cpp-client.cpp +446 -402
  57. package/integration/cross-language/cpp-pipe-server.cpp +139 -0
  58. package/integration/cross-language/cpp-pipe_test.go +114 -0
  59. package/integration/cross-language/cpp-server.cpp +222 -186
  60. package/integration/cross-language/go-client/main.go +79 -3
  61. package/integration/cross-language/python-client.py +146 -0
  62. package/integration/cross-language/python-server.py +140 -0
  63. package/integration/cross-language/run.bash +190 -65
  64. package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
  65. package/integration/cross-language/tcp-packet-stream.ts +121 -0
  66. package/integration/cross-language/ts-client.ts +62 -40
  67. package/integration/cross-language/ts-server.ts +1 -45
  68. package/mock/mock_pb2.py +38 -0
  69. package/mock/mock_pb2.pyi +11 -0
  70. package/mock/mock_srpc.pb.cpp +1 -1
  71. package/mock/mock_srpc.pb.hpp +1 -1
  72. package/mock/mock_srpc.pb.rs +10 -9
  73. package/mock/mock_srpc.py +71 -0
  74. package/mock/mock_srpc.pyi +27 -0
  75. package/package.json +19 -5
  76. package/srpc/CMakeLists.txt +3 -58
  77. package/srpc/README-cpp.md +69 -0
  78. package/srpc/__init__.py +0 -0
  79. package/srpc/accept.go +10 -4
  80. package/srpc/accept_test.go +25 -0
  81. package/srpc/build.rs +129 -34
  82. package/srpc/client-half-close_test.go +152 -0
  83. package/srpc/client-invoker.go +32 -42
  84. package/srpc/client-invoker_test.go +53 -0
  85. package/srpc/client-rpc.cpp +65 -80
  86. package/srpc/client-rpc.go +32 -33
  87. package/srpc/client-rpc.hpp +52 -38
  88. package/srpc/client-rpc.test.ts +67 -0
  89. package/srpc/client-rpc.ts +10 -0
  90. package/srpc/client-rpc_test.go +76 -0
  91. package/srpc/client.cpp +99 -87
  92. package/srpc/client.hpp +46 -35
  93. package/srpc/client.rs +78 -38
  94. package/srpc/client.ts +20 -4
  95. package/srpc/client_half_close_test.cc +331 -0
  96. package/srpc/client_invoker.rs +75 -0
  97. package/srpc/codec.rs +6 -0
  98. package/srpc/common-rpc.cpp +186 -146
  99. package/srpc/common-rpc.go +5 -4
  100. package/srpc/common-rpc.hpp +119 -89
  101. package/srpc/common-rpc.test.ts +2 -0
  102. package/srpc/common-rpc.ts +43 -8
  103. package/srpc/common-rpc_test.go +23 -0
  104. package/srpc/error.rs +19 -2
  105. package/srpc/errors.hpp +80 -50
  106. package/srpc/errors.ts +16 -0
  107. package/srpc/handler.hpp +9 -7
  108. package/srpc/handler.rs +7 -45
  109. package/srpc/index.ts +2 -0
  110. package/srpc/invoker.hpp +82 -55
  111. package/srpc/invoker.rs +14 -52
  112. package/srpc/lib.rs +2 -0
  113. package/srpc/message.hpp +17 -12
  114. package/srpc/msg-stream.go +1 -0
  115. package/srpc/msg-stream.hpp +108 -61
  116. package/srpc/mux.cpp +84 -84
  117. package/srpc/mux.hpp +48 -38
  118. package/srpc/mux.rs +4 -4
  119. package/srpc/net.go +12 -3
  120. package/srpc/packet-codec-vectors_test.go +195 -0
  121. package/srpc/packet-codec.test.ts +139 -0
  122. package/srpc/packet-rw.go +9 -2
  123. package/srpc/packet.cpp +60 -58
  124. package/srpc/packet.hpp +35 -20
  125. package/srpc/packet.rs +8 -27
  126. package/srpc/packet.ts +15 -2
  127. package/srpc/py.typed +0 -0
  128. package/srpc/rpc.rs +332 -115
  129. package/srpc/rpc_lifecycle_test.cc +333 -0
  130. package/srpc/rpcproto_pb2.py +40 -0
  131. package/srpc/rpcproto_pb2.pyi +40 -0
  132. package/srpc/server-cancel_test.go +39 -0
  133. package/srpc/server-rpc.cpp +83 -73
  134. package/srpc/server-rpc.go +21 -19
  135. package/srpc/server-rpc.hpp +56 -37
  136. package/srpc/server.rs +76 -131
  137. package/srpc/server.test.ts +50 -0
  138. package/srpc/server.ts +22 -6
  139. package/srpc/starpc.hpp +4 -2
  140. package/srpc/stream.go +4 -0
  141. package/srpc/stream.hpp +95 -34
  142. package/srpc/stream.rs +7 -2
  143. package/srpc/stream.test.ts +132 -1
  144. package/srpc/stream.ts +65 -9
  145. package/srpc/termination.test.ts +30 -0
  146. package/srpc/termination.ts +70 -0
  147. package/srpc/testing.rs +11 -66
  148. package/srpc/transport.rs +21 -71
  149. package/srpc/websocket.rs +106 -57
  150. package/srpc/writer.hpp +46 -32
  151. package/srpc/yamux.rs +82 -26
  152. package/srpc/tests/integration_test.rs +0 -495
package/srpc/transport.rs CHANGED
@@ -12,6 +12,7 @@ use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
12
12
  use tokio::sync::Mutex;
13
13
  use tokio_util::codec::{Encoder, FramedRead};
14
14
 
15
+ use crate::client::PacketReceiver;
15
16
  use crate::codec::PacketCodec;
16
17
  use crate::error::{Error, Result};
17
18
  use crate::proto::Packet;
@@ -68,65 +69,13 @@ impl<W: AsyncWrite + Send + Unpin + 'static> PacketWriter for TransportPacketWri
68
69
  }
69
70
  }
70
71
 
71
- /// Receiver for incoming packets from a transport.
72
- pub type PacketReceiver = tokio::sync::mpsc::Receiver<Packet>;
72
+ /// Default bounded request capacity for multiplexed transport admission.
73
+ #[cfg(feature = "yamux")]
74
+ pub(crate) const DEFAULT_CHANNEL_BUFFER: usize = 32;
73
75
 
74
- /// Sender for incoming packets to be processed.
75
- pub type PacketSender = tokio::sync::mpsc::Sender<Packet>;
76
-
77
- /// Default channel buffer size for packet channels.
78
- pub const DEFAULT_CHANNEL_BUFFER: usize = 32;
79
-
80
- /// Spawns a task that reads packets from a transport and sends them through a channel.
81
- ///
82
- /// This is a common pattern used by both client and server to handle incoming packets.
83
- /// The task will run until the transport is closed or an error occurs.
84
- ///
85
- /// # Arguments
86
- /// * `reader` - The async reader to read packets from
87
- /// * `sender` - The channel sender to forward packets to
88
- ///
89
- /// # Returns
90
- /// A `JoinHandle` for the spawned task.
91
- pub fn spawn_packet_reader<R>(
92
- reader: R,
93
- sender: PacketSender,
94
- ) -> tokio::task::JoinHandle<()>
95
- where
96
- R: AsyncRead + Send + Unpin + 'static,
97
- {
98
- tokio::spawn(async move {
99
- let mut framed = FramedRead::new(reader, PacketCodec::new());
100
- while let Some(result) = framed.next().await {
101
- match result {
102
- Ok(packet) => {
103
- if sender.send(packet).await.is_err() {
104
- // Receiver dropped, stop reading
105
- break;
106
- }
107
- }
108
- Err(_) => {
109
- // Read error, stop reading
110
- break;
111
- }
112
- }
113
- }
114
- })
115
- }
116
-
117
- /// Creates a packet writer and receiver from a split transport.
118
- ///
119
- /// This is a convenience function that:
120
- /// 1. Creates a `TransportPacketWriter` from the write half
121
- /// 2. Spawns a packet reader task for the read half
122
- /// 3. Returns the writer and receiver channel
123
- ///
124
- /// # Arguments
125
- /// * `read_half` - The read half of the transport
126
- /// * `write_half` - The write half of the transport
127
- ///
128
- /// # Returns
129
- /// A tuple of (packet writer, packet receiver).
76
+ /// Creates a packet writer and an owned incoming packet stream from a split transport.
77
+ /// The receiver reads on demand; dropping it immediately releases the read half.
78
+ /// No background task or additional packet queue is created.
130
79
  pub fn create_packet_channel<R, W>(
131
80
  read_half: R,
132
81
  write_half: W,
@@ -135,10 +84,18 @@ where
135
84
  R: AsyncRead + Send + Unpin + 'static,
136
85
  W: AsyncWrite + Send + Unpin + 'static,
137
86
  {
87
+ // Framing belongs to the returned receive capability for its complete lifetime.
88
+ let reader = FramedRead::new(read_half, PacketCodec::new());
89
+ let packets = futures::stream::unfold(reader, |mut reader| async move {
90
+ match reader.next().await {
91
+ Some(Ok(packet)) => Some((packet, reader)),
92
+ _ => None,
93
+ }
94
+ });
95
+
96
+ // The peer-facing writer retains its own half of this same transport.
138
97
  let writer: Arc<dyn PacketWriter> = Arc::new(TransportPacketWriter::new(write_half));
139
- let (tx, rx) = tokio::sync::mpsc::channel(DEFAULT_CHANNEL_BUFFER);
140
- spawn_packet_reader(read_half, tx);
141
- (writer, rx)
98
+ (writer, Box::pin(packets))
142
99
  }
143
100
 
144
101
  /// Encodes optional data for protobuf messages.
@@ -147,9 +104,6 @@ where
147
104
  /// - `None` -> empty data, `data_is_zero = false`
148
105
  /// - `Some(empty)` -> empty data, `data_is_zero = true`
149
106
  /// - `Some(data)` -> data bytes, `data_is_zero = false`
150
- ///
151
- /// # Returns
152
- /// A tuple of (data bytes, data_is_zero flag).
153
107
  pub fn encode_optional_data(data: Option<Bytes>) -> (Vec<u8>, bool) {
154
108
  match data {
155
109
  Some(d) if d.is_empty() => (vec![], true),
@@ -158,13 +112,9 @@ pub fn encode_optional_data(data: Option<Bytes>) -> (Vec<u8>, bool) {
158
112
  }
159
113
  }
160
114
 
161
- /// Decodes optional data from protobuf messages.
162
- ///
163
- /// Inverse of `encode_optional_data`.
164
- ///
165
- /// # Returns
166
- /// `Some(Bytes)` if data was present (including empty data with `data_is_zero`),
167
- /// `None` if no data was included.
115
+ /// Decodes optional data from protobuf messages, inverting
116
+ /// `encode_optional_data`: `Some(Bytes)` when data was present, including
117
+ /// empty data with `data_is_zero` set.
168
118
  pub fn decode_optional_data(data: Vec<u8>, data_is_zero: bool) -> Option<Bytes> {
169
119
  if !data.is_empty() || data_is_zero {
170
120
  Some(Bytes::from(data))
package/srpc/websocket.rs CHANGED
@@ -1,70 +1,119 @@
1
- //! WebSocket transport adapters for starpc.
1
+ //! Owned WebSocket byte transport for StarPC.
2
2
 
3
- use bytes::Bytes;
4
- use futures::{SinkExt, StreamExt};
5
- use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream};
6
- use tokio_tungstenite::{tungstenite::Message, WebSocketStream};
3
+ use bytes::{Buf, Bytes};
4
+ use futures::{Sink, Stream};
5
+ use std::io;
6
+ use std::pin::Pin;
7
+ use std::task::{ready, Context, Poll};
8
+ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
9
+ use tokio_tungstenite::{tungstenite, tungstenite::Message, WebSocketStream};
7
10
 
8
- /// WEBSOCKET_BYTE_STREAM_BUFFER is the duplex buffer used by WebSocket byte adapters.
9
- pub const WEBSOCKET_BYTE_STREAM_BUFFER: usize = 64 * 1024;
10
-
11
- /// WEBSOCKET_MESSAGE_BUFFER is the maximum byte chunk sent in one WebSocket message.
11
+ /// Maximum byte chunk sent in one WebSocket message.
12
12
  pub const WEBSOCKET_MESSAGE_BUFFER: usize = 16 * 1024;
13
13
 
14
- /// websocket_byte_stream adapts a binary WebSocket stream to Tokio byte I/O.
15
- ///
16
- /// The adapter treats incoming binary WebSocket messages as consecutive bytes
17
- /// and writes outbound byte chunks as binary WebSocket messages. Control and
18
- /// text messages are ignored except that close terminates the byte stream.
19
- pub fn websocket_byte_stream<S>(socket: WebSocketStream<S>) -> DuplexStream
20
- where
21
- S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
22
- {
23
- let (mut socket_writer, mut socket_reader) = socket.split();
24
- let (stream, peer) = tokio::io::duplex(WEBSOCKET_BYTE_STREAM_BUFFER);
25
- let (mut peer_reader, mut peer_writer) = tokio::io::split(peer);
14
+ /// Adapts binary WebSocket messages to byte I/O without background tasks.
15
+ /// Incoming binary messages form one byte stream. Text and control messages
16
+ /// carry no application bytes; a close message ends input. Dropping this
17
+ /// capability drops its socket, including when input is idle.
18
+ pub fn websocket_byte_stream<S>(socket: WebSocketStream<S>) -> WebSocketByteStream<S> {
19
+ WebSocketByteStream {
20
+ socket,
21
+ buffered: Bytes::new(),
22
+ input: Input::Open,
23
+ }
24
+ }
26
25
 
27
- tokio::spawn(async move {
28
- while let Some(result) = socket_reader.next().await {
29
- match result {
30
- Ok(message) if message.is_binary() => {
31
- let data = message.into_data();
32
- if peer_writer.write_all(data.as_ref()).await.is_err() {
33
- break;
34
- }
35
- if peer_writer.flush().await.is_err() {
36
- break;
37
- }
38
- }
39
- Ok(message) if message.is_close() => break,
40
- Ok(_) => {}
41
- Err(_) => break,
42
- }
26
+ /// Byte I/O that owns its WebSocket and the unread part of one incoming message.
27
+ pub struct WebSocketByteStream<S> {
28
+ socket: WebSocketStream<S>,
29
+ buffered: Bytes,
30
+ input: Input,
31
+ }
32
+
33
+ enum Input {
34
+ Open,
35
+ Closing,
36
+ Ended,
37
+ }
38
+
39
+ impl<S: AsyncRead + AsyncWrite + Unpin> AsyncRead for WebSocketByteStream<S> {
40
+ fn poll_read(
41
+ self: Pin<&mut Self>,
42
+ cx: &mut Context<'_>,
43
+ output: &mut ReadBuf<'_>,
44
+ ) -> Poll<io::Result<()>> {
45
+ let this = self.get_mut();
46
+ if output.remaining() == 0 {
47
+ return Poll::Ready(Ok(()));
43
48
  }
44
- let _ = peer_writer.shutdown().await;
45
- });
46
49
 
47
- tokio::spawn(async move {
48
- let mut buf = vec![0; WEBSOCKET_MESSAGE_BUFFER];
50
+ // Retain each message until all its bytes have reached the reader.
49
51
  loop {
50
- match peer_reader.read(&mut buf).await {
51
- Ok(0) => {
52
- let _ = socket_writer.close().await;
53
- break;
54
- }
55
- Ok(n) => {
56
- let message = Message::Binary(Bytes::copy_from_slice(&buf[..n]));
57
- if socket_writer.send(message).await.is_err() {
58
- break;
59
- }
60
- }
61
- Err(_) => {
62
- let _ = socket_writer.close().await;
63
- break;
52
+ if !this.buffered.is_empty() {
53
+ let count = output.remaining().min(this.buffered.len());
54
+ output.put_slice(&this.buffered[..count]);
55
+ this.buffered.advance(count);
56
+ return Poll::Ready(Ok(()));
57
+ }
58
+ match this.input {
59
+ Input::Closing => {
60
+ // Flush Tungstenite's close acknowledgment before reporting EOF.
61
+ ready!(Pin::new(&mut this.socket).poll_flush(cx)).map_err(socket_error)?;
62
+ this.input = Input::Ended;
63
+ return Poll::Ready(Ok(()));
64
64
  }
65
+ Input::Ended => return Poll::Ready(Ok(())),
66
+ Input::Open => {}
67
+ }
68
+
69
+ // Tungstenite owns WebSocket control handling and pending control writes.
70
+ match ready!(Pin::new(&mut this.socket).poll_next(cx)) {
71
+ Some(Ok(Message::Binary(data))) => this.buffered = data,
72
+ Some(Ok(Message::Close(_))) => this.input = Input::Closing,
73
+ None => this.input = Input::Ended,
74
+ Some(Ok(_)) => {}
75
+ Some(Err(error)) => return Poll::Ready(Err(socket_error(error))),
65
76
  }
66
77
  }
67
- });
78
+ }
79
+ }
80
+
81
+ impl<S: AsyncRead + AsyncWrite + Unpin> AsyncWrite for WebSocketByteStream<S> {
82
+ fn poll_write(
83
+ self: Pin<&mut Self>,
84
+ cx: &mut Context<'_>,
85
+ input: &[u8],
86
+ ) -> Poll<io::Result<usize>> {
87
+ if input.is_empty() {
88
+ return Poll::Ready(Ok(0));
89
+ }
90
+
91
+ // Respect socket backpressure before accepting another bounded message.
92
+ let socket = &mut self.get_mut().socket;
93
+ ready!(Pin::new(&mut *socket).poll_ready(cx)).map_err(socket_error)?;
94
+ let count = input.len().min(WEBSOCKET_MESSAGE_BUFFER);
95
+ Pin::new(socket)
96
+ .start_send(Message::Binary(Bytes::copy_from_slice(&input[..count])))
97
+ .map_err(socket_error)?;
98
+ Poll::Ready(Ok(count))
99
+ }
100
+
101
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
102
+ Pin::new(&mut self.get_mut().socket)
103
+ .poll_flush(cx)
104
+ .map_err(socket_error)
105
+ }
106
+
107
+ fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
108
+ Pin::new(&mut self.get_mut().socket)
109
+ .poll_close(cx)
110
+ .map_err(socket_error)
111
+ }
112
+ }
68
113
 
69
- stream
114
+ fn socket_error(error: tungstenite::Error) -> io::Error {
115
+ match error {
116
+ tungstenite::Error::Io(error) => error,
117
+ other => io::Error::other(other),
118
+ }
70
119
  }
package/srpc/writer.hpp CHANGED
@@ -1,60 +1,74 @@
1
1
  #pragma once
2
2
 
3
+ #include "errors.hpp"
4
+
3
5
  #include <functional>
4
6
  #include <memory>
5
7
 
6
- #include "errors.hpp"
7
-
8
8
  namespace srpc {
9
9
  class Packet;
10
10
  }
11
11
 
12
12
  namespace starpc {
13
13
 
14
- // PacketWriter is the interface used to write messages to a PacketStream.
15
- // Matches Go interface in writer.go
14
+ /*
15
+ * PacketWriter writes one packet to the transport. Close may run from a
16
+ * receive callback or concurrently with a write, and must interrupt blocked
17
+ * I/O without joining callbacks. Destruction joins any transport receive
18
+ * threads.
19
+ */
16
20
  class PacketWriter {
17
21
  public:
18
- virtual ~PacketWriter() = default;
22
+ virtual ~PacketWriter() = default;
19
23
 
20
- // WritePacket writes a packet to the remote.
21
- virtual Error WritePacket(const srpc::Packet &pkt) = 0;
24
+ /* WritePacket serializes pkt onto the transport. */
25
+ virtual Error WritePacket(const srpc::Packet &pkt) = 0;
22
26
 
23
- // Close closes the writer.
24
- virtual Error Close() = 0;
27
+ /* Close interrupts the transport. Repeated calls are safe. */
28
+ virtual Error Close() = 0;
25
29
  };
26
30
 
27
- // PacketWriterWithClose wraps a PacketWriter with an additional close function.
28
- // Matches packetWriterWithClose in writer.go
31
+ /*
32
+ * PacketWriterWithClose forwards writes to an owned writer and runs an extra
33
+ * callback on the first Close. Matches packetWriterWithClose in writer.go.
34
+ */
29
35
  class PacketWriterWithClose : public PacketWriter {
30
36
  public:
31
- PacketWriterWithClose(std::unique_ptr<PacketWriter> inner,
32
- std::function<Error()> close_fn)
33
- : inner_(std::move(inner)), close_fn_(std::move(close_fn)) {}
34
-
35
- Error WritePacket(const srpc::Packet &pkt) override {
36
- return inner_->WritePacket(pkt);
37
- }
38
-
39
- Error Close() override {
40
- Error err = inner_->Close();
41
- Error err2 = close_fn_();
42
- if (err != Error::OK)
43
- return err;
44
- return err2;
45
- }
37
+ /* close_fn runs once, after inner->Close. */
38
+ PacketWriterWithClose(std::unique_ptr<PacketWriter> inner,
39
+ std::function<Error()> close_fn)
40
+ : inner(std::move(inner)),
41
+ close_fn(std::move(close_fn))
42
+ {
43
+ }
44
+
45
+ Error WritePacket(const srpc::Packet &pkt) override
46
+ {
47
+ return inner->WritePacket(pkt);
48
+ }
49
+
50
+ Error Close() override
51
+ {
52
+ /* Report the inner close even when the callback also fails. */
53
+ Error err = inner->Close();
54
+ Error err2 = close_fn();
55
+ if (err != Error::OK)
56
+ return err;
57
+ return err2;
58
+ }
46
59
 
47
60
  private:
48
- std::unique_ptr<PacketWriter> inner_;
49
- std::function<Error()> close_fn_;
61
+ std::unique_ptr<PacketWriter> inner;
62
+ std::function<Error()> close_fn;
50
63
  };
51
64
 
52
- // NewPacketWriterWithClose wraps a PacketWriter with a close function.
65
+ /* NewPacketWriterWithClose wraps prw so close_fn runs on the first Close. */
53
66
  inline std::unique_ptr<PacketWriter>
54
67
  NewPacketWriterWithClose(std::unique_ptr<PacketWriter> prw,
55
- std::function<Error()> close_fn) {
56
- return std::make_unique<PacketWriterWithClose>(std::move(prw),
57
- std::move(close_fn));
68
+ std::function<Error()> close_fn)
69
+ {
70
+ return std::make_unique<PacketWriterWithClose>(std::move(prw),
71
+ std::move(close_fn));
58
72
  }
59
73
 
60
74
  } // namespace starpc
package/srpc/yamux.rs CHANGED
@@ -1,13 +1,15 @@
1
1
  //! Yamux transport adapters for starpc.
2
2
 
3
3
  use async_trait::async_trait;
4
- use futures::future::poll_fn;
4
+ use futures::{future::poll_fn, stream::FuturesUnordered, StreamExt};
5
5
  use std::io;
6
6
  use std::pin::Pin;
7
7
  use std::sync::Arc;
8
8
  use tokio::io::{AsyncRead, AsyncWrite};
9
- use tokio::sync::{mpsc, oneshot};
9
+ use tokio::sync::{mpsc, oneshot, Mutex};
10
+ use tokio::task::JoinHandle;
10
11
  use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
12
+ use tokio_util::sync::CancellationToken;
11
13
 
12
14
  use crate::client::{OpenStream, PacketReceiver};
13
15
  use crate::error::{Error, Result};
@@ -19,14 +21,30 @@ use crate::transport::{create_packet_channel, DEFAULT_CHANNEL_BUFFER};
19
21
  type OpenResult = std::result::Result<::yamux::Stream, ::yamux::ConnectionError>;
20
22
  type OpenRequest = oneshot::Sender<OpenResult>;
21
23
 
22
- /// YamuxStreamOpener opens Starpc packet streams over one yamux connection.
24
+ /// Opens StarPC packet streams over one owned yamux connection.
25
+ /// Clones share the connection. Dropping the last opener cancels its driver;
26
+ /// explicit close cancels and joins the driver before returning.
23
27
  #[derive(Clone)]
24
28
  pub struct YamuxStreamOpener {
29
+ driver: Arc<ClientDriver>,
30
+ }
31
+
32
+ struct ClientDriver {
25
33
  requests: mpsc::Sender<OpenRequest>,
34
+ task: Mutex<Option<JoinHandle<()>>>,
35
+ closed: CancellationToken,
36
+ }
37
+
38
+ impl Drop for ClientDriver {
39
+ fn drop(&mut self) {
40
+ if let Some(task) = self.task.get_mut().take() {
41
+ task.abort();
42
+ }
43
+ }
26
44
  }
27
45
 
28
46
  impl YamuxStreamOpener {
29
- /// client creates a client-mode yamux opener over a Tokio transport.
47
+ /// Creates a client-mode yamux opener over a Tokio transport.
30
48
  pub fn client<T>(transport: T) -> Self
31
49
  where
32
50
  T: AsyncRead + AsyncWrite + Send + Unpin + 'static,
@@ -34,17 +52,41 @@ impl YamuxStreamOpener {
34
52
  Self::client_with_config(transport, ::yamux::Config::default())
35
53
  }
36
54
 
37
- /// client_with_config creates a client-mode yamux opener with a custom config.
55
+ /// Creates a client-mode yamux opener with a custom configuration.
38
56
  pub fn client_with_config<T>(transport: T, config: ::yamux::Config) -> Self
39
57
  where
40
58
  T: AsyncRead + AsyncWrite + Send + Unpin + 'static,
41
59
  {
42
60
  let (requests, request_rx) = mpsc::channel(DEFAULT_CHANNEL_BUFFER);
43
- spawn_client_driver(transport.compat(), config, request_rx);
44
- Self { requests }
61
+ let closed = CancellationToken::new();
62
+ let task = spawn_client_driver(transport.compat(), config, request_rx, closed.clone());
63
+ Self {
64
+ driver: Arc::new(ClientDriver {
65
+ requests,
66
+ task: Mutex::new(Some(task)),
67
+ closed,
68
+ }),
69
+ }
45
70
  }
46
71
 
47
- /// client_websocket creates a client-mode yamux opener over a WebSocket.
72
+ /// Cancels the shared connection and joins its driver.
73
+ /// Concurrent callers wait for the same completed teardown.
74
+ pub async fn close(&self) {
75
+ let mut task = self.driver.task.lock().await;
76
+ if let Some(task) = task.as_mut() {
77
+ task.abort();
78
+ let _ = task.await;
79
+ }
80
+ task.take();
81
+ }
82
+
83
+ /// Waits for the connection driver to end, including peer closure and protocol failure.
84
+ /// Observers do not prevent a concurrent explicit close from joining the driver.
85
+ pub async fn closed(&self) {
86
+ self.driver.closed.cancelled().await;
87
+ }
88
+
89
+ /// Creates a client-mode yamux opener over a WebSocket.
48
90
  #[cfg(feature = "websocket")]
49
91
  pub fn client_websocket<S>(socket: tokio_tungstenite::WebSocketStream<S>) -> Self
50
92
  where
@@ -53,7 +95,7 @@ impl YamuxStreamOpener {
53
95
  Self::client(crate::websocket::websocket_byte_stream(socket))
54
96
  }
55
97
 
56
- /// client_websocket_with_config creates a client-mode yamux WebSocket opener with a custom config.
98
+ /// Creates a client-mode yamux WebSocket opener with a custom configuration.
57
99
  #[cfg(feature = "websocket")]
58
100
  pub fn client_websocket_with_config<S>(
59
101
  socket: tokio_tungstenite::WebSocketStream<S>,
@@ -70,7 +112,8 @@ impl YamuxStreamOpener {
70
112
  impl OpenStream for YamuxStreamOpener {
71
113
  async fn open_stream(&self) -> Result<(Arc<dyn PacketWriter>, PacketReceiver)> {
72
114
  let (tx, rx) = oneshot::channel();
73
- self.requests
115
+ self.driver
116
+ .requests
74
117
  .send(tx)
75
118
  .await
76
119
  .map_err(|_| Error::StreamClosed)?;
@@ -85,7 +128,7 @@ impl OpenStream for YamuxStreamOpener {
85
128
  }
86
129
  }
87
130
 
88
- /// handle_server_connection accepts yamux streams and serves each as Starpc.
131
+ /// Serves yamux substreams within this connection's owned lifetime.
89
132
  pub async fn handle_server_connection<I, T>(
90
133
  server: &Server<I>,
91
134
  transport: T,
@@ -98,18 +141,23 @@ where
98
141
  let mut connection =
99
142
  ::yamux::Connection::new(transport.compat(), config, ::yamux::Mode::Server);
100
143
 
144
+ // Keep accepted calls within the connection's lifetime. Losing the connection
145
+ // or dropping this future drops every call and its cancellation guard.
146
+ let mut calls = FuturesUnordered::new();
101
147
  loop {
102
- match poll_fn(|cx| connection.poll_next_inbound(cx)).await {
103
- Some(Ok(stream)) => {
104
- let server = server.clone_for_spawn();
105
- tokio::spawn(async move {
106
- if let Err(err) = server.handle_stream(stream.compat()).await {
107
- server.report_error(err);
108
- }
109
- });
148
+ tokio::select! {
149
+ incoming = poll_fn(|cx| connection.poll_next_inbound(cx)) => {
150
+ match incoming {
151
+ Some(Ok(stream)) => calls.push(server.handle_stream(stream.compat())),
152
+ Some(Err(err)) => return Err(connection_error(err)),
153
+ None => return Ok(()),
154
+ }
155
+ }
156
+ Some(result) = calls.next(), if !calls.is_empty() => {
157
+ if let Err(err) = result {
158
+ server.report_error(err);
159
+ }
110
160
  }
111
- Some(Err(err)) => return Err(connection_error(err)),
112
- None => return Ok(()),
113
161
  }
114
162
  }
115
163
  }
@@ -126,21 +174,29 @@ fn spawn_client_driver<T>(
126
174
  transport: T,
127
175
  config: ::yamux::Config,
128
176
  mut requests: mpsc::Receiver<OpenRequest>,
129
- ) -> tokio::task::JoinHandle<()>
177
+ closed: CancellationToken,
178
+ ) -> JoinHandle<()>
130
179
  where
131
180
  T: futures::io::AsyncRead + futures::io::AsyncWrite + Send + Unpin + 'static,
132
181
  {
182
+ // Construct the guard before spawning so cancellation before the first poll also notifies.
183
+ let ended = closed.drop_guard();
133
184
  tokio::spawn(async move {
185
+ let _ended = ended;
134
186
  let mut connection = ::yamux::Connection::new(transport, config, ::yamux::Mode::Client);
135
187
  let mut pending = None;
136
- let mut requests_closed = false;
137
188
 
138
189
  loop {
139
190
  let event = poll_fn(|cx| {
140
- if pending.is_none() && !requests_closed {
191
+ if pending.is_none() {
141
192
  match Pin::new(&mut requests).poll_recv(cx) {
142
193
  std::task::Poll::Ready(Some(request)) => pending = Some(request),
143
- std::task::Poll::Ready(None) => requests_closed = true,
194
+ std::task::Poll::Ready(None) => {
195
+ return std::task::Poll::Ready(DriverEvent::Closed {
196
+ pending: None,
197
+ err: None,
198
+ });
199
+ }
144
200
  std::task::Poll::Pending => {}
145
201
  }
146
202
  }
@@ -193,6 +249,6 @@ where
193
249
  fn connection_error(err: ::yamux::ConnectionError) -> Error {
194
250
  match err {
195
251
  ::yamux::ConnectionError::Io(err) => Error::Io(err),
196
- other => Error::Io(io::Error::new(io::ErrorKind::Other, other.to_string())),
252
+ other => Error::Io(io::Error::other(other.to_string())),
197
253
  }
198
254
  }