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
package/srpc/client.rs CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  use async_trait::async_trait;
8
8
  use bytes::Bytes;
9
+ use futures::StreamExt as _;
9
10
  use prost::Message;
10
11
  use std::sync::Arc;
11
12
 
@@ -14,8 +15,8 @@ use crate::rpc::{ClientRpc, PacketWriter};
14
15
  use crate::stream::{Context, Stream, StreamExt};
15
16
  use crate::transport::create_packet_channel;
16
17
 
17
- /// Receiver for incoming packets.
18
- pub type PacketReceiver = tokio::sync::mpsc::Receiver<crate::proto::Packet>;
18
+ /// Owned incoming packets; dropping this stream releases its transport or nested call.
19
+ pub type PacketReceiver = futures::stream::BoxStream<'static, crate::proto::Packet>;
19
20
 
20
21
  /// Trait for opening streams to a remote server.
21
22
  ///
@@ -40,12 +41,8 @@ pub trait Client: Send + Sync {
40
41
  I: Message + Send + Sync,
41
42
  O: Message + Default;
42
43
 
43
- /// Opens a new stream for a streaming RPC.
44
- ///
45
- /// # Arguments
46
- /// * `service` - The service ID
47
- /// * `method` - The method ID
48
- /// * `first_msg` - Optional initial message data
44
+ /// Opens a new stream for a streaming RPC, sending `first_msg` as the
45
+ /// initial data when present.
49
46
  async fn new_stream(
50
47
  &self,
51
48
  service: &str,
@@ -95,38 +92,36 @@ impl<T: OpenStream + 'static> Client for SrpcClient<T> {
95
92
  writer,
96
93
  ));
97
94
 
98
- // Start the RPC with the input data.
95
+ // Cancellation and every early return release the directly owned incoming transport.
96
+ let _cancel = ctx.cancel_token().drop_guard_ref();
99
97
  rpc.start(Some(Bytes::from(input_data))).await?;
100
98
 
101
- // Spawn a task to handle incoming packets.
102
- let rpc_clone = rpc.clone();
103
- let packet_handler = tokio::spawn(async move {
104
- while let Some(packet) = receiver.recv().await {
105
- if rpc_clone.handle_packet(packet).await.is_err() {
99
+ // Read packets alongside the unary response without a second task or lifetime.
100
+ let read = async {
101
+ while let Some(packet) = receiver.next().await {
102
+ if rpc.handle_packet(packet).await.is_err() {
106
103
  break;
107
104
  }
108
105
  }
109
- let _ = rpc_clone.handle_stream_close(None).await;
110
- });
111
-
112
- // Send close to indicate we're done sending.
113
- rpc.close_send().await?;
114
-
115
- // Receive the response.
116
- let output: O = rpc.msg_recv().await?;
117
-
118
- // Wait for the RPC to complete properly (receive any trailing packets).
119
- // This ensures we process any completion/error packets from the server
120
- // before cleaning up.
121
- let _ = rpc.wait().await;
122
-
123
- // Close the RPC to signal completion.
124
- let _ = rpc.close().await;
125
-
126
- // Clean up the packet handler.
127
- packet_handler.abort();
128
-
129
- Ok(output)
106
+ let _ = rpc.handle_stream_close(None).await;
107
+ };
108
+ let response = async {
109
+ // A failed half-close means the call ended; msg_recv reports its outcome.
110
+ let _ = rpc.close_send().await;
111
+ let output: O = rpc.msg_recv().await?;
112
+
113
+ // Process trailing completion before releasing the writer and receive capability.
114
+ let _ = rpc.wait().await;
115
+ let _ = rpc.close().await;
116
+ Ok(output)
117
+ };
118
+ tokio::pin!(read, response);
119
+
120
+ // Transport closure settles the same RPC that the response future is observing.
121
+ tokio::select! {
122
+ output = &mut response => output,
123
+ () = &mut read => response.await,
124
+ }
130
125
  }
131
126
 
132
127
  async fn new_stream(
@@ -154,7 +149,7 @@ impl<T: OpenStream + 'static> Client for SrpcClient<T> {
154
149
  // Spawn a task to handle incoming packets.
155
150
  let rpc_clone = rpc.clone();
156
151
  let packet_handler = tokio::spawn(async move {
157
- while let Some(packet) = receiver.recv().await {
152
+ while let Some(packet) = receiver.next().await {
158
153
  if rpc_clone.handle_packet(packet).await.is_err() {
159
154
  break;
160
155
  }
@@ -178,6 +173,15 @@ struct ClientStream {
178
173
  packet_handler: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
179
174
  }
180
175
 
176
+ impl Drop for ClientStream {
177
+ fn drop(&mut self) {
178
+ self.rpc.context().cancel();
179
+ if let Some(handler) = self.packet_handler.get_mut().take() {
180
+ handler.abort();
181
+ }
182
+ }
183
+ }
184
+
181
185
  #[async_trait]
182
186
  impl Stream for ClientStream {
183
187
  fn context(&self) -> &Context {
@@ -199,9 +203,12 @@ impl Stream for ClientStream {
199
203
  async fn close(&self) -> Result<()> {
200
204
  let _ = self.rpc.close().await;
201
205
  // Abort the background packet handler to ensure cleanup.
202
- if let Some(handle) = self.packet_handler.lock().await.take() {
206
+ let mut handler = self.packet_handler.lock().await;
207
+ if let Some(handle) = handler.as_mut() {
203
208
  handle.abort();
209
+ let _ = handle.await;
204
210
  }
211
+ handler.take();
205
212
  Ok(())
206
213
  }
207
214
  }
@@ -309,7 +316,12 @@ mod tests {
309
316
  (
310
317
  Self {
311
318
  writer: Arc::new(MockWriter::new()),
312
- receiver: Mutex::new(Some(rx)),
319
+ receiver: Mutex::new(Some(Box::pin(futures::stream::unfold(
320
+ rx,
321
+ |mut receiver| async move {
322
+ receiver.recv().await.map(|packet| (packet, receiver))
323
+ },
324
+ )))),
313
325
  },
314
326
  tx,
315
327
  )
@@ -340,6 +352,9 @@ mod tests {
340
352
  .unwrap();
341
353
 
342
354
  assert!(!stream.context().is_cancelled());
355
+ let context = stream.context().clone();
356
+ drop(stream);
357
+ assert!(context.is_cancelled());
343
358
  }
344
359
 
345
360
  #[tokio::test]
@@ -357,4 +372,29 @@ mod tests {
357
372
  let result2 = opener.open_stream().await;
358
373
  assert!(matches!(result2, Err(Error::StreamClosed)));
359
374
  }
375
+
376
+ #[tokio::test]
377
+ async fn malformed_unary_response_releases_receiver() {
378
+ #[derive(Clone, PartialEq, prost::Message)]
379
+ struct Reply {
380
+ #[prost(string, tag = "1")]
381
+ text: String,
382
+ }
383
+
384
+ let (opener, sender) = MockOpener::new();
385
+ let client = SrpcClient::new(opener);
386
+ sender
387
+ .send(crate::packet::new_call_data_full(
388
+ Some(Bytes::from_static(&[0xff])),
389
+ false,
390
+ None,
391
+ ))
392
+ .await
393
+ .unwrap();
394
+ let result: Result<Reply> = client.exec_call("test", "call", &Reply::default()).await;
395
+ assert!(result.is_err());
396
+ tokio::time::timeout(std::time::Duration::from_secs(2), sender.closed())
397
+ .await
398
+ .expect("failed unary call retained its packet receiver");
399
+ }
360
400
  }
@@ -0,0 +1,331 @@
1
+ //go:build deps_only
2
+
3
+ /*
4
+ * Half-close checks for the generated C++ callers. Once the request is
5
+ * written, the half-close is only a notice: a caller that half-closes after
6
+ * the remote ended the call still reads the buffered responses and the
7
+ * call's outcome.
8
+ */
9
+
10
+ #include "echo/echo_srpc.pb.hpp"
11
+ #include "srpc/rpcproto.pb.h"
12
+ #include "srpc/server-rpc.hpp"
13
+
14
+ #include <cstdlib>
15
+ #include <iostream>
16
+ #include <latch>
17
+
18
+ namespace {
19
+ using starpc::Error;
20
+
21
+ const char *body = "hello world via starpc half-close test";
22
+
23
+ void Check(bool value, const char *message)
24
+ {
25
+ if (value)
26
+ return;
27
+ std::cerr << message << '\n';
28
+ std::exit(1);
29
+ }
30
+
31
+ /*
32
+ * OnceEchoServer answers each streaming call once and completes it.
33
+ */
34
+ class OnceEchoServer final : public echo::SRPCEchoerServer {
35
+ public:
36
+ Error Echo(const echo::EchoMsg &req, echo::EchoMsg *resp) override
37
+ {
38
+ *resp = req;
39
+ return Error::OK;
40
+ }
41
+
42
+ Error
43
+ EchoServerStream(const echo::EchoMsg &req,
44
+ echo::SRPCEchoer_EchoServerStreamStream *strm) override
45
+ {
46
+ return strm->Send(req);
47
+ }
48
+
49
+ /*
50
+ * EchoClientStream replies to the first message without waiting for
51
+ * the half-close.
52
+ */
53
+ Error EchoClientStream(echo::SRPCEchoer_EchoClientStreamStream *strm,
54
+ echo::EchoMsg *resp) override
55
+ {
56
+ return strm->Recv(resp);
57
+ }
58
+
59
+ Error EchoBidiStream(echo::SRPCEchoer_EchoBidiStreamStream *) override
60
+ {
61
+ return Error::Unimplemented;
62
+ }
63
+
64
+ Error RpcStream(echo::SRPCEchoer_RpcStreamStream *) override
65
+ {
66
+ return Error::Unimplemented;
67
+ }
68
+
69
+ Error DoNothing(const google::protobuf::Empty &,
70
+ google::protobuf::Empty *) override
71
+ {
72
+ return Error::OK;
73
+ }
74
+ };
75
+
76
+ /* FuncWriter forwards packets and the transport close to callbacks. */
77
+ class FuncWriter final : public starpc::PacketWriter {
78
+ public:
79
+ FuncWriter(std::function<Error(const srpc::Packet &)> write,
80
+ std::function<void()> close)
81
+ : write(std::move(write)),
82
+ close(std::move(close))
83
+ {
84
+ }
85
+
86
+ Error WritePacket(const srpc::Packet &packet) override
87
+ {
88
+ return write(packet);
89
+ }
90
+
91
+ Error Close() override
92
+ {
93
+ close();
94
+ return Error::OK;
95
+ }
96
+
97
+ private:
98
+ std::function<Error(const srpc::Packet &)> write;
99
+ std::function<void()> close;
100
+ };
101
+
102
+ /*
103
+ * EchoPipe serves OnceEchoServer to one client stream in memory. closed
104
+ * counts down after the client's transport close callback returns.
105
+ */
106
+ class EchoPipe {
107
+ public:
108
+ EchoPipe()
109
+ {
110
+ auto [handler, err] =
111
+ echo::SRPCRegisterEchoer(mux.get(), &impl);
112
+ Check(err == Error::OK, "register echoer");
113
+ this->handler = std::move(handler);
114
+ }
115
+
116
+ /* OpenStream connects the client stream to the server. */
117
+ starpc::OpenStreamFunc OpenStream()
118
+ {
119
+ return [this](starpc::PacketDataHandler on_packet,
120
+ starpc::CloseHandler on_close) {
121
+ server_writer = std::make_unique<FuncWriter>(
122
+ [on_packet](const srpc::Packet &packet) {
123
+ return on_packet(
124
+ packet.SerializeAsString());
125
+ },
126
+ [this, on_close] {
127
+ on_close(Error::OK);
128
+ closed.count_down();
129
+ });
130
+ server = starpc::NewServerRPC(mux.get(),
131
+ server_writer.get());
132
+ auto client_writer = std::make_unique<FuncWriter>(
133
+ [this](const srpc::Packet &packet) {
134
+ return server->HandlePacket(packet);
135
+ },
136
+ [this] {
137
+ server->HandleStreamClose(Error::OK);
138
+ });
139
+ return std::make_pair(
140
+ std::unique_ptr<starpc::PacketWriter>(
141
+ std::move(client_writer)),
142
+ Error::OK);
143
+ };
144
+ }
145
+
146
+ std::latch closed{1};
147
+
148
+ private:
149
+ OnceEchoServer impl;
150
+ std::unique_ptr<starpc::Mux> mux = starpc::NewMux();
151
+ std::unique_ptr<echo::SRPCEchoerHandler> handler;
152
+ std::unique_ptr<FuncWriter> server_writer;
153
+ std::unique_ptr<starpc::ServerRPC> server;
154
+ };
155
+
156
+ /*
157
+ * DroppingPipe accepts CallStart, then drops the transport without a
158
+ * response. closed counts down after the client's transport close callback
159
+ * returns.
160
+ */
161
+ class DroppingPipe {
162
+ public:
163
+ starpc::OpenStreamFunc OpenStream()
164
+ {
165
+ return [this](starpc::PacketDataHandler,
166
+ starpc::CloseHandler on_close) {
167
+ auto writer = std::make_unique<FuncWriter>(
168
+ [this, on_close](const srpc::Packet &) {
169
+ dropper =
170
+ std::jthread([this, on_close] {
171
+ on_close(Error::EOF_);
172
+ closed.count_down();
173
+ });
174
+ return Error::OK;
175
+ },
176
+ [] {});
177
+ return std::make_pair(
178
+ std::unique_ptr<starpc::PacketWriter>(
179
+ std::move(writer)),
180
+ Error::OK);
181
+ };
182
+ }
183
+
184
+ std::latch closed{1};
185
+
186
+ private:
187
+ std::jthread dropper;
188
+ };
189
+
190
+ /*
191
+ * EndedClient returns each new stream only after its transport has closed,
192
+ * so the generated caller half-closes after the remote already ended the
193
+ * call.
194
+ */
195
+ class EndedClient final : public starpc::Client {
196
+ public:
197
+ EndedClient(starpc::OpenStreamFunc open_stream, std::latch *closed)
198
+ : client(std::move(open_stream)),
199
+ closed(closed)
200
+ {
201
+ }
202
+
203
+ Error ExecCall(const std::string &service, const std::string &method,
204
+ const starpc::Message &in, starpc::Message *out) override
205
+ {
206
+ return client.ExecCall(service, method, in, out);
207
+ }
208
+
209
+ std::pair<std::unique_ptr<starpc::Stream>, Error>
210
+ NewStream(const std::string &service, const std::string &method,
211
+ const starpc::Message *first_msg) override
212
+ {
213
+ auto opened = client.NewStream(service, method, first_msg);
214
+ if (opened.second == Error::OK)
215
+ closed->wait();
216
+ return opened;
217
+ }
218
+
219
+ private:
220
+ starpc::ClientImpl client;
221
+ std::latch *closed;
222
+ };
223
+
224
+ /* ServerStreamCompletedBeforeCloseSend reads a reply after remote completion.
225
+ */
226
+ void ServerStreamCompletedBeforeCloseSend()
227
+ {
228
+ /* Complete the server stream before the generated caller half-closes.
229
+ */
230
+ EchoPipe pipe;
231
+ EndedClient client(pipe.OpenStream(), &pipe.closed);
232
+ echo::SRPCEchoerClientImpl echoer(&client);
233
+ echo::EchoMsg req;
234
+ req.set_body(body);
235
+ auto [strm, err] = echoer.EchoServerStream(req);
236
+ Check(err == Error::OK,
237
+ "server stream discarded after remote completion");
238
+
239
+ /* Read the buffered response, then the clean completion. */
240
+ echo::EchoMsg msg;
241
+ Check(strm->Recv(&msg) == Error::OK, "buffered response lost");
242
+ Check(msg.body() == body, "response body");
243
+ Check(strm->Recv(&msg) == Error::EOF_, "expected EOF after completion");
244
+ }
245
+
246
+ /* ClientStreamCompletedBeforeCloseSend reads a reply after a failed half-close.
247
+ */
248
+ void ClientStreamCompletedBeforeCloseSend()
249
+ {
250
+ EchoPipe pipe;
251
+ auto client = starpc::NewClient(pipe.OpenStream());
252
+ echo::SRPCEchoerClientImpl echoer(client.get());
253
+ auto [strm, err] = echoer.EchoClientStream();
254
+ Check(err == Error::OK, "open client stream");
255
+
256
+ /* Send the one request and wait for the call to end. */
257
+ echo::EchoMsg req;
258
+ req.set_body(body);
259
+ Check(strm->Send(req) == Error::OK, "send request");
260
+ pipe.closed.wait();
261
+
262
+ /* The half-close fails, but the reply is still readable. */
263
+ echo::EchoMsg msg;
264
+ Check(strm->CloseAndRecv(&msg) == Error::OK, "buffered reply lost");
265
+ Check(msg.body() == body, "reply body");
266
+ }
267
+
268
+ /* ServerStreamTransportFailureBeforeCloseSend reports a dropped transport. */
269
+ void ServerStreamTransportFailureBeforeCloseSend()
270
+ {
271
+ DroppingPipe pipe;
272
+ EndedClient client(pipe.OpenStream(), &pipe.closed);
273
+ echo::SRPCEchoerClientImpl echoer(&client);
274
+ echo::EchoMsg req;
275
+ req.set_body(body);
276
+ auto [strm, err] = echoer.EchoServerStream(req);
277
+ Check(err == Error::OK,
278
+ "server stream discarded after transport failure");
279
+
280
+ /* Recv reports that the call ended without a verdict. */
281
+ echo::EchoMsg msg;
282
+ Check(strm->Recv(&msg) == Error::ClosedBeforeCompletion,
283
+ "expected ClosedBeforeCompletion");
284
+ }
285
+
286
+ /*
287
+ * FailedHalfCloseKeepsPendingCompletion keeps the transport's buffered reply
288
+ * when the half-close write fails.
289
+ */
290
+ void FailedHalfCloseKeepsPendingCompletion()
291
+ {
292
+ /*
293
+ * The half-close write fails while the transport still holds the
294
+ * reply and completion it read but has not delivered.
295
+ */
296
+ starpc::ClientRPC rpc("test", "call");
297
+ FuncWriter writer(
298
+ [](const srpc::Packet &packet) {
299
+ return packet.has_call_data() ? Error::Canceled
300
+ : Error::OK;
301
+ },
302
+ [] {});
303
+ Check(rpc.Start(&writer, false, "") == Error::OK, "start");
304
+ auto strm = starpc::NewMsgStream(&rpc, [&] { rpc.Close(); });
305
+ Check(strm->CloseSend() == Error::Canceled, "half-close write fails");
306
+
307
+ /* The transport delivers what it read, then closes. */
308
+ srpc::CallData reply;
309
+ reply.set_data("reply");
310
+ reply.set_complete(true);
311
+ Check(rpc.HandleCallData(reply) == Error::OK,
312
+ "deliver pending completion");
313
+ rpc.HandleStreamClose(Error::OK);
314
+
315
+ std::string data;
316
+ Check(rpc.ReadOne(&data) == Error::OK && data == "reply",
317
+ "pending reply lost");
318
+ Check(rpc.ReadOne(&data) == Error::EOF_,
319
+ "expected EOF after completion");
320
+ }
321
+
322
+ } // namespace
323
+
324
+ int main()
325
+ {
326
+ ServerStreamCompletedBeforeCloseSend();
327
+ ClientStreamCompletedBeforeCloseSend();
328
+ ServerStreamTransportFailureBeforeCloseSend();
329
+ FailedHalfCloseKeepsPendingCompletion();
330
+ std::cout << "Half-close checks passed\n";
331
+ }
@@ -0,0 +1,75 @@
1
+ //! Proxy invocations through an existing client while retaining both directions.
2
+
3
+ use async_trait::async_trait;
4
+
5
+ use crate::{Client, Error, Invoker, Result, Stream};
6
+
7
+ /// Routes incoming methods through one client, preserving the remote terminal result.
8
+ pub struct ClientInvoker<C> {
9
+ client: C,
10
+ }
11
+
12
+ impl<C: Client> ClientInvoker<C> {
13
+ /// Wraps a client whose transport and routing remain under its existing owner.
14
+ pub fn new(client: C) -> Self {
15
+ Self { client }
16
+ }
17
+ }
18
+
19
+ #[async_trait]
20
+ impl<C: Client> Invoker for ClientInvoker<C> {
21
+ async fn invoke_method(
22
+ &self,
23
+ service: &str,
24
+ method: &str,
25
+ stream: Box<dyn Stream>,
26
+ ) -> (bool, Result<()>) {
27
+ // Opening a remote stream remains within the incoming call's cancellation.
28
+ let remote = tokio::select! {
29
+ () = stream.context().cancelled() => return (true, Err(Error::Cancelled)),
30
+ remote = self.client.new_stream(service, method, None) => match remote {
31
+ Ok(remote) => remote,
32
+ Err(error) => return (true, Err(error)),
33
+ },
34
+ };
35
+
36
+ // Request forwarding may half-close before the remote sends its result.
37
+ let requests = async {
38
+ match forward_messages(stream.as_ref(), remote.as_ref()).await {
39
+ Ok(()) => remote.close_send().await,
40
+ Err(error) => {
41
+ let _ = remote.close().await;
42
+ Err(error)
43
+ }
44
+ }
45
+ };
46
+ let responses = forward_messages(remote.as_ref(), stream.as_ref());
47
+ tokio::pin!(requests, responses);
48
+
49
+ // A remote result settles the call even if its caller has not half-closed.
50
+ let exchange = async {
51
+ tokio::select! {
52
+ result = &mut responses => result,
53
+ _ = &mut requests => responses.await,
54
+ }
55
+ };
56
+ let result = tokio::select! {
57
+ result = exchange => result,
58
+ () = stream.context().cancelled() => Err(Error::Cancelled),
59
+ };
60
+ let _ = remote.close().await;
61
+ (true, result)
62
+ }
63
+ }
64
+
65
+ /// Copies every message, including empty protobufs, until the source half-closes.
66
+ async fn forward_messages(source: &dyn Stream, target: &dyn Stream) -> Result<()> {
67
+ loop {
68
+ let data = match source.recv_bytes().await {
69
+ Ok(data) => data,
70
+ Err(Error::StreamClosed) => return Ok(()),
71
+ Err(error) => return Err(error),
72
+ };
73
+ target.send_bytes(data).await?;
74
+ }
75
+ }