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
@@ -6,15 +6,15 @@ import (
6
6
  "github.com/pkg/errors"
7
7
  )
8
8
 
9
- // ServerRPC represents the server side of an on-going RPC call message stream.
9
+ // ServerRPC runs one remote method and retains its resources until the handler exits.
10
10
  type ServerRPC struct {
11
11
  commonRPC
12
- // invoker is the rpc call invoker
12
+
13
+ // invoker dispatches the selected service and method.
13
14
  invoker Invoker
14
15
  }
15
16
 
16
- // NewServerRPC constructs a new ServerRPC session.
17
- // note: call SetWriter before handling any incoming messages.
17
+ // NewServerRPC attaches the handler and transport for one incoming call.
18
18
  func NewServerRPC(ctx context.Context, invoker Invoker, writer PacketWriter) *ServerRPC {
19
19
  rpc := &ServerRPC{invoker: invoker}
20
20
  initCommonRPC(ctx, &rpc.commonRPC)
@@ -33,6 +33,7 @@ func (r *ServerRPC) HandlePacketData(data []byte) error {
33
33
 
34
34
  // HandlePacket handles an incoming parsed message packet.
35
35
  func (r *ServerRPC) HandlePacket(msg *Packet) error {
36
+ // Ignore an absent packet and reject malformed messages before dispatch.
36
37
  if msg == nil {
37
38
  return nil
38
39
  }
@@ -40,6 +41,7 @@ func (r *ServerRPC) HandlePacket(msg *Packet) error {
40
41
  return err
41
42
  }
42
43
 
44
+ // Route each packet through the shared stream state.
43
45
  switch b := msg.GetBody().(type) {
44
46
  case *Packet_CallStart:
45
47
  return r.HandleCallStart(b.CallStart)
@@ -57,25 +59,20 @@ func (r *ServerRPC) HandlePacket(msg *Packet) error {
57
59
 
58
60
  // HandleCallStart handles the call start packet.
59
61
  func (r *ServerRPC) HandleCallStart(pkt *CallStart) error {
60
- var err error
61
-
62
+ // Accept one start before any terminal stream transition.
62
63
  locked := r.bcast.Lock()
63
- // process start: method and service
64
64
  if r.method != "" || r.service != "" {
65
- err = errors.New("call start must be sent only once")
66
65
  locked.Unlock()
67
- return err
66
+ return errors.New("call start must be sent only once")
68
67
  }
69
68
  if r.dataClosed {
70
- err = ErrCompleted
71
69
  locked.Unlock()
72
- return err
70
+ return ErrCompleted
73
71
  }
74
72
 
73
+ // Retain the request identity and optional first message for the handler.
75
74
  service, method := pkt.GetRpcService(), pkt.GetRpcMethod()
76
75
  r.service, r.method = service, method
77
-
78
- // process first data packet, if included
79
76
  if data := pkt.GetData(); len(data) != 0 || pkt.GetDataIsZero() {
80
77
  r.dataQueue = append(r.dataQueue, data)
81
78
  }
@@ -84,27 +81,32 @@ func (r *ServerRPC) HandleCallStart(pkt *CallStart) error {
84
81
  // Mark the method active before scheduling it so cancellation cannot make
85
82
  // Wait return and release a mux while invokeRPC is still running user code.
86
83
  r.localActive = true
87
-
88
- // invoke the rpc
89
84
  locked.Broadcast()
90
85
  startServerRPCInvoke(func() {
91
86
  r.invokeRPC(service, method)
92
87
  })
93
88
  locked.Unlock()
94
89
 
95
- return err
90
+ return nil
96
91
  }
97
92
 
98
93
  // invokeRPC invokes the RPC after CallStart is received.
99
94
  func (r *ServerRPC) invokeRPC(serviceID, methodID string) {
100
- // on the server side, the writer is closed by invokeRPC.
95
+ // Run the selected handler before publishing its terminal result.
101
96
  strm := NewMsgStream(r.ctx, r, r.cancelContext)
102
97
  ok, err := r.invoker.InvokeMethod(serviceID, methodID, strm)
103
98
  if err == nil && !ok {
104
99
  err = ErrUnimplemented
105
100
  }
106
- r.beginLocalCompletion()
101
+
102
+ // Cancellation has its own packet; a text error would lose its identity.
107
103
  outPkt := NewCallDataPacket(nil, false, true, err)
108
- _ = r.writer.WritePacket(outPkt)
104
+ if errors.Is(err, context.Canceled) {
105
+ outPkt = NewCallCancelPacket()
106
+ }
107
+ r.beginLocalCompletion()
108
+ if err := r.writer.WritePacket(outPkt); err != nil {
109
+ r.HandleStreamClose(err)
110
+ }
109
111
  r.finishLocalCompletion()
110
112
  }
@@ -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);
@@ -1,5 +1,7 @@
1
1
  import { describe, it, beforeEach, expect, vi } from 'vitest'
2
2
  import { pipe } from 'it-pipe'
3
+ import { pushable } from 'it-pushable'
4
+ import type { Source } from 'it-stream-types'
3
5
  import {
4
6
  createHandler,
5
7
  createMux,
@@ -175,6 +177,8 @@ describe('srpc server', () => {
175
177
  const server = new Server(mux.lookupMethod)
176
178
  const firstResponse = new Promise<Packet>((resolve, reject) => {
177
179
  server.handlePacketStream({
180
+ close: async () => {},
181
+ abort: () => {},
178
182
  source: (async function* () {
179
183
  yield Packet.toBinary({
180
184
  body: {
@@ -342,6 +346,52 @@ describe('srpc server', () => {
342
346
  await Promise.resolve()
343
347
  })
344
348
 
349
+ it('closes the packet stream when the server pipeline completes', async () => {
350
+ const server = new Server(createMux().lookupMethod)
351
+ const close = vi.fn(async () => {})
352
+ const source = pushable<Uint8Array>({ objectMode: true })
353
+ const stream = {
354
+ close,
355
+ abort: vi.fn(),
356
+ source,
357
+ sink: async (output: Source<Uint8Array>) => {
358
+ for await (const _packet of output) {
359
+ // Drain the response pipeline.
360
+ }
361
+ },
362
+ }
363
+ const rpc = server.handlePacketStream(stream)
364
+
365
+ await rpc.close()
366
+ source.end()
367
+
368
+ await vi.waitFor(() => expect(close).toHaveBeenCalledOnce())
369
+ expect(stream.abort).not.toHaveBeenCalled()
370
+ })
371
+
372
+ it('aborts the packet stream when the server pipeline fails', async () => {
373
+ const server = new Server(createMux().lookupMethod)
374
+ const error = new Error('input failed')
375
+ const abort = vi.fn()
376
+ const source = pushable<Uint8Array>({ objectMode: true })
377
+ const stream = {
378
+ close: vi.fn(async () => {}),
379
+ abort,
380
+ source,
381
+ sink: async (output: Source<Uint8Array>) => {
382
+ for await (const _packet of output) {
383
+ // Drain the response pipeline.
384
+ }
385
+ },
386
+ }
387
+
388
+ server.handlePacketStream(stream)
389
+ source.end(error)
390
+
391
+ await vi.waitFor(() => expect(abort).toHaveBeenCalledWith(error))
392
+ expect(stream.close).not.toHaveBeenCalled()
393
+ })
394
+
345
395
  it('tears down passive channel close state', async () => {
346
396
  const { port1, port2 } = new MessageChannel()
347
397
  const opts: ChannelStreamOpts = { idleTimeoutMs: 1000, keepAliveMs: 1000 }
package/srpc/server.ts CHANGED
@@ -20,9 +20,7 @@ export class Server implements StreamHandler {
20
20
  public get rpcStreamHandler(): HandleStreamFunc {
21
21
  return async (stream: PacketStream) => {
22
22
  const rpc = this.startRpc()
23
- return pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
24
- .catch((err: Error) => rpc.close(err))
25
- .then(() => rpc.close())
23
+ return runPacketStream(stream, rpc)
26
24
  }
27
25
  }
28
26
 
@@ -36,9 +34,27 @@ export class Server implements StreamHandler {
36
34
  // the stream has one Uint8Array per packet w/o length prefix.
37
35
  public handlePacketStream(stream: PacketStream): ServerRPC {
38
36
  const rpc = this.startRpc()
39
- pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
40
- .catch((err: Error) => rpc.close(err))
41
- .then(() => rpc.close())
37
+ void runPacketStream(stream, rpc).catch(() => undefined)
42
38
  return rpc
43
39
  }
44
40
  }
41
+
42
+ async function runPacketStream(
43
+ stream: PacketStream,
44
+ rpc: ServerRPC,
45
+ ): Promise<void> {
46
+ try {
47
+ await pipe(stream, decodePacketSource, rpc, encodePacketSource, stream)
48
+ if (rpc.isClosed instanceof Error) {
49
+ stream.abort(rpc.isClosed)
50
+ throw rpc.isClosed
51
+ }
52
+ await stream.close()
53
+ await rpc.close()
54
+ } catch (err) {
55
+ const error = err instanceof Error ? err : new Error(String(err))
56
+ stream.abort(error)
57
+ await rpc.close(error)
58
+ throw error
59
+ }
60
+ }
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.