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.
Files changed (91) hide show
  1. package/README.md +37 -8
  2. package/dist/srpc/client-rpc.d.ts +1 -0
  3. package/dist/srpc/client-rpc.js +9 -0
  4. package/dist/srpc/client-rpc.test.d.ts +1 -0
  5. package/dist/srpc/client-rpc.test.js +50 -0
  6. package/dist/srpc/common-rpc.d.ts +5 -0
  7. package/dist/srpc/common-rpc.js +37 -8
  8. package/dist/srpc/errors.d.ts +2 -0
  9. package/dist/srpc/errors.js +14 -0
  10. package/dist/srpc/index.d.ts +1 -1
  11. package/dist/srpc/index.js +1 -1
  12. package/echo/Cargo.toml +8 -0
  13. package/echo/echo_e2e_test.cpp +542 -985
  14. package/echo/echo_srpc.pb.cpp +3 -5
  15. package/echo/echo_srpc.pb.go +6 -9
  16. package/echo/echo_srpc.pb.hpp +23 -3
  17. package/echo/echo_srpc.pb.rs +61 -26
  18. package/echo/echo_srpc.py +4 -1
  19. package/echo/half_close_test.rs +178 -0
  20. package/echo/integration_server.rs +14 -21
  21. package/echo/main.rs +28 -48
  22. package/go.mod +1 -1
  23. package/go.sum +2 -18
  24. package/integration/cross-language/cpp-client.cpp +446 -402
  25. package/integration/cross-language/cpp-pipe-server.cpp +139 -0
  26. package/integration/cross-language/cpp-pipe_test.go +114 -0
  27. package/integration/cross-language/cpp-server.cpp +222 -186
  28. package/mock/mock_srpc.pb.cpp +1 -1
  29. package/mock/mock_srpc.pb.hpp +1 -1
  30. package/mock/mock_srpc.pb.rs +10 -9
  31. package/package.json +1 -1
  32. package/srpc/CMakeLists.txt +3 -58
  33. package/srpc/README-cpp.md +69 -0
  34. package/srpc/accept.go +10 -4
  35. package/srpc/accept_test.go +25 -0
  36. package/srpc/build.rs +129 -34
  37. package/srpc/client-half-close_test.go +152 -0
  38. package/srpc/client-invoker.go +32 -42
  39. package/srpc/client-invoker_test.go +53 -0
  40. package/srpc/client-rpc.cpp +65 -80
  41. package/srpc/client-rpc.go +32 -33
  42. package/srpc/client-rpc.hpp +52 -38
  43. package/srpc/client-rpc.test.ts +67 -0
  44. package/srpc/client-rpc.ts +10 -0
  45. package/srpc/client-rpc_test.go +76 -0
  46. package/srpc/client.cpp +99 -87
  47. package/srpc/client.hpp +46 -35
  48. package/srpc/client.rs +78 -38
  49. package/srpc/client_half_close_test.cc +331 -0
  50. package/srpc/client_invoker.rs +75 -0
  51. package/srpc/common-rpc.cpp +186 -146
  52. package/srpc/common-rpc.go +5 -4
  53. package/srpc/common-rpc.hpp +119 -89
  54. package/srpc/common-rpc.ts +43 -8
  55. package/srpc/common-rpc_test.go +23 -0
  56. package/srpc/error.rs +19 -2
  57. package/srpc/errors.hpp +80 -50
  58. package/srpc/errors.ts +16 -0
  59. package/srpc/handler.hpp +9 -7
  60. package/srpc/handler.rs +7 -45
  61. package/srpc/index.ts +2 -0
  62. package/srpc/invoker.hpp +82 -55
  63. package/srpc/invoker.rs +14 -52
  64. package/srpc/lib.rs +2 -0
  65. package/srpc/message.hpp +17 -12
  66. package/srpc/msg-stream.go +1 -0
  67. package/srpc/msg-stream.hpp +108 -61
  68. package/srpc/mux.cpp +84 -84
  69. package/srpc/mux.hpp +48 -38
  70. package/srpc/mux.rs +4 -4
  71. package/srpc/net.go +12 -3
  72. package/srpc/packet.cpp +60 -58
  73. package/srpc/packet.hpp +35 -20
  74. package/srpc/packet.rs +8 -27
  75. package/srpc/rpc.rs +332 -115
  76. package/srpc/rpc_lifecycle_test.cc +333 -0
  77. package/srpc/server-cancel_test.go +39 -0
  78. package/srpc/server-rpc.cpp +83 -73
  79. package/srpc/server-rpc.go +21 -19
  80. package/srpc/server-rpc.hpp +56 -37
  81. package/srpc/server.rs +76 -131
  82. package/srpc/starpc.hpp +4 -2
  83. package/srpc/stream.go +4 -0
  84. package/srpc/stream.hpp +95 -34
  85. package/srpc/stream.rs +7 -2
  86. package/srpc/testing.rs +11 -66
  87. package/srpc/transport.rs +21 -71
  88. package/srpc/websocket.rs +106 -57
  89. package/srpc/writer.hpp +46 -32
  90. package/srpc/yamux.rs +82 -26
  91. package/srpc/tests/integration_test.rs +0 -495
@@ -1,14 +1,14 @@
1
1
  #pragma once
2
2
 
3
- #include <string>
4
- #include <thread>
5
-
6
3
  #include "common-rpc.hpp"
7
4
  #include "errors.hpp"
8
5
  #include "invoker.hpp"
9
6
  #include "msg-stream.hpp"
10
7
  #include "writer.hpp"
11
8
 
9
+ #include <string>
10
+ #include <thread>
11
+
12
12
  namespace srpc {
13
13
  class Packet;
14
14
  class CallStart;
@@ -16,48 +16,67 @@ class CallStart;
16
16
 
17
17
  namespace starpc {
18
18
 
19
- // ServerRPC represents the server side of an on-going RPC call message stream.
20
- // Matches Go ServerRPC struct in server-rpc.go
19
+ /*
20
+ * ServerRPC is the remote's side of one call: it runs the handler the
21
+ * Invoker selects and cancels and joins it on destruction. The Invoker and
22
+ * PacketWriter must outlive it. HandlePacket calls are serialized by the
23
+ * transport; handlers observe cancellation through Stream::StopToken.
24
+ */
21
25
  class ServerRPC : public CommonRPC, public MsgStreamRw {
22
26
  public:
23
- // Constructor matching NewServerRPC in Go
24
- ServerRPC(Invoker *invoker, PacketWriter *writer);
25
- ~ServerRPC() override;
26
-
27
- // HandlePacketData handles an incoming unparsed message packet.
28
- // Matches Go HandlePacketData in server-rpc.go
29
- Error HandlePacketData(const std::string &data);
30
-
31
- // HandlePacket handles an incoming parsed message packet.
32
- // Matches Go HandlePacket in server-rpc.go
33
- Error HandlePacket(const srpc::Packet &msg);
34
-
35
- // HandleCallStart handles the call start packet.
36
- // Matches Go HandleCallStart in server-rpc.go
37
- Error HandleCallStart(const srpc::CallStart &pkt);
38
-
39
- // MsgStreamRw interface implementation
40
- Error ReadOne(std::string *out) override { return CommonRPC::ReadOne(out); }
41
- Error WriteCallData(const std::string &data, bool data_is_zero, bool complete,
42
- Error err) override {
43
- return CommonRPC::WriteCallData(data, data_is_zero, complete, err);
44
- }
45
- Error WriteCallCancel() override { return CommonRPC::WriteCallCancel(); }
27
+ /* ServerRPC serves one call through invoker, writing via writer. */
28
+ ServerRPC(Invoker *invoker, PacketWriter *writer);
29
+ ~ServerRPC() override;
30
+
31
+ /* HandlePacketData parses one serialized packet and handles it. */
32
+ Error HandlePacketData(const std::string &data);
33
+
34
+ /* HandlePacket validates and dispatches one parsed packet. */
35
+ Error HandlePacket(const srpc::Packet &msg);
36
+
37
+ /*
38
+ * HandleCallStart accepts the call's first packet and runs the
39
+ * handler on its own thread.
40
+ */
41
+ Error HandleCallStart(const srpc::CallStart &pkt);
42
+
43
+ /* MsgStreamRw over the shared call state. */
44
+ Error ReadOne(std::string *out) override
45
+ {
46
+ return CommonRPC::ReadOne(out);
47
+ }
48
+ Error WriteCallData(const std::string &data, bool data_is_zero,
49
+ bool complete, Error err) override
50
+ {
51
+ return CommonRPC::WriteCallData(data, data_is_zero, complete,
52
+ err);
53
+ }
54
+ Error WriteCallCancel() override
55
+ {
56
+ return CommonRPC::WriteCallCancel();
57
+ }
58
+ std::string RemoteErrorMessage() const override
59
+ {
60
+ return CommonRPC::RemoteErrorMessage();
61
+ }
46
62
 
47
63
  private:
48
- // InvokeRPC invokes the RPC after CallStart is received.
49
- // Matches Go invokeRPC in server-rpc.go
50
- void InvokeRPC(const std::string &service_id, const std::string &method_id);
64
+ /* InvokeRPC runs the handler and publishes its verdict. */
65
+ void InvokeRPC(const std::string &service_id,
66
+ const std::string &method_id);
67
+
68
+ /* invoker is borrowed; the caller owns it. */
69
+ Invoker *invoker;
51
70
 
52
- Invoker *invoker_;
53
- std::thread invoke_thread_;
71
+ /* invoke_thread is last so it stops before the state it reads. */
72
+ std::jthread invoke_thread;
54
73
  };
55
74
 
56
- // NewServerRPC constructs a new ServerRPC session.
57
- // Matches Go NewServerRPC function in server-rpc.go
75
+ /* NewServerRPC constructs a ServerRPC for one call. */
58
76
  inline std::unique_ptr<ServerRPC> NewServerRPC(Invoker *invoker,
59
- PacketWriter *writer) {
60
- return std::make_unique<ServerRPC>(invoker, writer);
77
+ PacketWriter *writer)
78
+ {
79
+ return std::make_unique<ServerRPC>(invoker, writer);
61
80
  }
62
81
 
63
82
  } // namespace starpc
package/srpc/server.rs CHANGED
@@ -4,8 +4,7 @@
4
4
  //! The server supports all streaming patterns: unary, client streaming,
5
5
  //! server streaming, and bidirectional streaming.
6
6
 
7
- use bytes::Bytes;
8
- use futures::StreamExt;
7
+ use futures::{stream::FuturesUnordered, StreamExt};
9
8
  use std::sync::Arc;
10
9
  use std::time::Duration;
11
10
  use tokio::io::{AsyncRead, AsyncWrite};
@@ -128,86 +127,48 @@ impl<I: Invoker + 'static> Server<I> {
128
127
  // Create framed reader.
129
128
  let mut framed = FramedRead::new(read_half, PacketCodec::new());
130
129
 
131
- // Wait for the first packet (CallStart).
130
+ // Wait for the first packet and take its CallStart body.
132
131
  let first_packet = framed.next().await;
133
- let call_start = match first_packet {
134
- Some(Ok(packet)) => {
135
- // Validate the packet
136
- packet.validate()?;
137
-
138
- match packet.body {
139
- Some(Body::CallStart(cs)) => cs,
140
- _ => return Err(Error::ExpectedCallStart),
141
- }
142
- }
143
- Some(Err(e)) => return Err(e),
144
- None => return Err(Error::StreamClosed),
132
+ let Some(Ok(packet)) = first_packet else {
133
+ // A decode error ends the stream; a closed transport has no error.
134
+ return first_packet.map_or(Err(Error::StreamClosed), |res| res.map(|_| ()));
135
+ };
136
+ packet.validate()?;
137
+ let Some(Body::CallStart(call_start)) = packet.body else {
138
+ return Err(Error::ExpectedCallStart);
145
139
  };
146
140
 
147
- // Validate service and method IDs (already done by packet.validate(),
148
- // but double-check for clarity).
149
- if call_start.rpc_service.is_empty() {
150
- return Err(Error::EmptyServiceId);
151
- }
152
- if call_start.rpc_method.is_empty() {
153
- return Err(Error::EmptyMethodId);
154
- }
155
-
156
- let service_id = call_start.rpc_service.clone();
157
- let method_id = call_start.rpc_method.clone();
158
-
159
- // Create the server RPC.
160
- let ctx = Context::new();
161
- let rpc = Arc::new(ServerRpc::from_call_start(ctx, call_start, writer));
141
+ // Retain the call's transport context until this borrowing operation ends.
142
+ let rpc = Arc::new(ServerRpc::from_call_start(
143
+ Context::new(),
144
+ call_start,
145
+ writer,
146
+ ));
147
+ let _cancel = rpc.context().cancel_token().drop_guard_ref();
162
148
 
163
- // Spawn a task to read remaining packets.
164
- let rpc_clone = rpc.clone();
165
- let mut read_task = tokio::spawn(async move {
149
+ // The packet reader is a sibling future, so dropping this call cannot detach transport work.
150
+ let read = async {
166
151
  while let Some(result) = framed.next().await {
167
152
  match result {
168
153
  Ok(packet) => {
169
- if rpc_clone.handle_packet(packet).await.is_err() {
154
+ if rpc.handle_packet(packet).await.is_err() {
170
155
  break;
171
156
  }
172
157
  }
173
158
  Err(_) => break,
174
159
  }
175
160
  }
176
- let _ = rpc_clone.handle_stream_close(None).await;
177
- });
178
-
179
- // Invoke the method.
180
- let stream: Box<dyn Stream> = Box::new(ServerStream { rpc: rpc.clone() });
181
- let (found, result) = self
182
- .invoker
183
- .invoke_method(&service_id, &method_id, stream)
184
- .await;
185
-
186
- // Handle the result.
187
- if !found {
188
- // Send unimplemented error.
189
- let _ = rpc.send_error("method not implemented".to_string()).await;
190
- } else if let Err(e) = result {
191
- // Send error response.
192
- let _ = rpc.send_error(e.to_string()).await;
193
- } else {
194
- // Close send side on success.
195
- let _ = rpc.close_send().await;
196
- }
197
-
198
- // Explicitly close the RPC to release the writer/transport.
199
- // This ensures the connection doesn't remain open after the terminal response.
200
- let _ = rpc.close().await;
161
+ let _ = rpc.handle_stream_close(None).await;
162
+ };
163
+ let invoke = serve_rpc(self.invoker.as_ref(), &rpc);
164
+ tokio::pin!(read, invoke);
201
165
 
202
- // Wait for the read task to finish or timeout, then abort it.
203
- // This gives time for the client to receive our response.
166
+ // Remote closure cancels the same context that the active service receives.
167
+ // After a reply, retain the existing bounded grace period for the peer's transport close.
204
168
  tokio::select! {
205
- _ = &mut read_task => {
206
- // Read task finished naturally
207
- }
208
- _ = tokio::time::sleep(self.config.shutdown_timeout) => {
209
- // Timeout - abort the read task
210
- read_task.abort();
169
+ () = &mut read => invoke.await,
170
+ () = &mut invoke => {
171
+ let _ = tokio::time::timeout(self.config.shutdown_timeout, read).await;
211
172
  }
212
173
  }
213
174
 
@@ -252,73 +213,59 @@ impl<I: Invoker + 'static> Server<I> {
252
213
  .await
253
214
  }
254
215
 
255
- /// Accepts and handles connections in a loop.
256
- ///
257
- /// This is a convenience method that accepts connections from a listener
258
- /// and spawns a task to handle each one.
259
- ///
260
- /// Errors from individual connections are reported via the error handler
261
- /// (if configured) but don't stop the server.
216
+ /// Accepts and handles connections concurrently, draining accepted calls when the listener ends.
217
+ /// Dropping this future drops every active call and cancels its transport context.
218
+ /// Individual connection failures reach the error handler without stopping other calls.
262
219
  pub async fn serve<L, T>(&self, mut listener: L) -> Result<()>
263
220
  where
264
221
  L: futures::Stream<Item = std::io::Result<T>> + Unpin,
265
222
  T: AsyncRead + AsyncWrite + Send + Unpin + 'static,
266
223
  {
267
- while let Some(result) = listener.next().await {
268
- match result {
269
- Ok(stream) => {
270
- let server = self.clone_for_spawn();
271
- tokio::spawn(async move {
272
- if let Err(e) = server.handle_stream(stream).await {
273
- server.report_error(e);
274
- }
275
- });
276
- }
277
- Err(e) => {
278
- self.report_error(Error::Io(e));
224
+ // Concurrent call futures remain owned by this invocation, without detached tasks.
225
+ let mut calls = FuturesUnordered::new();
226
+ let mut accepting = true;
227
+ loop {
228
+ tokio::select! {
229
+ connection = listener.next(), if accepting => {
230
+ match connection {
231
+ Some(Ok(stream)) => calls.push(async move {
232
+ if let Err(error) = self.handle_stream(stream).await {
233
+ self.report_error(error);
234
+ }
235
+ }),
236
+ Some(Err(error)) => self.report_error(Error::Io(error)),
237
+ None => accepting = false,
238
+ }
279
239
  }
240
+ Some(()) = calls.next(), if !calls.is_empty() => {}
241
+ else => return Ok(()),
280
242
  }
281
243
  }
282
-
283
- Ok(())
284
- }
285
-
286
- /// Creates a clone of the server for spawning tasks.
287
- pub(crate) fn clone_for_spawn(&self) -> Server<I> {
288
- Server {
289
- invoker: self.invoker.clone(),
290
- config: self.config.clone(),
291
- error_handler: self.error_handler.clone(),
292
- }
293
244
  }
294
245
  }
295
246
 
296
- /// Wrapper to provide Stream interface for ServerRpc.
297
- struct ServerStream {
298
- rpc: Arc<ServerRpc>,
299
- }
300
-
301
- #[async_trait::async_trait]
302
- impl Stream for ServerStream {
303
- fn context(&self) -> &Context {
304
- self.rpc.context()
305
- }
306
-
307
- async fn send_bytes(&self, data: Bytes) -> Result<()> {
308
- self.rpc.send_bytes(data).await
309
- }
310
-
311
- async fn recv_bytes(&self) -> Result<Bytes> {
312
- self.rpc.recv_bytes().await
313
- }
314
-
315
- async fn close_send(&self) -> Result<()> {
316
- self.rpc.close_send().await
317
- }
318
-
319
- async fn close(&self) -> Result<()> {
320
- self.rpc.close().await
321
- }
247
+ /// Invokes the method a server RPC names and settles the call with the outcome.
248
+ ///
249
+ /// An unknown method or a handler error reaches the peer as an error, and a
250
+ /// handler that returns cleanly closes the send side. The RPC is closed last,
251
+ /// which releases its writer.
252
+ pub(crate) async fn serve_rpc<I: Invoker + ?Sized>(invoker: &I, rpc: &Arc<ServerRpc>) {
253
+ // Invoke the method with the RPC as its stream.
254
+ let (found, result) = invoker
255
+ .invoke_method(rpc.service(), rpc.method(), Box::new(rpc.clone()))
256
+ .await;
257
+
258
+ // Report the outcome to the peer; a failed write leaves nothing to settle.
259
+ let _ = if !found {
260
+ rpc.send_error("method not implemented".to_string()).await
261
+ } else if let Err(err) = result {
262
+ rpc.send_error(err.to_string()).await
263
+ } else {
264
+ rpc.close_send().await
265
+ };
266
+
267
+ // Close the RPC so the connection does not stay open after the terminal response.
268
+ let _ = rpc.close().await;
322
269
  }
323
270
 
324
271
  #[cfg(test)]
@@ -330,10 +277,9 @@ mod tests {
330
277
  #[tokio::test]
331
278
  async fn test_server_config() {
332
279
  let mux = Mux::new();
333
- let server = Server::new(mux)
334
- .with_config(ServerConfig {
335
- shutdown_timeout: Duration::from_secs(1),
336
- });
280
+ let server = Server::new(mux).with_config(ServerConfig {
281
+ shutdown_timeout: Duration::from_secs(1),
282
+ });
337
283
 
338
284
  assert_eq!(server.config.shutdown_timeout, Duration::from_secs(1));
339
285
  }
@@ -346,10 +292,9 @@ mod tests {
346
292
  let errors_clone = errors.clone();
347
293
 
348
294
  let mux = Mux::new();
349
- let server = Server::new(mux)
350
- .with_error_handler(move |e| {
351
- errors_clone.lock().unwrap().push(e.to_string());
352
- });
295
+ let server = Server::new(mux).with_error_handler(move |e| {
296
+ errors_clone.lock().unwrap().push(e.to_string());
297
+ });
353
298
 
354
299
  // Report an error
355
300
  server.report_error(Error::StreamClosed);
package/srpc/starpc.hpp CHANGED
@@ -1,7 +1,9 @@
1
1
  #pragma once
2
2
 
3
- // Main header for starpc C++ library
4
- // Include this to get all starpc functionality
3
+ /*
4
+ * starpc.hpp is the library's umbrella header: include it for the whole
5
+ * hand-written C++ API.
6
+ */
5
7
 
6
8
  #include "client-rpc.hpp"
7
9
  #include "client.hpp"
package/srpc/stream.go CHANGED
@@ -17,6 +17,10 @@ type Stream interface {
17
17
  MsgRecv(msg Message) error
18
18
 
19
19
  // CloseSend signals to the remote that we will no longer send any messages.
20
+ //
21
+ // An error means the signal was not written, usually because the call
22
+ // already ended. MsgRecv still returns every message received before the
23
+ // end, then the call's outcome.
20
24
  CloseSend() error
21
25
 
22
26
  // Close closes the stream for reading and writing.
package/srpc/stream.hpp CHANGED
@@ -1,61 +1,122 @@
1
1
  #pragma once
2
2
 
3
- #include <functional>
4
- #include <memory>
5
-
6
3
  #include "errors.hpp"
7
4
  #include "message.hpp"
8
5
 
6
+ #include <atomic>
7
+ #include <functional>
8
+ #include <memory>
9
+ #include <stop_token>
10
+ #include <string>
11
+
9
12
  namespace starpc {
10
13
 
11
- // Stream is a handle to an on-going bi-directional or one-directional stream
12
- // RPC handle. Matches Go Stream interface in stream.go
14
+ /*
15
+ * Stream is one live RPC stream between the caller and the remote. Matches
16
+ * the Go Stream interface in stream.go.
17
+ */
13
18
  class Stream {
14
19
  public:
15
- virtual ~Stream() = default;
20
+ virtual ~Stream() = default;
21
+
22
+ /* StopToken interrupts work when the RPC ends or is canceled. */
23
+ virtual std::stop_token StopToken() const
24
+ {
25
+ return {};
26
+ }
16
27
 
17
- // MsgSend sends the message to the remote.
18
- virtual Error MsgSend(const Message &msg) = 0;
28
+ /*
29
+ * RemoteErrorMessage returns the diagnostic accompanying
30
+ * Error::RemoteError.
31
+ */
32
+ virtual std::string RemoteErrorMessage() const
33
+ {
34
+ return {};
35
+ }
19
36
 
20
- // MsgRecv receives an incoming message from the remote.
21
- // Parses the message into the object at msg.
22
- virtual Error MsgRecv(Message *msg) = 0;
37
+ /* MsgSend serializes msg and writes it to the remote. */
38
+ virtual Error MsgSend(const Message &msg) = 0;
23
39
 
24
- // CloseSend signals to the remote that we will no longer send any messages.
25
- virtual Error CloseSend() = 0;
40
+ /*
41
+ * MsgRecv reads the next message from the remote and parses it into
42
+ * msg. Returns EOF_ after the call completes.
43
+ */
44
+ virtual Error MsgRecv(Message *msg) = 0;
26
45
 
27
- // Close closes the stream for reading and writing.
28
- virtual Error Close() = 0;
46
+ /*
47
+ * CloseSend half-closes: no further messages will be sent. Once the
48
+ * request is written, the half-close is only a notice: it fails when
49
+ * the call already ended, and MsgRecv then returns the buffered
50
+ * messages followed by the call's outcome.
51
+ */
52
+ virtual Error CloseSend() = 0;
53
+
54
+ /* Close ends the stream for both reading and writing. */
55
+ virtual Error Close() = 0;
29
56
  };
30
57
 
31
- // StreamWithClose wraps a Stream with a close function to call when Close is
32
- // called. Matches streamWithClose in stream.go
58
+ /*
59
+ * StreamWithClose forwards a Stream and runs an extra callback on the first
60
+ * Close. Matches streamWithClose in stream.go.
61
+ */
33
62
  class StreamWithClose : public Stream {
34
63
  public:
35
- StreamWithClose(Stream *inner, std::function<Error()> close_fn)
36
- : inner_(inner), close_fn_(std::move(close_fn)) {}
64
+ /*
65
+ * StreamWithClose borrows inner; it must outlive the wrapper.
66
+ * close_fn runs once, after inner->Close.
67
+ */
68
+ StreamWithClose(Stream *inner, std::function<Error()> close_fn)
69
+ : inner(inner),
70
+ close_fn(std::move(close_fn))
71
+ {
72
+ }
73
+
74
+ Error MsgSend(const Message &msg) override
75
+ {
76
+ return inner->MsgSend(msg);
77
+ }
78
+ Error MsgRecv(Message *msg) override
79
+ {
80
+ return inner->MsgRecv(msg);
81
+ }
82
+ Error CloseSend() override
83
+ {
84
+ return inner->CloseSend();
85
+ }
86
+ std::stop_token StopToken() const override
87
+ {
88
+ return inner->StopToken();
89
+ }
90
+ std::string RemoteErrorMessage() const override
91
+ {
92
+ return inner->RemoteErrorMessage();
93
+ }
37
94
 
38
- Error MsgSend(const Message &msg) override { return inner_->MsgSend(msg); }
39
- Error MsgRecv(Message *msg) override { return inner_->MsgRecv(msg); }
40
- Error CloseSend() override { return inner_->CloseSend(); }
95
+ Error Close() override
96
+ {
97
+ if (closed.exchange(true))
98
+ return Error::OK;
41
99
 
42
- Error Close() override {
43
- Error err = inner_->Close();
44
- Error err2 = close_fn_();
45
- if (err != Error::OK)
46
- return err;
47
- return err2;
48
- }
100
+ /* Report the inner close even when the callback also fails. */
101
+ Error err = inner->Close();
102
+ Error err2 = close_fn();
103
+ if (err != Error::OK)
104
+ return err;
105
+ return err2;
106
+ }
49
107
 
50
108
  private:
51
- Stream *inner_;
52
- std::function<Error()> close_fn_;
109
+ /* inner is borrowed; the caller owns it. */
110
+ Stream *inner;
111
+ std::function<Error()> close_fn;
112
+ std::atomic<bool> closed{false};
53
113
  };
54
114
 
55
- // NewStreamWithClose wraps a Stream with a close function.
115
+ /* NewStreamWithClose wraps strm so close_fn runs on the first Close. */
56
116
  inline std::unique_ptr<Stream>
57
- NewStreamWithClose(Stream *strm, std::function<Error()> close_fn) {
58
- return std::make_unique<StreamWithClose>(strm, std::move(close_fn));
117
+ NewStreamWithClose(Stream *strm, std::function<Error()> close_fn)
118
+ {
119
+ return std::make_unique<StreamWithClose>(strm, std::move(close_fn));
59
120
  }
60
121
 
61
122
  } // namespace starpc
package/srpc/stream.rs CHANGED
@@ -110,7 +110,7 @@ impl Context {
110
110
  ///
111
111
  /// - All methods are async and may block waiting for I/O or messages
112
112
  /// - `send_bytes` may return `Error::Completed` if `close_send` was already called
113
- /// - `recv_bytes` returns `Error::StreamClosed` when the remote closes
113
+ /// - `recv_bytes` returns `Error::StreamClosed` when the remote completes
114
114
  /// - `close` cancels the context and releases all resources
115
115
  #[async_trait]
116
116
  pub trait Stream: Send + Sync {
@@ -133,8 +133,9 @@ pub trait Stream: Send + Sync {
133
133
  ///
134
134
  /// # Errors
135
135
  ///
136
- /// - `Error::StreamClosed` if the remote closed the stream
136
+ /// - `Error::StreamClosed` if the remote completed the call
137
137
  /// - `Error::Remote` if the remote sent an error
138
+ /// - `Error::ClosedBeforeCompletion` if the transport closed first
138
139
  /// - `Error::Cancelled` if the context was cancelled
139
140
  async fn recv_bytes(&self) -> Result<Bytes>;
140
141
 
@@ -142,6 +143,10 @@ pub trait Stream: Send + Sync {
142
143
  ///
143
144
  /// After calling this, `send_bytes` will return `Error::Completed`.
144
145
  /// The receive side remains open until the remote closes.
146
+ ///
147
+ /// Once the request is written, the half-close is only a notice. It fails
148
+ /// when the call already ended, and `recv_bytes` then returns the buffered
149
+ /// messages followed by the call's outcome.
145
150
  async fn close_send(&self) -> Result<()>;
146
151
 
147
152
  /// Closes both sides of the stream.