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
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ from collections.abc import AsyncIterable, AsyncIterator
5
+ from typing import Protocol
6
+
7
+ from google.protobuf import empty_pb2 as _google_protobuf_empty_pb2
8
+
9
+ from echo import (
10
+ echo_pb2 as _github_com_aperturerobotics_starpc_echo_echo_pb2,
11
+ )
12
+ from rpcstream import (
13
+ rpcstream_pb2 as _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2,
14
+ )
15
+ from starpc.call import Call, CallProtocolError
16
+ from starpc.client import Client
17
+ from starpc.server import ServiceRegistry
18
+ from starpc.service import MethodDescriptor, ServiceDescriptor, bidirectional_bytes
19
+
20
+ ECHOER_SERVICE = ServiceDescriptor(
21
+ "echo.Echoer",
22
+ (
23
+ MethodDescriptor(
24
+ "Echo",
25
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
26
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
27
+ False,
28
+ False,
29
+ ),
30
+ MethodDescriptor(
31
+ "EchoServerStream",
32
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
33
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
34
+ False,
35
+ True,
36
+ ),
37
+ MethodDescriptor(
38
+ "EchoClientStream",
39
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
40
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
41
+ True,
42
+ False,
43
+ ),
44
+ MethodDescriptor(
45
+ "EchoBidiStream",
46
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
47
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg,
48
+ True,
49
+ True,
50
+ ),
51
+ MethodDescriptor(
52
+ "RpcStream",
53
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket,
54
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket,
55
+ True,
56
+ True,
57
+ ),
58
+ MethodDescriptor(
59
+ "DoNothing",
60
+ _google_protobuf_empty_pb2.Empty,
61
+ _google_protobuf_empty_pb2.Empty,
62
+ False,
63
+ False,
64
+ ),
65
+ ),
66
+ )
67
+
68
+
69
+ class EchoerClient:
70
+ def __init__(self, client: Client, service: str | None = None) -> None:
71
+ self._client = client
72
+ self._service = service or "echo.Echoer"
73
+
74
+ async def echo(
75
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
76
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg:
77
+ call = await self._client.open_call(
78
+ self._service, "Echo", request.SerializeToString(deterministic=True)
79
+ )
80
+ try:
81
+ data = await call.receive()
82
+ if data is None:
83
+ raise CallProtocolError("missing unary response")
84
+ response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
85
+ response.ParseFromString(data)
86
+ if await call.receive() is not None:
87
+ raise CallProtocolError("extra unary response")
88
+ return response
89
+ finally:
90
+ await call.aclose()
91
+
92
+ async def echo_server_stream(
93
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
94
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]:
95
+ call = await self._client.open_call(
96
+ self._service,
97
+ "EchoServerStream",
98
+ request.SerializeToString(deterministic=True),
99
+ )
100
+ try:
101
+ while True:
102
+ data = await call.receive()
103
+ if data is None:
104
+ return
105
+ response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
106
+ response.ParseFromString(data)
107
+ yield response
108
+ finally:
109
+ await call.aclose()
110
+
111
+ async def echo_client_stream(
112
+ self,
113
+ requests: AsyncIterable[
114
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
115
+ ],
116
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg:
117
+ call = await self._client.open_call(self._service, "EchoClientStream")
118
+ try:
119
+ async for request in requests:
120
+ await call.send(request.SerializeToString(deterministic=True))
121
+ # A failed half-close means the call ended; receive reports its outcome.
122
+ with contextlib.suppress(OSError):
123
+ await call.finish()
124
+ data = await call.receive()
125
+ if data is None:
126
+ raise CallProtocolError("missing client-stream response")
127
+ response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
128
+ response.ParseFromString(data)
129
+ if await call.receive() is not None:
130
+ raise CallProtocolError("extra client-stream response")
131
+ return response
132
+ finally:
133
+ await call.aclose()
134
+
135
+ async def echo_bidi_stream(
136
+ self,
137
+ requests: AsyncIterable[
138
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
139
+ ],
140
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]:
141
+ call = await self._client.open_call(self._service, "EchoBidiStream")
142
+
143
+ async def encoded() -> AsyncIterator[bytes]:
144
+ async for request in requests:
145
+ yield request.SerializeToString(deterministic=True)
146
+
147
+ async for data in bidirectional_bytes(call, encoded()):
148
+ response = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
149
+ response.ParseFromString(data)
150
+ yield response
151
+
152
+ async def rpc_stream(
153
+ self,
154
+ requests: AsyncIterable[
155
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
156
+ ],
157
+ ) -> AsyncIterator[
158
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
159
+ ]:
160
+ call = await self._client.open_call(self._service, "RpcStream")
161
+
162
+ async def encoded() -> AsyncIterator[bytes]:
163
+ async for request in requests:
164
+ yield request.SerializeToString(deterministic=True)
165
+
166
+ async for data in bidirectional_bytes(call, encoded()):
167
+ response = _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket()
168
+ response.ParseFromString(data)
169
+ yield response
170
+
171
+ async def do_nothing(
172
+ self, request: _google_protobuf_empty_pb2.Empty
173
+ ) -> _google_protobuf_empty_pb2.Empty:
174
+ call = await self._client.open_call(
175
+ self._service, "DoNothing", request.SerializeToString(deterministic=True)
176
+ )
177
+ try:
178
+ data = await call.receive()
179
+ if data is None:
180
+ raise CallProtocolError("missing unary response")
181
+ response = _google_protobuf_empty_pb2.Empty()
182
+ response.ParseFromString(data)
183
+ if await call.receive() is not None:
184
+ raise CallProtocolError("extra unary response")
185
+ return response
186
+ finally:
187
+ await call.aclose()
188
+
189
+
190
+ class EchoerServer(Protocol):
191
+ async def echo(
192
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
193
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
194
+ def echo_server_stream(
195
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
196
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
197
+ async def echo_client_stream(
198
+ self,
199
+ requests: AsyncIterator[
200
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
201
+ ],
202
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
203
+ def echo_bidi_stream(
204
+ self,
205
+ requests: AsyncIterator[
206
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
207
+ ],
208
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
209
+ def rpc_stream(
210
+ self,
211
+ requests: AsyncIterator[
212
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
213
+ ],
214
+ ) -> AsyncIterator[
215
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
216
+ ]: ...
217
+ async def do_nothing(
218
+ self, request: _google_protobuf_empty_pb2.Empty
219
+ ) -> _google_protobuf_empty_pb2.Empty: ...
220
+
221
+
222
+ def register_echoer(
223
+ registry: ServiceRegistry,
224
+ implementation: EchoerServer,
225
+ service: str = "echo.Echoer",
226
+ ) -> None:
227
+ async def echo_handler(call: Call) -> None:
228
+ first = await call.receive()
229
+ if first is None:
230
+ raise CallProtocolError("missing initial request")
231
+ request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
232
+ request.ParseFromString(first)
233
+ response = await implementation.echo(request)
234
+ await call.send(response.SerializeToString(deterministic=True))
235
+
236
+ registry.register(service, "Echo", echo_handler)
237
+
238
+ async def echo_server_stream_handler(call: Call) -> None:
239
+ first = await call.receive()
240
+ if first is None:
241
+ raise CallProtocolError("missing initial request")
242
+ request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
243
+ request.ParseFromString(first)
244
+ async for response in implementation.echo_server_stream(request):
245
+ await call.send(response.SerializeToString(deterministic=True))
246
+
247
+ registry.register(service, "EchoServerStream", echo_server_stream_handler)
248
+
249
+ async def echo_client_stream_handler(call: Call) -> None:
250
+ async def requests() -> AsyncIterator[
251
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
252
+ ]:
253
+ while True:
254
+ data = await call.receive()
255
+ if data is None:
256
+ return
257
+ request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
258
+ request.ParseFromString(data)
259
+ yield request
260
+
261
+ response = await implementation.echo_client_stream(requests())
262
+ await call.send(response.SerializeToString(deterministic=True))
263
+
264
+ registry.register(service, "EchoClientStream", echo_client_stream_handler)
265
+
266
+ async def echo_bidi_stream_handler(call: Call) -> None:
267
+ async def requests() -> AsyncIterator[
268
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
269
+ ]:
270
+ while True:
271
+ data = await call.receive()
272
+ if data is None:
273
+ return
274
+ request = _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg()
275
+ request.ParseFromString(data)
276
+ yield request
277
+
278
+ async for response in implementation.echo_bidi_stream(requests()):
279
+ await call.send(response.SerializeToString(deterministic=True))
280
+
281
+ registry.register(service, "EchoBidiStream", echo_bidi_stream_handler)
282
+
283
+ async def rpc_stream_handler(call: Call) -> None:
284
+ async def requests() -> AsyncIterator[
285
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
286
+ ]:
287
+ while True:
288
+ data = await call.receive()
289
+ if data is None:
290
+ return
291
+ request = _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket()
292
+ request.ParseFromString(data)
293
+ yield request
294
+
295
+ async for response in implementation.rpc_stream(requests()):
296
+ await call.send(response.SerializeToString(deterministic=True))
297
+
298
+ registry.register(service, "RpcStream", rpc_stream_handler)
299
+
300
+ async def do_nothing_handler(call: Call) -> None:
301
+ first = await call.receive()
302
+ if first is None:
303
+ raise CallProtocolError("missing initial request")
304
+ request = _google_protobuf_empty_pb2.Empty()
305
+ request.ParseFromString(first)
306
+ response = await implementation.do_nothing(request)
307
+ await call.send(response.SerializeToString(deterministic=True))
308
+
309
+ registry.register(service, "DoNothing", do_nothing_handler)
@@ -0,0 +1,85 @@
1
+ from collections.abc import AsyncIterable, AsyncIterator
2
+ from typing import Protocol
3
+
4
+ from google.protobuf import empty_pb2 as _google_protobuf_empty_pb2
5
+
6
+ from echo import (
7
+ echo_pb2 as _github_com_aperturerobotics_starpc_echo_echo_pb2,
8
+ )
9
+ from rpcstream import (
10
+ rpcstream_pb2 as _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2,
11
+ )
12
+ from starpc.client import Client
13
+ from starpc.server import ServiceRegistry
14
+ from starpc.service import ServiceDescriptor
15
+
16
+ ECHOER_SERVICE: ServiceDescriptor
17
+
18
+ class EchoerClient:
19
+ def __init__(self, client: Client, service: str | None = None) -> None: ...
20
+ async def echo(
21
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
22
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
23
+ def echo_server_stream(
24
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
25
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
26
+ async def echo_client_stream(
27
+ self,
28
+ requests: AsyncIterable[
29
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
30
+ ],
31
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
32
+ def echo_bidi_stream(
33
+ self,
34
+ requests: AsyncIterable[
35
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
36
+ ],
37
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
38
+ def rpc_stream(
39
+ self,
40
+ requests: AsyncIterable[
41
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
42
+ ],
43
+ ) -> AsyncIterator[
44
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
45
+ ]: ...
46
+ async def do_nothing(
47
+ self, request: _google_protobuf_empty_pb2.Empty
48
+ ) -> _google_protobuf_empty_pb2.Empty: ...
49
+
50
+ class EchoerServer(Protocol):
51
+ async def echo(
52
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
53
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
54
+ def echo_server_stream(
55
+ self, request: _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
56
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
57
+ async def echo_client_stream(
58
+ self,
59
+ requests: AsyncIterator[
60
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
61
+ ],
62
+ ) -> _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg: ...
63
+ def echo_bidi_stream(
64
+ self,
65
+ requests: AsyncIterator[
66
+ _github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg
67
+ ],
68
+ ) -> AsyncIterator[_github_com_aperturerobotics_starpc_echo_echo_pb2.EchoMsg]: ...
69
+ def rpc_stream(
70
+ self,
71
+ requests: AsyncIterator[
72
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
73
+ ],
74
+ ) -> AsyncIterator[
75
+ _github_com_aperturerobotics_starpc_rpcstream_rpcstream_pb2.RpcStreamPacket
76
+ ]: ...
77
+ async def do_nothing(
78
+ self, request: _google_protobuf_empty_pb2.Empty
79
+ ) -> _google_protobuf_empty_pb2.Empty: ...
80
+
81
+ def register_echoer(
82
+ registry: ServiceRegistry,
83
+ implementation: EchoerServer,
84
+ service: str = "echo.Echoer",
85
+ ) -> None: ...
@@ -0,0 +1,178 @@
1
+ //! Half-close tests for the generated Rust callers.
2
+ //!
3
+ //! The half-close is only a notice once the request is written: a caller that
4
+ //! half-closes after the remote ended the call must still read the buffered
5
+ //! responses and the call's outcome.
6
+
7
+ #[allow(dead_code)]
8
+ mod gen;
9
+
10
+ use std::sync::Arc;
11
+
12
+ use async_trait::async_trait;
13
+ use futures::StreamExt as _;
14
+ use prost::Message;
15
+ use starpc::client::OpenStream;
16
+ use starpc::testing::{create_pipe_default, create_test_pair, SingleInMemoryOpener};
17
+ use starpc::{Client, Error, Mux, PacketCodec, Result, Server, SrpcClient, Stream, StreamExt};
18
+ use tokio::task::JoinHandle;
19
+ use tokio_util::codec::FramedRead;
20
+
21
+ use gen::{EchoMsg, EchoerClient, EchoerClientImpl, EchoerHandler, EchoerServer};
22
+
23
+ const BODY_TXT: &str = "hello world via starpc half-close test";
24
+
25
+ /// Answers each streaming call once and completes it.
26
+ struct OnceEchoServer;
27
+
28
+ #[async_trait]
29
+ impl EchoerServer for OnceEchoServer {
30
+ async fn echo(&self, _context: &starpc::Context, request: EchoMsg) -> Result<EchoMsg> {
31
+ Ok(request)
32
+ }
33
+
34
+ async fn echo_server_stream(&self, request: EchoMsg, stream: Box<dyn Stream>) -> Result<()> {
35
+ stream.msg_send(&request).await
36
+ }
37
+
38
+ async fn echo_client_stream(&self, stream: &dyn Stream) -> Result<EchoMsg> {
39
+ // Reply to the first message without waiting for the half-close.
40
+ stream.msg_recv().await
41
+ }
42
+
43
+ async fn echo_bidi_stream(&self, _stream: Box<dyn Stream>) -> Result<()> {
44
+ Err(Error::Unimplemented)
45
+ }
46
+
47
+ async fn rpc_stream(&self, _stream: Box<dyn Stream>) -> Result<()> {
48
+ Err(Error::Unimplemented)
49
+ }
50
+
51
+ async fn do_nothing(
52
+ &self,
53
+ _context: &starpc::Context,
54
+ request: gen::Empty,
55
+ ) -> Result<gen::Empty> {
56
+ Ok(request)
57
+ }
58
+ }
59
+
60
+ /// Returns each new stream only after its transport has closed, so the
61
+ /// generated caller half-closes after the remote already ended the call.
62
+ struct EndedClient<T: OpenStream> {
63
+ client: SrpcClient<T>,
64
+ }
65
+
66
+ #[async_trait]
67
+ impl<T: OpenStream + 'static> Client for EndedClient<T> {
68
+ async fn exec_call<I, O>(&self, service: &str, method: &str, input: &I) -> Result<O>
69
+ where
70
+ I: Message + Send + Sync,
71
+ O: Message + Default,
72
+ {
73
+ self.client.exec_call(service, method, input).await
74
+ }
75
+
76
+ async fn new_stream(
77
+ &self,
78
+ service: &str,
79
+ method: &str,
80
+ first_msg: Option<&[u8]>,
81
+ ) -> Result<Box<dyn Stream>> {
82
+ let stream = self.client.new_stream(service, method, first_msg).await?;
83
+ // The transport close handler cancels the context after it settles
84
+ // the call's outcome and closes the writer.
85
+ stream.context().cancelled().await;
86
+ Ok(stream)
87
+ }
88
+ }
89
+
90
+ /// Returns one transport opener and the server task that its caller must join.
91
+ fn serve_once_echo() -> (SingleInMemoryOpener, JoinHandle<Result<()>>) {
92
+ let mux = Arc::new(Mux::new());
93
+ mux.register(Arc::new(EchoerHandler::new(OnceEchoServer)))
94
+ .unwrap();
95
+ let (opener, server_stream) = create_test_pair();
96
+ let server = Server::with_arc(mux);
97
+ let task = tokio::spawn(async move { server.handle_stream(server_stream).await });
98
+ (opener, task)
99
+ }
100
+
101
+ /// Returns a transport that closes after CallStart and the peer task to join after observation.
102
+ fn drop_after_request() -> (SingleInMemoryOpener, JoinHandle<()>) {
103
+ // Use the actual framing boundary to order peer closure after request admission.
104
+ let (client_stream, server_stream) = create_pipe_default();
105
+ let task = tokio::spawn(async move {
106
+ let mut framed = FramedRead::new(server_stream, PacketCodec::new());
107
+ let _ = framed.next().await;
108
+ });
109
+ (SingleInMemoryOpener::new(client_stream), task)
110
+ }
111
+
112
+ #[tokio::test]
113
+ async fn server_stream_completed_before_close_send() {
114
+ // Complete the server stream before the generated caller half-closes.
115
+ let (opener, server) = serve_once_echo();
116
+ let client = EchoerClientImpl::new(EndedClient {
117
+ client: SrpcClient::new(opener),
118
+ });
119
+ let request = EchoMsg {
120
+ body: BODY_TXT.into(),
121
+ };
122
+ let stream = client
123
+ .echo_server_stream(&request)
124
+ .await
125
+ .expect("server stream discarded after remote completion");
126
+
127
+ // Read the buffered response, then the clean completion.
128
+ let msg = stream.recv().await.expect("buffered response lost");
129
+ assert_eq!(msg.body, BODY_TXT);
130
+ let end = stream.recv().await;
131
+ assert!(
132
+ matches!(end, Err(Error::StreamClosed)),
133
+ "expected StreamClosed after completion, got {end:?}"
134
+ );
135
+ server.await.unwrap().unwrap();
136
+ }
137
+
138
+ #[tokio::test]
139
+ async fn client_stream_completed_before_close_send() {
140
+ let (opener, server) = serve_once_echo();
141
+ let client = EchoerClientImpl::new(SrpcClient::new(opener));
142
+ let stream = client.echo_client_stream().await.unwrap();
143
+
144
+ // Send the one request and wait for the call to end.
145
+ let request = EchoMsg {
146
+ body: BODY_TXT.into(),
147
+ };
148
+ stream.send(&request).await.unwrap();
149
+ stream.context().cancelled().await;
150
+
151
+ // The half-close fails, but the reply is still readable.
152
+ let msg = stream.close_and_recv().await.expect("buffered reply lost");
153
+ assert_eq!(msg.body, BODY_TXT);
154
+ server.await.unwrap().unwrap();
155
+ }
156
+
157
+ #[tokio::test]
158
+ async fn server_stream_transport_failure_before_close_send() {
159
+ let (opener, server) = drop_after_request();
160
+ let client = EchoerClientImpl::new(EndedClient {
161
+ client: SrpcClient::new(opener),
162
+ });
163
+ let request = EchoMsg {
164
+ body: BODY_TXT.into(),
165
+ };
166
+ let stream = client
167
+ .echo_server_stream(&request)
168
+ .await
169
+ .expect("server stream discarded after transport failure");
170
+
171
+ // Recv reports that the call ended without a verdict.
172
+ let end = stream.recv().await;
173
+ assert!(
174
+ matches!(end, Err(Error::ClosedBeforeCompletion)),
175
+ "expected ClosedBeforeCompletion, got {end:?}"
176
+ );
177
+ server.await.unwrap();
178
+ }
@@ -1,3 +1,5 @@
1
+ //! Echo transport endpoint used by cross-language integration checks.
2
+
1
3
  #[allow(dead_code)]
2
4
  mod gen;
3
5
 
@@ -9,21 +11,16 @@ use tokio::net::TcpListener;
9
11
 
10
12
  use gen::{EchoMsg, EchoerHandler, EchoerServer, Empty};
11
13
 
14
+ /// Implements each echo call shape through the supplied transport stream.
12
15
  struct EchoServerImpl;
13
16
 
14
17
  #[async_trait]
15
18
  impl EchoerServer for EchoServerImpl {
16
- async fn echo(&self, request: EchoMsg) -> Result<EchoMsg> {
17
- Ok(EchoMsg {
18
- body: request.body,
19
- })
19
+ async fn echo(&self, _context: &starpc::Context, request: EchoMsg) -> Result<EchoMsg> {
20
+ Ok(EchoMsg { body: request.body })
20
21
  }
21
22
 
22
- async fn echo_server_stream(
23
- &self,
24
- request: EchoMsg,
25
- stream: Box<dyn Stream>,
26
- ) -> Result<()> {
23
+ async fn echo_server_stream(&self, request: EchoMsg, stream: Box<dyn Stream>) -> Result<()> {
27
24
  for _ in 0..5 {
28
25
  let response = EchoMsg {
29
26
  body: request.body.clone(),
@@ -34,10 +31,7 @@ impl EchoerServer for EchoServerImpl {
34
31
  }
35
32
 
36
33
  async fn echo_client_stream(&self, stream: &dyn Stream) -> Result<EchoMsg> {
37
- match stream.msg_recv::<EchoMsg>().await {
38
- Ok(msg) => Ok(msg),
39
- Err(e) => Err(e),
40
- }
34
+ stream.msg_recv().await
41
35
  }
42
36
 
43
37
  async fn echo_bidi_stream(&self, stream: Box<dyn Stream>) -> Result<()> {
@@ -63,11 +57,12 @@ impl EchoerServer for EchoServerImpl {
63
57
  Err(Error::Unimplemented)
64
58
  }
65
59
 
66
- async fn do_nothing(&self, _request: Empty) -> Result<Empty> {
60
+ async fn do_nothing(&self, _context: &starpc::Context, _request: Empty) -> Result<Empty> {
67
61
  Ok(Empty {})
68
62
  }
69
63
  }
70
64
 
65
+ /// Publishes a bound address and keeps accepted connections under the server owner.
71
66
  #[tokio::main]
72
67
  async fn main() -> Result<()> {
73
68
  let listener = TcpListener::bind("127.0.0.1:0").await?;
@@ -77,11 +72,9 @@ async fn main() -> Result<()> {
77
72
  let mux = Arc::new(Mux::new());
78
73
  mux.register(Arc::new(EchoerHandler::new(EchoServerImpl)))?;
79
74
 
80
- loop {
81
- let (stream, _) = listener.accept().await?;
82
- let server = Server::with_arc(mux.clone());
83
- tokio::spawn(async move {
84
- let _ = server.handle_stream(stream).await;
85
- });
86
- }
75
+ // The server owns concurrent calls and their cancellation for this listener's lifetime.
76
+ let incoming = futures::stream::unfold(listener, |listener| async move {
77
+ Some((listener.accept().await.map(|(stream, _)| stream), listener))
78
+ });
79
+ Server::with_arc(mux).serve(Box::pin(incoming)).await
87
80
  }