starpc 0.51.0 → 0.52.1

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 (93) hide show
  1. package/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
  2. package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
  3. package/dist/echo/client-test.d.ts +1 -1
  4. package/dist/echo/client-test.js +110 -2
  5. package/dist/echo/echo_srpc.pb.d.ts +44 -1
  6. package/dist/echo/server.d.ts +9 -8
  7. package/dist/echo/server.js +6 -6
  8. package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
  9. package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
  10. package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
  11. package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
  12. package/dist/integration/cross-language/ts-client.js +50 -37
  13. package/dist/integration/cross-language/ts-server.js +1 -36
  14. package/dist/mock/mock_srpc.pb.d.ts +14 -1
  15. package/dist/rpcstream/rpcstream.d.ts +5 -1
  16. package/dist/rpcstream/rpcstream.js +75 -28
  17. package/dist/rpcstream/rpcstream.test.d.ts +1 -0
  18. package/dist/rpcstream/rpcstream.test.js +92 -0
  19. package/dist/srpc/channel.js +6 -3
  20. package/dist/srpc/channel.test.js +20 -1
  21. package/dist/srpc/client.js +18 -4
  22. package/dist/srpc/common-rpc.test.js +2 -0
  23. package/dist/srpc/handler.d.ts +12 -3
  24. package/dist/srpc/index.d.ts +2 -0
  25. package/dist/srpc/index.js +1 -0
  26. package/dist/srpc/invoker.d.ts +2 -1
  27. package/dist/srpc/invoker.js +2 -2
  28. package/dist/srpc/packet-codec.test.d.ts +1 -0
  29. package/dist/srpc/packet-codec.test.js +75 -0
  30. package/dist/srpc/packet.d.ts +1 -1
  31. package/dist/srpc/packet.js +11 -1
  32. package/dist/srpc/server-context.d.ts +11 -0
  33. package/dist/srpc/server-context.js +28 -0
  34. package/dist/srpc/server-rpc.js +3 -1
  35. package/dist/srpc/server.js +19 -6
  36. package/dist/srpc/server.test.js +78 -6
  37. package/dist/srpc/stream.d.ts +4 -1
  38. package/dist/srpc/stream.js +62 -3
  39. package/dist/srpc/stream.test.js +110 -1
  40. package/dist/srpc/termination.d.ts +27 -0
  41. package/dist/srpc/termination.js +56 -0
  42. package/dist/srpc/termination.test.d.ts +1 -0
  43. package/dist/srpc/termination.test.js +24 -0
  44. package/dist/srpc/watchdog.test.js +1 -0
  45. package/dist/testdata/packet-codec-vectors.json +64 -0
  46. package/echo/client-test.ts +124 -2
  47. package/echo/echo_pb2.py +40 -0
  48. package/echo/echo_pb2.pyi +13 -0
  49. package/echo/echo_srpc.pb.ts +74 -0
  50. package/echo/echo_srpc.py +306 -0
  51. package/echo/echo_srpc.pyi +85 -0
  52. package/echo/server.ts +24 -5
  53. package/go.mod +2 -2
  54. package/go.sum +14 -0
  55. package/integration/cross-language/go-client/main.go +79 -3
  56. package/integration/cross-language/python-client.py +146 -0
  57. package/integration/cross-language/python-server.py +140 -0
  58. package/integration/cross-language/run.bash +190 -65
  59. package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
  60. package/integration/cross-language/tcp-packet-stream.ts +121 -0
  61. package/integration/cross-language/ts-client.ts +62 -40
  62. package/integration/cross-language/ts-server.ts +1 -45
  63. package/mock/mock_pb2.py +38 -0
  64. package/mock/mock_pb2.pyi +11 -0
  65. package/mock/mock_srpc.pb.ts +19 -1
  66. package/mock/mock_srpc.py +71 -0
  67. package/mock/mock_srpc.pyi +27 -0
  68. package/package.json +20 -6
  69. package/srpc/__init__.py +0 -0
  70. package/srpc/channel.test.ts +21 -1
  71. package/srpc/channel.ts +7 -3
  72. package/srpc/client.ts +20 -4
  73. package/srpc/codec.rs +6 -0
  74. package/srpc/common-rpc.test.ts +2 -0
  75. package/srpc/handler.ts +54 -4
  76. package/srpc/index.ts +7 -0
  77. package/srpc/invoker.ts +23 -6
  78. package/srpc/packet-codec-vectors_test.go +195 -0
  79. package/srpc/packet-codec.test.ts +139 -0
  80. package/srpc/packet-rw.go +9 -2
  81. package/srpc/packet.ts +15 -2
  82. package/srpc/py.typed +0 -0
  83. package/srpc/rpcproto_pb2.py +40 -0
  84. package/srpc/rpcproto_pb2.pyi +40 -0
  85. package/srpc/server-context.ts +55 -0
  86. package/srpc/server-rpc.ts +4 -1
  87. package/srpc/server.test.ts +100 -5
  88. package/srpc/server.ts +22 -6
  89. package/srpc/stream.test.ts +132 -1
  90. package/srpc/stream.ts +65 -9
  91. package/srpc/termination.test.ts +30 -0
  92. package/srpc/termination.ts +70 -0
  93. package/srpc/watchdog.test.ts +1 -0
@@ -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: ...
package/echo/server.ts CHANGED
@@ -3,14 +3,15 @@ import { Message } from '@aptre/protobuf-es-lite'
3
3
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty'
4
4
  import { EchoMsg } from './echo.pb.js'
5
5
  import { Server } from '../srpc/server.js'
6
+ import type { ServerContext } from '../srpc/server-context.js'
6
7
  import { messagePushable, writeToPushable } from '../srpc/pushable.js'
7
8
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js'
8
9
  import { MessageStream } from '../srpc/message.js'
9
10
  import { handleRpcStream, RpcStreamHandler } from '../rpcstream/rpcstream.js'
10
- import { Echoer } from './echo_srpc.pb.js'
11
+ import type { EchoerHandler } from './echo_srpc.pb.js'
11
12
 
12
13
  // EchoServer implements the Echoer server.
13
- export class EchoerServer implements Echoer {
14
+ export class EchoerServer implements EchoerHandler {
14
15
  // proxyServer is the server used for RpcStream requests.
15
16
  private proxyServer?: Server
16
17
 
@@ -18,11 +19,19 @@ export class EchoerServer implements Echoer {
18
19
  this.proxyServer = proxyServer
19
20
  }
20
21
 
21
- public async Echo(request: EchoMsg): Promise<Message<EchoMsg>> {
22
+ public async Echo(
23
+ request: EchoMsg,
24
+ _abortSignal: AbortSignal,
25
+ _context: ServerContext,
26
+ ): Promise<Message<EchoMsg>> {
22
27
  return request
23
28
  }
24
29
 
25
- public async *EchoServerStream(request: EchoMsg): MessageStream<EchoMsg> {
30
+ public async *EchoServerStream(
31
+ request: EchoMsg,
32
+ _abortSignal: AbortSignal,
33
+ _context: ServerContext,
34
+ ): MessageStream<EchoMsg> {
26
35
  for (let i = 0; i < 5; i++) {
27
36
  yield request
28
37
  await new Promise((resolve) => setTimeout(resolve, 200))
@@ -31,6 +40,8 @@ export class EchoerServer implements Echoer {
31
40
 
32
41
  public async EchoClientStream(
33
42
  request: MessageStream<EchoMsg>,
43
+ _abortSignal: AbortSignal,
44
+ _context: ServerContext,
34
45
  ): Promise<Message<EchoMsg>> {
35
46
  // return the first message sent by the client.
36
47
  const message = await first(request)
@@ -42,6 +53,8 @@ export class EchoerServer implements Echoer {
42
53
 
43
54
  public EchoBidiStream(
44
55
  request: MessageStream<EchoMsg>,
56
+ _abortSignal: AbortSignal,
57
+ _context: ServerContext,
45
58
  ): MessageStream<EchoMsg> {
46
59
  // build result observable
47
60
  const result = messagePushable<EchoMsg>()
@@ -52,6 +65,8 @@ export class EchoerServer implements Echoer {
52
65
 
53
66
  public RpcStream(
54
67
  request: MessageStream<RpcStreamPacket>,
68
+ _abortSignal: AbortSignal,
69
+ _context: ServerContext,
55
70
  ): MessageStream<RpcStreamPacket> {
56
71
  return handleRpcStream(
57
72
  request[Symbol.asyncIterator](),
@@ -64,7 +79,11 @@ export class EchoerServer implements Echoer {
64
79
  )
65
80
  }
66
81
 
67
- public async DoNothing(): Promise<Empty> {
82
+ public async DoNothing(
83
+ _request: Empty,
84
+ _abortSignal: AbortSignal,
85
+ _context: ServerContext,
86
+ ): Promise<Empty> {
68
87
  return {}
69
88
  }
70
89
  }
package/go.mod CHANGED
@@ -3,7 +3,7 @@ module github.com/aperturerobotics/starpc
3
3
  go 1.25.0
4
4
 
5
5
  require (
6
- github.com/aperturerobotics/common v0.35.0 // latest
6
+ github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd // latest
7
7
  github.com/aperturerobotics/protobuf-go-lite v0.16.0 // latest
8
8
  github.com/aperturerobotics/util v1.34.9 // latest
9
9
  )
@@ -12,7 +12,7 @@ require (
12
12
  github.com/aperturerobotics/abseil-cpp v0.0.0-20260131110040-4bb56e2f9017 // aperture-2
13
13
  github.com/aperturerobotics/cli v1.1.0 // indirect
14
14
  github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b // indirect
15
- github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260712054757-d8078c296c17 // indirect
15
+ github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f // indirect
16
16
  github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1 // master
17
17
  github.com/aperturerobotics/json-iterator-lite v1.1.0 // indirect
18
18
  github.com/aperturerobotics/protobuf v0.0.0-20260203024654-8201686529c4 // wasi
package/go.sum CHANGED
@@ -6,10 +6,24 @@ github.com/aperturerobotics/common v0.34.4 h1:RQTOm6LxEnucXkED/kruhKKAOL6tbaPzMg
6
6
  github.com/aperturerobotics/common v0.34.4/go.mod h1:xnb1VBRs3x2o1PaKg0OlP7SqDaC9Hmrmt9xH85rOS+A=
7
7
  github.com/aperturerobotics/common v0.35.0 h1:VU4OPMXJyqT37lTOtNEMa3QLW5tR8YIBAKbjgxSzuZs=
8
8
  github.com/aperturerobotics/common v0.35.0/go.mod h1:xnb1VBRs3x2o1PaKg0OlP7SqDaC9Hmrmt9xH85rOS+A=
9
+ github.com/aperturerobotics/common v0.35.1-0.20260808094909-49d3232b4f83 h1:QkFoyeJ0QBw21a+1YR0kB+JaBSY2mHFiIGiFwWUJ/wQ=
10
+ github.com/aperturerobotics/common v0.35.1-0.20260808094909-49d3232b4f83/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
11
+ github.com/aperturerobotics/common v0.35.1-0.20260808095531-8f6fd4367389 h1:J/vc+AKFh8hIVJJ2LrAJVwEgIYvFeQ5XANg3dK3OnYk=
12
+ github.com/aperturerobotics/common v0.35.1-0.20260808095531-8f6fd4367389/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
13
+ github.com/aperturerobotics/common v0.35.1-0.20260808100945-fe2b561bec93 h1:Bc7ThfLt5ekHZmOH9vGW0PiiuVUl1R+vneFH/G0j5D4=
14
+ github.com/aperturerobotics/common v0.35.1-0.20260808100945-fe2b561bec93/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
15
+ github.com/aperturerobotics/common v0.35.1-0.20260808101203-caeb6303ba93 h1:JoERBAX9HuOzopNLZQCT851YnYz9h9nYSpEe6XjcP0o=
16
+ github.com/aperturerobotics/common v0.35.1-0.20260808101203-caeb6303ba93/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
17
+ github.com/aperturerobotics/common v0.35.1-0.20260808102028-33a08259dd38 h1:c3IXUqJHt1EAlCyD0d0IrI7YYf4txwICtoh+ylQV6tU=
18
+ github.com/aperturerobotics/common v0.35.1-0.20260808102028-33a08259dd38/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
19
+ github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd h1:BTfB1V2X+u3/56negSjrx+AWg+Uk84rFFryvpGTj7f4=
20
+ github.com/aperturerobotics/common v0.35.1-0.20260808123156-c77af26f63bd/go.mod h1:sPjrQZeS/yr9kBro4rN2BrYvG9TCn1eIGk7dMH/E2NQ=
9
21
  github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b h1:MMk+AbWPCMfqc2Lm2bJl8lCCsuvlM2+C0CP10fpE3bc=
10
22
  github.com/aperturerobotics/go-protoc-gen-prost v0.0.0-20260705010911-9f53feac967b/go.mod h1:OBb/beWmr/pDIZAUfi86j/4tBh2v5ctTxKMqSnh9c/4=
11
23
  github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260712054757-d8078c296c17 h1:WT1YmZjJf8fUF1zKbeNNKXFA+t8Wtbr8AVMAmdKcrUU=
12
24
  github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260712054757-d8078c296c17/go.mod h1:vEq8i7EKb32+KXGtIEZjjhNns+BdsL2dUMw4uhy3578=
25
+ github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f h1:UszdpmKBfQd0LdOZjypeRGrHz2tcPpmKpw6SV3DYyN0=
26
+ github.com/aperturerobotics/go-protoc-wasi v0.0.0-20260808023521-7b1595380c3f/go.mod h1:vEq8i7EKb32+KXGtIEZjjhNns+BdsL2dUMw4uhy3578=
13
27
  github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1 h1:q2qkdNOrnhAFf6B7TBeUoAYstYWxPvozN+3vJi2jgp0=
14
28
  github.com/aperturerobotics/go-websocket v1.8.15-0.20260619192713-a096778f08c1/go.mod h1:9KnSGuqxSXbdB/Oi0I6vvfPLkclfJwMGAQaDDGVgGow=
15
29
  github.com/aperturerobotics/json-iterator-lite v1.1.0 h1:ZLLqHhHTKYlmCAP873ras2e/yVU/THtwC+ji3tXLMMg=
@@ -6,19 +6,23 @@ import (
6
6
  "io"
7
7
  "net"
8
8
  "os"
9
+ "strings"
9
10
 
10
11
  "github.com/aperturerobotics/starpc/echo"
12
+ "github.com/aperturerobotics/starpc/rpcstream"
11
13
  "github.com/aperturerobotics/starpc/srpc"
12
14
  )
13
15
 
14
16
  const bodyTxt = "hello world via starpc cross-language e2e test"
15
17
 
16
18
  func main() {
17
- if len(os.Args) <= 1 {
18
- fmt.Fprintf(os.Stderr, "usage: go-client <addr>\n")
19
+ nested := len(os.Args) >= 3 && os.Args[1] == "--nested"
20
+ nestedRelease := len(os.Args) >= 3 && os.Args[1] == "--nested-release"
21
+ if len(os.Args) != 2 && !nested && !nestedRelease {
22
+ fmt.Fprintf(os.Stderr, "usage: go-client [--nested] <addr>\n")
19
23
  os.Exit(1)
20
24
  }
21
- addr := os.Args[1]
25
+ addr := os.Args[len(os.Args)-1]
22
26
  openStream := func(ctx context.Context, msgHandler srpc.PacketDataHandler, closeHandler srpc.CloseHandler) (srpc.PacketWriter, error) {
23
27
  conn, err := net.Dial("tcp", addr) //nolint:gosec
24
28
  if err != nil {
@@ -48,6 +52,12 @@ func main() {
48
52
  fmt.Fprintf(os.Stderr, "bidi stream test failed: %v\n", err)
49
53
  os.Exit(1)
50
54
  }
55
+ if nested || nestedRelease {
56
+ if err := testNested(ctx, echoClient, nestedRelease); err != nil {
57
+ fmt.Fprintf(os.Stderr, "nested lifecycle test failed: %v\n", err)
58
+ os.Exit(1)
59
+ }
60
+ }
51
61
  fmt.Println("All tests passed.")
52
62
  }
53
63
 
@@ -143,3 +153,69 @@ func testBidiStream(ctx context.Context, client echo.SRPCEchoerClient) error {
143
153
  fmt.Println(" PASSED")
144
154
  return nil
145
155
  }
156
+
157
+ func testNested(ctx context.Context, parent echo.SRPCEchoerClient, release bool) error {
158
+ proxy := rpcstream.NewRpcStreamClient(parent.RpcStream, "test", true)
159
+ if err := testUnary(ctx, echo.NewSRPCEchoerClient(proxy)); err != nil {
160
+ return fmt.Errorf("nested unary: %w", err)
161
+ }
162
+
163
+ if release {
164
+ missing := rpcstream.NewRpcStreamClient(parent.RpcStream, "missing", true)
165
+ var missingOut echo.EchoMsg
166
+ if err := missing.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{}, &missingOut); err == nil || !strings.Contains(err.Error(), "unknown component: missing") {
167
+ return fmt.Errorf("unknown component returned wrong result: %v", err)
168
+ }
169
+ }
170
+
171
+ stream, err := proxy.NewStream(ctx, "echo.Echoer", "EchoBidiStream", nil)
172
+ if err != nil {
173
+ return fmt.Errorf("nested bidi: %w", err)
174
+ }
175
+ if err := stream.MsgSend(&echo.EchoMsg{Body: "nested later data"}); err != nil {
176
+ return fmt.Errorf("nested later data: %w", err)
177
+ }
178
+ if err := stream.Close(); err != nil {
179
+ return fmt.Errorf("nested cancel: %w", err)
180
+ }
181
+
182
+ var terminal echo.EchoMsg
183
+ if err := proxy.ExecCall(ctx, "missing.Service", "Missing", &echo.EchoMsg{}, &terminal); err == nil || !strings.Contains(err.Error(), "missing.Service") {
184
+ return fmt.Errorf("unknown nested method returned wrong result: %v", err)
185
+ }
186
+
187
+ if release {
188
+ var terminalError echo.EchoMsg
189
+ if err := proxy.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{Body: "__nested_error__"}, &terminalError); err == nil || !strings.Contains(err.Error(), "nested terminal error") {
190
+ return fmt.Errorf("terminal nested error returned wrong result: %v", err)
191
+ }
192
+ }
193
+
194
+ if release {
195
+ releaseClient := rpcstream.NewRpcStreamClient(parent.RpcStream, "release", true)
196
+ var released echo.EchoMsg
197
+ err := releaseClient.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{Body: "__nested_release__"}, &released)
198
+ if err == nil || (!strings.Contains(err.Error(), "stream closed before the remote reported completion") && !strings.Contains(err.Error(), "EOF") && !strings.Contains(err.Error(), "canceled")) {
199
+ return fmt.Errorf("release during active call returned wrong result: %v", err)
200
+ }
201
+ status, err := parent.Echo(ctx, &echo.EchoMsg{Body: "__nested_release_status__"})
202
+ if err != nil {
203
+ return fmt.Errorf("release completion status failed: %w", err)
204
+ }
205
+ if status.GetBody() != "released" {
206
+ return fmt.Errorf("release completion returned %q", status.GetBody())
207
+ }
208
+ if err := releaseClient.ExecCall(ctx, "echo.Echoer", "Echo", &echo.EchoMsg{}, &released); err == nil || !strings.Contains(err.Error(), "unknown component: release") {
209
+ return fmt.Errorf("released component returned wrong result: %v", err)
210
+ }
211
+ }
212
+
213
+ direct, err := rpcstream.OpenRpcStream(ctx, parent.RpcStream, "test", true)
214
+ if err != nil {
215
+ return fmt.Errorf("nested direct open: %w", err)
216
+ }
217
+ if err := direct.Close(); err != nil {
218
+ return fmt.Errorf("nested abrupt close: %w", err)
219
+ }
220
+ return nil
221
+ }
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ # The checkout supplies generated message packages outside the installed wheel.
4
+ import asyncio
5
+ import sys
6
+ from collections.abc import AsyncIterator
7
+ from pathlib import Path
8
+
9
+ from google.protobuf import empty_pb2
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
12
+
13
+ from echo import echo_pb2
14
+ from echo.echo_srpc import EchoerClient
15
+ from starpc.call import CallCancelledError, RemoteCallError
16
+ from starpc.client import Client
17
+ from starpc.stream import open_tcp_stream
18
+
19
+
20
+ async def main() -> None:
21
+ args = sys.argv[1:]
22
+ nested = args[:1] in (["--nested"], ["--nested-release"])
23
+ strict_nested = args[:1] == ["--nested-release"]
24
+ if nested:
25
+ args = args[1:]
26
+ if len(args) != 1 or ":" not in args[0]:
27
+ raise ValueError("usage: python-client.py [--nested] host:port")
28
+ host, port_text = args[0].rsplit(":", 1)
29
+ port = int(port_text)
30
+ raw_client = Client(lambda: open_tcp_stream(host, port))
31
+ client = EchoerClient(raw_client)
32
+ body = "hello world via python"
33
+ response = await client.echo(echo_pb2.EchoMsg(body=body))
34
+ if response.body != body:
35
+ raise RuntimeError(f"unexpected unary response: {response.body!r}")
36
+ empty_response = await client.echo(echo_pb2.EchoMsg())
37
+ if empty_response.body:
38
+ raise RuntimeError(f"unexpected empty unary response: {empty_response.body!r}")
39
+ received = [
40
+ message
41
+ async for message in client.echo_server_stream(echo_pb2.EchoMsg(body=body))
42
+ ]
43
+ if len(received) != 5 or any(message.body != body for message in received):
44
+ raise RuntimeError("unexpected server-stream response")
45
+ client_stream = await client.echo_client_stream(_one_request(body))
46
+ if client_stream.body != body:
47
+ raise RuntimeError("unexpected client-stream response")
48
+ bidi = client.echo_bidi_stream(_one_request(body))
49
+ initial = await bidi.__anext__()
50
+ if initial.body != "hello from server":
51
+ raise RuntimeError("unexpected bidi initial response")
52
+ echoed = await bidi.__anext__()
53
+ if echoed.body != body:
54
+ raise RuntimeError("unexpected bidi echo")
55
+ try:
56
+ await bidi.__anext__()
57
+ except StopAsyncIteration:
58
+ pass
59
+ else:
60
+ raise RuntimeError("bidi stream did not terminate")
61
+ empty = await client.do_nothing(empty_pb2.Empty())
62
+ if empty.ByteSize() != 0:
63
+ raise RuntimeError("unexpected DoNothing response")
64
+
65
+ unknown = await raw_client.open_call("missing.Service", "Missing")
66
+ try:
67
+ await unknown.finish()
68
+ try:
69
+ await unknown.receive()
70
+ except RemoteCallError:
71
+ pass
72
+ else:
73
+ raise RuntimeError("unknown method did not return a remote error")
74
+ finally:
75
+ await unknown.aclose()
76
+
77
+ cancelled = await raw_client.open_call("echo.Echoer", "EchoBidiStream")
78
+ await cancelled.cancel()
79
+ try:
80
+ await cancelled.wait_closed()
81
+ except CallCancelledError:
82
+ pass
83
+ await cancelled.aclose()
84
+ if nested:
85
+ await run_nested_test(client, strict_nested)
86
+ print("All tests passed.")
87
+
88
+
89
+ async def run_nested_test(client: EchoerClient, strict: bool = False) -> None:
90
+ from starpc.rpcstream import (
91
+ RpcStreamRemoteError,
92
+ build_rpc_stream_open_stream,
93
+ )
94
+
95
+ nested_client = Client(build_rpc_stream_open_stream("test", client.rpc_stream))
96
+ nested_echo = EchoerClient(nested_client)
97
+ body = "hello world via nested python"
98
+ response = await nested_echo.echo(echo_pb2.EchoMsg(body=body))
99
+ if response.body != body:
100
+ raise RuntimeError(f"unexpected nested unary response: {response.body!r}")
101
+
102
+ if strict:
103
+ try:
104
+ await nested_echo.echo(echo_pb2.EchoMsg(body="__nested_error__"))
105
+ except RemoteCallError:
106
+ pass
107
+ else:
108
+ raise RuntimeError("terminal nested error unexpectedly succeeded")
109
+
110
+ if strict:
111
+ missing = Client(build_rpc_stream_open_stream("missing", client.rpc_stream))
112
+ try:
113
+ await missing.open_call("echo.Echoer", "Echo")
114
+ except RpcStreamRemoteError:
115
+ pass
116
+ else:
117
+ raise RuntimeError("unknown component unexpectedly succeeded")
118
+
119
+ terminal = await nested_client.open_call("missing.Service", "Missing")
120
+ try:
121
+ await terminal.receive()
122
+ except RemoteCallError:
123
+ pass
124
+ else:
125
+ raise RuntimeError("unknown nested method unexpectedly succeeded")
126
+ finally:
127
+ await terminal.aclose()
128
+
129
+ cancelled = await nested_client.open_call("echo.Echoer", "EchoBidiStream")
130
+ await cancelled.send(
131
+ echo_pb2.EchoMsg(body="nested later data").SerializeToString(deterministic=True)
132
+ )
133
+ await cancelled.cancel()
134
+ try:
135
+ await cancelled.wait_closed()
136
+ except CallCancelledError:
137
+ pass
138
+ await cancelled.aclose()
139
+
140
+
141
+ async def _one_request(body: str) -> AsyncIterator[echo_pb2.EchoMsg]:
142
+ yield echo_pb2.EchoMsg(body=body)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ asyncio.run(main())
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ # The checkout supplies generated message packages outside the installed wheel.
4
+ import asyncio
5
+ import signal
6
+ import sys
7
+ from collections.abc import AsyncIterator
8
+ from pathlib import Path
9
+
10
+ from google.protobuf import empty_pb2
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
13
+
14
+ from echo import echo_pb2
15
+ from echo.echo_srpc import register_echoer
16
+ from rpcstream import rpcstream_pb2
17
+ from starpc.rpcstream import ComponentRegistry, handle_rpc_stream
18
+ from starpc.server import Server, ServiceRegistry
19
+ from starpc.stream import TCPStreamServer
20
+
21
+
22
+ class Implementation:
23
+ def __init__(self, components: ComponentRegistry) -> None:
24
+ self._components = components
25
+ self._release_task: asyncio.Task[None] | None = None
26
+ self._release_complete = asyncio.Event()
27
+
28
+ async def _release_component(self) -> None:
29
+ try:
30
+ await self._components.unregister("release")
31
+ finally:
32
+ self._release_complete.set()
33
+
34
+ async def wait_for_release(self) -> None:
35
+ task = self._release_task
36
+ if task is not None:
37
+ await task
38
+
39
+ async def echo(self, request: echo_pb2.EchoMsg) -> echo_pb2.EchoMsg:
40
+ if request.body == "__nested_error__":
41
+ raise RuntimeError("nested terminal error")
42
+ if request.body == "__nested_release__":
43
+ if self._release_task is None:
44
+ self._release_task = asyncio.create_task(self._release_component())
45
+ await asyncio.Future()
46
+ if request.body == "__nested_release_status__":
47
+ await self._release_complete.wait()
48
+ await self.wait_for_release()
49
+ return echo_pb2.EchoMsg(body="released")
50
+ return echo_pb2.EchoMsg(body=request.body)
51
+
52
+ async def echo_server_stream(
53
+ self, request: echo_pb2.EchoMsg
54
+ ) -> AsyncIterator[echo_pb2.EchoMsg]:
55
+ for _ in range(5):
56
+ yield echo_pb2.EchoMsg(body=request.body)
57
+ await asyncio.sleep(0.2)
58
+
59
+ async def echo_client_stream(
60
+ self, requests: AsyncIterator[echo_pb2.EchoMsg]
61
+ ) -> echo_pb2.EchoMsg:
62
+ async for request in requests:
63
+ return request
64
+ return echo_pb2.EchoMsg()
65
+
66
+ async def echo_bidi_stream(
67
+ self, requests: AsyncIterator[echo_pb2.EchoMsg]
68
+ ) -> AsyncIterator[echo_pb2.EchoMsg]:
69
+ yield echo_pb2.EchoMsg(body="hello from server")
70
+ async for request in requests:
71
+ yield request
72
+
73
+ def rpc_stream(
74
+ self, requests: AsyncIterator[rpcstream_pb2.RpcStreamPacket]
75
+ ) -> AsyncIterator[rpcstream_pb2.RpcStreamPacket]:
76
+ return handle_rpc_stream(requests, self._components)
77
+
78
+ async def do_nothing(self, request: empty_pb2.Empty) -> empty_pb2.Empty:
79
+ return request
80
+
81
+
82
+ async def main() -> None:
83
+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
84
+ registry = ServiceRegistry()
85
+ components = ComponentRegistry()
86
+ await components.register("test", Server(registry))
87
+ await components.register("release", Server(registry))
88
+ implementation = Implementation(components)
89
+ register_echoer(registry, implementation)
90
+ server = TCPStreamServer()
91
+ await server.start("127.0.0.1", port)
92
+ host, bound_port = server.address
93
+ print(f"LISTENING {host}:{bound_port}", flush=True)
94
+ rpc_server = Server(registry)
95
+ tasks: set[asyncio.Task[None]] = set()
96
+ errors: list[BaseException] = []
97
+ stopping = asyncio.Event()
98
+ loop = asyncio.get_running_loop()
99
+ for sig in (signal.SIGINT, signal.SIGTERM):
100
+ loop.add_signal_handler(sig, stopping.set)
101
+
102
+ def observe_task(task: asyncio.Task[None]) -> None:
103
+ tasks.discard(task)
104
+ if not task.cancelled() and (error := task.exception()) is not None:
105
+ errors.append(error)
106
+
107
+ try:
108
+ while not stopping.is_set():
109
+ accepting = asyncio.create_task(server.accept())
110
+ stopped = asyncio.create_task(stopping.wait())
111
+ done, _ = await asyncio.wait(
112
+ {accepting, stopped}, return_when=asyncio.FIRST_COMPLETED
113
+ )
114
+ if stopped in done:
115
+ accepting.cancel()
116
+ await asyncio.gather(accepting, return_exceptions=True)
117
+ break
118
+ stopped.cancel()
119
+ await asyncio.gather(stopped, return_exceptions=True)
120
+ stream = accepting.result()
121
+ task = asyncio.create_task(rpc_server.serve(stream))
122
+ tasks.add(task)
123
+ task.add_done_callback(observe_task)
124
+ finally:
125
+ await server.aclose()
126
+ for task in tasks:
127
+ task.cancel()
128
+ await asyncio.gather(*tasks, return_exceptions=True)
129
+ await implementation.wait_for_release()
130
+ await components.unregister("test")
131
+ await components.unregister("release")
132
+ if errors:
133
+ raise ExceptionGroup("cross-language server tasks failed", errors)
134
+ if components._components or components._retired:
135
+ raise RuntimeError("nested component cleanup is incomplete")
136
+ print("NESTED_CLEAN", flush=True)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ asyncio.run(main())