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,154 @@
1
+ import net from 'net'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { pushable } from 'it-pushable'
4
+
5
+ import { tcpSocketToPacketStream } from './tcp-packet-stream.js'
6
+
7
+ const sockets: net.Socket[] = []
8
+ const servers: net.Server[] = []
9
+
10
+ afterEach(() => {
11
+ for (const socket of sockets) socket.destroy()
12
+ for (const server of servers) server.close()
13
+ sockets.length = 0
14
+ servers.length = 0
15
+ })
16
+
17
+ describe('TCP PacketStream lifecycle', () => {
18
+ it('settles a blocked source when the peer ends cleanly', async () => {
19
+ const { local, peer } = await connectSockets()
20
+ const stream = tcpSocketToPacketStream(local)
21
+ const pending = stream.source.next()
22
+
23
+ peer.end()
24
+
25
+ await expect(settleBeforeTimeout(pending)).resolves.toEqual({
26
+ done: true,
27
+ value: undefined,
28
+ })
29
+ })
30
+
31
+ it('reports a truncated frame when the peer ends', async () => {
32
+ const { local, peer } = await connectSockets()
33
+ const stream = tcpSocketToPacketStream(local)
34
+ const pending = stream.source.next()
35
+
36
+ peer.end(Buffer.from([4, 0, 0, 0, 10, 1]))
37
+
38
+ await expect(settleBeforeTimeout(pending)).rejects.toThrow(
39
+ 'truncated packet frame',
40
+ )
41
+ })
42
+
43
+ it('rejects a blocked source with the abort error', async () => {
44
+ const { local } = await connectSockets()
45
+ const stream = tcpSocketToPacketStream(local)
46
+ const pending = stream.source.next()
47
+ const error = new Error('stopped')
48
+
49
+ stream.abort(error)
50
+
51
+ await expect(settleBeforeTimeout(pending)).rejects.toBe(error)
52
+ })
53
+
54
+ it('settles a blocked sink when closed', async () => {
55
+ const { local } = await connectSockets()
56
+ const stream = tcpSocketToPacketStream(local)
57
+ const input = pushable<Uint8Array>({ objectMode: true })
58
+ const pending = stream.sink(input)
59
+
60
+ await stream.close()
61
+
62
+ await expect(pending).resolves.toBeUndefined()
63
+ })
64
+
65
+ it('settles an in-flight socket write when closed', async () => {
66
+ const { local } = await connectSockets()
67
+ const stream = tcpSocketToPacketStream(local)
68
+ const input = pushable<Uint8Array>({ objectMode: true })
69
+ const writeStarted = Promise.withResolvers<void>()
70
+ vi.spyOn(local, 'write').mockImplementation(((
71
+ _data: Uint8Array,
72
+ _callback: (error?: Error) => void,
73
+ ) => {
74
+ writeStarted.resolve()
75
+ return true
76
+ }) as typeof local.write)
77
+ input.push(new Uint8Array([1]))
78
+ const pending = stream.sink(input)
79
+ await writeStarted.promise
80
+
81
+ await stream.close()
82
+
83
+ await expect(settleBeforeTimeout(pending)).resolves.toBeUndefined()
84
+ })
85
+
86
+ it('rejects a blocked sink with the abort error', async () => {
87
+ const { local } = await connectSockets()
88
+ const stream = tcpSocketToPacketStream(local)
89
+ const input = pushable<Uint8Array>({ objectMode: true })
90
+ const pending = stream.sink(input)
91
+ const error = new Error('stopped')
92
+
93
+ stream.abort(error)
94
+
95
+ await expect(pending).rejects.toBe(error)
96
+ })
97
+
98
+ it('does not write ready input after close', async () => {
99
+ const { local, peer } = await connectSockets()
100
+ const stream = tcpSocketToPacketStream(local)
101
+ const input = pushable<Uint8Array>({ objectMode: true })
102
+ const received: Buffer[] = []
103
+ peer.on('data', (data) => {
104
+ if (typeof data !== 'string') received.push(data)
105
+ })
106
+ input.push(new Uint8Array([1]))
107
+
108
+ const pending = stream.sink(input)
109
+ await stream.close()
110
+ await pending
111
+ await new Promise<void>((resolve) => peer.once('close', () => resolve()))
112
+
113
+ expect(received).toEqual([])
114
+ })
115
+ })
116
+
117
+ async function connectSockets(): Promise<{
118
+ local: net.Socket
119
+ peer: net.Socket
120
+ }> {
121
+ const accepted = Promise.withResolvers<net.Socket>()
122
+ const server = net.createServer((socket) => accepted.resolve(socket))
123
+ servers.push(server)
124
+ await new Promise<void>((resolve, reject) => {
125
+ server.once('error', reject)
126
+ server.listen(0, '127.0.0.1', resolve)
127
+ })
128
+ const address = server.address() as net.AddressInfo
129
+ const local = net.connect(address.port, address.address)
130
+ await new Promise<void>((resolve, reject) => {
131
+ local.once('connect', resolve)
132
+ local.once('error', reject)
133
+ })
134
+ const peer = await accepted.promise
135
+ sockets.push(local, peer)
136
+ return { local, peer }
137
+ }
138
+
139
+ async function settleBeforeTimeout<T>(promise: Promise<T>): Promise<T> {
140
+ let timeout: ReturnType<typeof setTimeout> | undefined
141
+ try {
142
+ return await Promise.race([
143
+ promise,
144
+ new Promise<never>((_resolve, reject) => {
145
+ timeout = setTimeout(
146
+ () => reject(new Error('source stayed blocked')),
147
+ 500,
148
+ )
149
+ }),
150
+ ])
151
+ } finally {
152
+ clearTimeout(timeout)
153
+ }
154
+ }
@@ -0,0 +1,121 @@
1
+ import net from 'net'
2
+ import { pipe } from 'it-pipe'
3
+ import { pushable } from 'it-pushable'
4
+ import type { Source } from 'it-stream-types'
5
+
6
+ import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
7
+ import {
8
+ parseLengthPrefixTransform,
9
+ prependLengthPrefixTransform,
10
+ } from '../../srpc/packet.js'
11
+ import type { PacketStream } from '../../srpc/stream.js'
12
+ import {
13
+ closeIterator,
14
+ sourceIterator,
15
+ TerminationGate,
16
+ } from '../../srpc/termination.js'
17
+
18
+ // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
19
+ export function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
20
+ const sourceTermination = new TerminationGate()
21
+ const sinkTermination = new TerminationGate()
22
+ const bytes = pushable<Uint8Array>({ objectMode: true })
23
+ let bytesEnded = false
24
+
25
+ const endBytes = (error?: Error) => {
26
+ if (bytesEnded) return
27
+ bytesEnded = true
28
+ bytes.end(error)
29
+ }
30
+ const terminate = (error?: Error): boolean => {
31
+ const first = sourceTermination.terminate(error)
32
+ sinkTermination.terminate(error)
33
+ endBytes(error)
34
+ return first
35
+ }
36
+
37
+ socket.on('data', (data: Buffer) => {
38
+ if (!bytesEnded) bytes.push(new Uint8Array(data))
39
+ })
40
+ socket.on('end', () => endBytes())
41
+ socket.on('error', (error) => terminate(error))
42
+ socket.on('close', () => {
43
+ endBytes()
44
+ sinkTermination.terminate()
45
+ })
46
+
47
+ const close = async (): Promise<void> => {
48
+ if (terminate()) socket.destroy()
49
+ }
50
+ const abort = (error: Error): void => {
51
+ if (terminate(error)) socket.destroy(error)
52
+ }
53
+
54
+ return {
55
+ close,
56
+ abort,
57
+ source: (async function* () {
58
+ const packets = pipe(
59
+ bytes,
60
+ parseLengthPrefixTransform(),
61
+ combineUint8ArrayListTransform(),
62
+ )[Symbol.asyncIterator]()
63
+ try {
64
+ while (true) {
65
+ const next = await sourceTermination.next(packets)
66
+ if ('terminated' in next) {
67
+ if (next.error) throw next.error
68
+ return
69
+ }
70
+ if ('error' in next) throw next.error
71
+ if (next.result.done) return
72
+ yield next.result.value
73
+ }
74
+ } finally {
75
+ closeIterator(packets)
76
+ }
77
+ })(),
78
+ sink: async (source: Source<Uint8Array>): Promise<void> => {
79
+ const iterator = sourceIterator(
80
+ pipe(source, prependLengthPrefixTransform()),
81
+ )
82
+ try {
83
+ while (true) {
84
+ const next = await sinkTermination.next(iterator)
85
+ if ('terminated' in next) {
86
+ if (next.error) throw next.error
87
+ return
88
+ }
89
+ if ('error' in next) throw next.error
90
+ if (next.result.done) {
91
+ socket.end()
92
+ return
93
+ }
94
+ const data =
95
+ next.result.value instanceof Uint8Array
96
+ ? next.result.value
97
+ : next.result.value.subarray()
98
+ const written = await sinkTermination.wait(
99
+ new Promise<void>((resolve, reject) => {
100
+ socket.write(data, (error) => {
101
+ if (error) reject(error)
102
+ else resolve()
103
+ })
104
+ }),
105
+ )
106
+ if ('terminated' in written) {
107
+ if (written.error) throw written.error
108
+ return
109
+ }
110
+ if ('error' in written) throw written.error
111
+ }
112
+ } catch (err) {
113
+ const error = err instanceof Error ? err : new Error(String(err))
114
+ abort(error)
115
+ throw error
116
+ } finally {
117
+ closeIterator(iterator)
118
+ }
119
+ },
120
+ }
121
+ }
@@ -1,59 +1,71 @@
1
1
  import net from 'net'
2
- import { pipe } from 'it-pipe'
3
2
  import { pushable } from 'it-pushable'
4
3
  import { Client } from '../../srpc/client.js'
5
4
  import {
6
- parseLengthPrefixTransform,
7
- prependLengthPrefixTransform,
8
- } from '../../srpc/packet.js'
9
- import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
10
- import { runClientTest } from '../../echo/index.js'
5
+ runClientTest,
6
+ runAbortControllerTest,
7
+ runRpcStreamTest,
8
+ } from '../../echo/client-test.js'
9
+ import { EchoerClient } from '../../echo/echo_srpc.pb.js'
11
10
  import type { OpenStreamFunc, PacketStream } from '../../srpc/stream.js'
12
- import type { Source } from 'it-stream-types'
11
+ import { tcpSocketToPacketStream } from './tcp-packet-stream.js'
13
12
 
14
- // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
15
- function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
16
- const socketSource = async function* (): AsyncGenerator<Uint8Array> {
17
- const source = pushable<Uint8Array>({ objectMode: true })
18
- socket.on('data', (data: Buffer) => {
19
- source.push(new Uint8Array(data))
20
- })
21
- socket.on('end', () => source.end())
22
- socket.on('error', (err) => source.end(err))
23
- socket.on('close', () => source.end())
24
- yield* pipe(
25
- source,
26
- parseLengthPrefixTransform(),
27
- combineUint8ArrayListTransform(),
28
- )
13
+ async function runEchoBidiStreamTest(client: Client): Promise<void> {
14
+ const request = pushable<{ body: string }>({ objectMode: true })
15
+ const stream = new EchoerClient(client).EchoBidiStream(request)
16
+ const iterator = stream[Symbol.asyncIterator]()
17
+
18
+ const initial = await iterator.next()
19
+ if (initial.done || initial.value.body !== 'hello from server') {
20
+ throw new Error('expected initial bidi message "hello from server"')
21
+ }
22
+
23
+ const body = 'hello from TypeScript bidi client'
24
+ request.push({ body })
25
+ request.end()
26
+
27
+ const echo = await iterator.next()
28
+ if (echo.done || echo.value.body !== body) {
29
+ throw new Error(`expected bidi echo ${JSON.stringify(body)}`)
29
30
  }
30
31
 
31
- return {
32
- source: socketSource(),
33
- sink: async (source: Source<Uint8Array>): Promise<void> => {
34
- for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
35
- const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
36
- await new Promise<void>((resolve, reject) => {
37
- socket.write(data, (err) => {
38
- if (err) reject(err)
39
- else resolve()
40
- })
41
- })
42
- }
43
- socket.end()
44
- },
32
+ const terminal = await iterator.next()
33
+ if (!terminal.done) {
34
+ throw new Error('expected bidi stream to terminate after input closes')
45
35
  }
46
36
  }
47
37
 
38
+ function parseAddr(addr: string): { host: string; port: number } {
39
+ const match = /^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/.exec(addr)
40
+ if (!match) {
41
+ throw new Error(`invalid host:port address: ${addr}`)
42
+ }
43
+
44
+ const port = Number(match[3])
45
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
46
+ throw new Error(`invalid port: ${match[3]}`)
47
+ }
48
+
49
+ return { host: match[1] ?? match[2], port }
50
+ }
51
+
48
52
  async function main() {
49
- const addr = process.argv[2]
53
+ const args = process.argv.slice(2)
54
+ const lifecycle = args.includes('lifecycle')
55
+ const nested = args.includes('--nested') || args.includes('--nested-release')
56
+ const nestedRelease = args.includes('--nested-release')
57
+ const addr = args.find(
58
+ (arg) =>
59
+ arg !== 'lifecycle' && arg !== '--nested' && arg !== '--nested-release',
60
+ )
50
61
  if (!addr) {
51
- console.error('usage: ts-client <host:port>')
62
+ console.error(
63
+ 'usage: ts-client [--nested] [--nested-release] [lifecycle] <host:port>',
64
+ )
52
65
  process.exit(1)
53
66
  }
54
67
 
55
- const [host, portStr] = addr.split(':')
56
- const port = parseInt(portStr, 10)
68
+ const { host, port } = parseAddr(addr)
57
69
  const openStream: OpenStreamFunc = async (): Promise<PacketStream> => {
58
70
  const { promise, resolve, reject } = Promise.withResolvers<PacketStream>()
59
71
  const socket = net.connect(port, host, () => {
@@ -66,6 +78,16 @@ async function main() {
66
78
  const client = new Client(openStream)
67
79
  console.log('Running client test via TCP...')
68
80
  await runClientTest(client)
81
+ console.log('Running EchoBidiStream test via TCP...')
82
+ await runEchoBidiStreamTest(client)
83
+ if (lifecycle) {
84
+ console.log('Running abort controller test via TCP...')
85
+ await runAbortControllerTest(client)
86
+ }
87
+ if (nested) {
88
+ console.log('Running RpcStream test via TCP...')
89
+ await runRpcStreamTest(client, nestedRelease)
90
+ }
69
91
  console.log('All tests passed.')
70
92
  }
71
93
 
@@ -1,53 +1,9 @@
1
1
  import net from 'net'
2
- import { pipe } from 'it-pipe'
3
- import { pushable } from 'it-pushable'
4
- import type { Source } from 'it-stream-types'
5
2
 
6
3
  import { createMux, createHandler, Server } from '../../srpc/index.js'
7
- import {
8
- parseLengthPrefixTransform,
9
- prependLengthPrefixTransform,
10
- } from '../../srpc/packet.js'
11
- import { combineUint8ArrayListTransform } from '../../srpc/array-list.js'
12
4
  import { EchoerServer } from '../../echo/index.js'
13
5
  import { EchoerDefinition } from '../../echo/echo_srpc.pb.js'
14
- import type { PacketStream } from '../../srpc/stream.js'
15
-
16
- // tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
17
- // Each Uint8Array in source/sink is one packet (no length prefix).
18
- function tcpSocketToPacketStream(socket: net.Socket): PacketStream {
19
- // Source: read from socket, strip length prefix, yield individual packets.
20
- const socketSource = async function* (): AsyncGenerator<Uint8Array> {
21
- const source = pushable<Uint8Array>({ objectMode: true })
22
- socket.on('data', (data: Buffer) => {
23
- source.push(new Uint8Array(data))
24
- })
25
- socket.on('end', () => source.end())
26
- socket.on('error', (err) => source.end(err))
27
- socket.on('close', () => source.end())
28
- yield* pipe(
29
- source,
30
- parseLengthPrefixTransform(),
31
- combineUint8ArrayListTransform(),
32
- )
33
- }
34
-
35
- return {
36
- source: socketSource(),
37
- sink: async (source: Source<Uint8Array>): Promise<void> => {
38
- for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
39
- const data = chunk instanceof Uint8Array ? chunk : chunk.subarray()
40
- await new Promise<void>((resolve, reject) => {
41
- socket.write(data, (err) => {
42
- if (err) reject(err)
43
- else resolve()
44
- })
45
- })
46
- }
47
- socket.end()
48
- },
49
- }
50
- }
6
+ import { tcpSocketToPacketStream } from './tcp-packet-stream.js'
51
7
 
52
8
  const mux = createMux()
53
9
  const server = new Server(mux.lookupMethod)
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: github.com/aperturerobotics/starpc/mock/mock.proto
5
+ # Protobuf Python Version: 6.33.4
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 33,
16
+ 4,
17
+ '',
18
+ 'github.com/aperturerobotics/starpc/mock/mock.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2github.com/aperturerobotics/starpc/mock/mock.proto\x12\x08\x65\x32\x65.mock\"\x17\n\x07MockMsg\x12\x0c\n\x04\x62ody\x18\x01 \x01(\t2;\n\x04Mock\x12\x33\n\x0bMockRequest\x12\x11.e2e.mock.MockMsg\x1a\x11.e2e.mock.MockMsgb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.aperturerobotics.starpc.mock.mock_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_MOCKMSG']._serialized_start=64
35
+ _globals['_MOCKMSG']._serialized_end=87
36
+ _globals['_MOCK']._serialized_start=89
37
+ _globals['_MOCK']._serialized_end=148
38
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,11 @@
1
+ from google.protobuf import descriptor as _descriptor
2
+ from google.protobuf import message as _message
3
+ from typing import ClassVar as _ClassVar, Optional as _Optional
4
+
5
+ DESCRIPTOR: _descriptor.FileDescriptor
6
+
7
+ class MockMsg(_message.Message):
8
+ __slots__ = ("body",)
9
+ BODY_FIELD_NUMBER: _ClassVar[int]
10
+ body: str
11
+ def __init__(self, body: _Optional[str] = ...) -> None: ...
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.49.21
4
+ // protoc-gen-starpc-cpp version: v0.52.2-0.20260903015026-8f335488f9dc+dirty
5
5
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
6
6
 
7
7
  #include "mock_srpc.pb.hpp"
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only && cgo
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.49.21
4
+ // protoc-gen-starpc-cpp version: v0.52.2-0.20260903015026-8f335488f9dc+dirty
5
5
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
6
6
 
7
7
  #pragma once
@@ -1,9 +1,9 @@
1
1
  // Code generated by protoc-gen-starpc-rust. DO NOT EDIT.
2
- // protoc-gen-starpc-rust version: v0.49.21
2
+ // protoc-gen-starpc-rust version: (devel)
3
3
  // source: github.com/aperturerobotics/starpc/mock/mock.proto
4
4
 
5
5
  #[allow(unused_imports)]
6
- use starpc::StreamExt;
6
+ use starpc::StreamExt as _;
7
7
 
8
8
  /// Service ID for Mock.
9
9
  pub const MOCK_SERVICE_ID: &str = "e2e.mock.Mock";
@@ -30,7 +30,9 @@ impl<C: starpc::Client> MockClientImpl<C> {
30
30
  #[starpc::async_trait]
31
31
  impl<C: starpc::Client + 'static> MockClient for MockClientImpl<C> {
32
32
  async fn mock_request(&self, request: &MockMsg) -> starpc::Result<MockMsg> {
33
- self.client.exec_call("e2e.mock.Mock", "MockRequest", request).await
33
+ self.client
34
+ .exec_call("e2e.mock.Mock", "MockRequest", request)
35
+ .await
34
36
  }
35
37
  }
36
38
 
@@ -41,9 +43,7 @@ pub trait MockServer: Send + Sync {
41
43
  async fn mock_request(&self, request: MockMsg) -> starpc::Result<MockMsg>;
42
44
  }
43
45
 
44
- const MOCK_METHOD_IDS: &[&str] = &[
45
- "MockRequest",
46
- ];
46
+ const MOCK_METHOD_IDS: &[&str] = &["MockRequest"];
47
47
 
48
48
  /// Handler for Mock.
49
49
  pub struct MockHandler<S: MockServer> {
@@ -53,7 +53,9 @@ pub struct MockHandler<S: MockServer> {
53
53
  impl<S: MockServer + 'static> MockHandler<S> {
54
54
  /// Creates a new handler wrapping the server implementation.
55
55
  pub fn new(server: S) -> Self {
56
- Self { server: std::sync::Arc::new(server) }
56
+ Self {
57
+ server: std::sync::Arc::new(server),
58
+ }
57
59
  }
58
60
 
59
61
  /// Creates a new handler with a shared server.
@@ -76,7 +78,7 @@ impl<S: MockServer + 'static> starpc::Invoker for MockHandler<S> {
76
78
  Ok(r) => r,
77
79
  Err(e) => return (true, Err(e)),
78
80
  };
79
- match self.server.mock_request(request).await {
81
+ match <S as MockServer>::mock_request(self.server.as_ref(), request).await {
80
82
  Ok(response) => {
81
83
  if let Err(e) = stream.msg_send(&response).await {
82
84
  return (true, Err(e));
@@ -100,4 +102,3 @@ impl<S: MockServer + 'static> starpc::Handler for MockHandler<S> {
100
102
  MOCK_METHOD_IDS
101
103
  }
102
104
  }
103
-
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from mock import (
6
+ mock_pb2 as _github_com_aperturerobotics_starpc_mock_mock_pb2,
7
+ )
8
+ from starpc.call import Call, CallProtocolError
9
+ from starpc.client import Client
10
+ from starpc.server import ServiceRegistry
11
+ from starpc.service import MethodDescriptor, ServiceDescriptor
12
+
13
+ MOCK_SERVICE = ServiceDescriptor(
14
+ "e2e.mock.Mock",
15
+ (
16
+ MethodDescriptor(
17
+ "MockRequest",
18
+ _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg,
19
+ _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg,
20
+ False,
21
+ False,
22
+ ),
23
+ ),
24
+ )
25
+
26
+
27
+ class MockClient:
28
+ def __init__(self, client: Client, service: str | None = None) -> None:
29
+ self._client = client
30
+ self._service = service or "e2e.mock.Mock"
31
+
32
+ async def mock_request(
33
+ self, request: _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg
34
+ ) -> _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg:
35
+ call = await self._client.open_call(
36
+ self._service, "MockRequest", request.SerializeToString(deterministic=True)
37
+ )
38
+ try:
39
+ data = await call.receive()
40
+ if data is None:
41
+ raise CallProtocolError("missing unary response")
42
+ response = _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg()
43
+ response.ParseFromString(data)
44
+ if await call.receive() is not None:
45
+ raise CallProtocolError("extra unary response")
46
+ return response
47
+ finally:
48
+ await call.aclose()
49
+
50
+
51
+ class MockServer(Protocol):
52
+ async def mock_request(
53
+ self, request: _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg
54
+ ) -> _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg: ...
55
+
56
+
57
+ def register_mock(
58
+ registry: ServiceRegistry,
59
+ implementation: MockServer,
60
+ service: str = "e2e.mock.Mock",
61
+ ) -> None:
62
+ async def mock_request_handler(call: Call) -> None:
63
+ first = await call.receive()
64
+ if first is None:
65
+ raise CallProtocolError("missing initial request")
66
+ request = _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg()
67
+ request.ParseFromString(first)
68
+ response = await implementation.mock_request(request)
69
+ await call.send(response.SerializeToString(deterministic=True))
70
+
71
+ registry.register(service, "MockRequest", mock_request_handler)
@@ -0,0 +1,27 @@
1
+ from typing import Protocol
2
+
3
+ from mock import (
4
+ mock_pb2 as _github_com_aperturerobotics_starpc_mock_mock_pb2,
5
+ )
6
+ from starpc.client import Client
7
+ from starpc.server import ServiceRegistry
8
+ from starpc.service import ServiceDescriptor
9
+
10
+ MOCK_SERVICE: ServiceDescriptor
11
+
12
+ class MockClient:
13
+ def __init__(self, client: Client, service: str | None = None) -> None: ...
14
+ async def mock_request(
15
+ self, request: _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg
16
+ ) -> _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg: ...
17
+
18
+ class MockServer(Protocol):
19
+ async def mock_request(
20
+ self, request: _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg
21
+ ) -> _github_com_aperturerobotics_starpc_mock_mock_pb2.MockMsg: ...
22
+
23
+ def register_mock(
24
+ registry: ServiceRegistry,
25
+ implementation: MockServer,
26
+ service: str = "e2e.mock.Mock",
27
+ ) -> None: ...