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
package/srpc/invoker.ts CHANGED
@@ -2,6 +2,7 @@ import { Sink, Source } from 'it-stream-types'
2
2
  import { pushable } from 'it-pushable'
3
3
  import { pipe } from 'it-pipe'
4
4
  import type { MethodDefinition } from './definition.js'
5
+ import type { ServerContext } from './server-context.js'
5
6
  import { InvokeFn } from './handler.js'
6
7
  import {
7
8
  buildDecodeMessageTransform,
@@ -13,10 +14,26 @@ import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite'
13
14
 
14
15
  // MethodProto is a function which matches one of the RPC signatures.
15
16
  export type MethodProto<R extends Message<R>, O extends Message<O>> =
16
- | ((request: R, invocation?: AbortSignal) => Promise<O>)
17
- | ((request: R, invocation?: AbortSignal) => AsyncIterable<O>)
18
- | ((request: AsyncIterable<R>, invocation?: AbortSignal) => Promise<O>)
19
- | ((request: AsyncIterable<R>, invocation?: AbortSignal) => AsyncIterable<O>)
17
+ | ((
18
+ request: R,
19
+ abortSignal: AbortSignal,
20
+ context: ServerContext,
21
+ ) => Promise<O>)
22
+ | ((
23
+ request: R,
24
+ abortSignal: AbortSignal,
25
+ context: ServerContext,
26
+ ) => AsyncIterable<O>)
27
+ | ((
28
+ request: AsyncIterable<R>,
29
+ abortSignal: AbortSignal,
30
+ context: ServerContext,
31
+ ) => Promise<O>)
32
+ | ((
33
+ request: AsyncIterable<R>,
34
+ abortSignal: AbortSignal,
35
+ context: ServerContext,
36
+ ) => AsyncIterable<O>)
20
37
 
21
38
  // createInvokeFn builds an InvokeFn from a method definition and a function prototype.
22
39
  export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
@@ -32,7 +49,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
32
49
  return async (
33
50
  dataSource: Source<Uint8Array>,
34
51
  dataSink: Sink<Source<Uint8Array>>,
35
- invocation?: AbortSignal,
52
+ context: ServerContext,
36
53
  ) => {
37
54
  // responseSink is a Sink for response messages.
38
55
  const responseSink = pushable<O>({
@@ -67,7 +84,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
67
84
 
68
85
  // Call the implementation.
69
86
  try {
70
- const responseObj = methodProto(requestArg, invocation)
87
+ const responseObj = methodProto(requestArg, context.signal, context)
71
88
  if (!responseObj) {
72
89
  throw new Error('return value was undefined')
73
90
  }
@@ -0,0 +1,195 @@
1
+ package srpc
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/binary"
6
+ "encoding/hex"
7
+ "encoding/json"
8
+ "errors"
9
+ "io"
10
+ "os"
11
+ "testing"
12
+ )
13
+
14
+ type packetCodecVector struct {
15
+ Name string `json:"name"`
16
+ PacketHex string `json:"packet_hex"`
17
+ FrameHex string `json:"frame_hex"`
18
+ }
19
+
20
+ func TestPacketCodecGoldenVectors(t *testing.T) {
21
+ data, err := os.ReadFile("../testdata/packet-codec-vectors.json")
22
+ if err != nil {
23
+ t.Fatal(err)
24
+ }
25
+ var document struct {
26
+ Cases []packetCodecVector `json:"cases"`
27
+ }
28
+ if err := json.Unmarshal(data, &document); err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ for _, tc := range document.Cases {
32
+ if tc.PacketHex == "" || tc.FrameHex == "" {
33
+ continue
34
+ }
35
+ t.Run(tc.Name, func(t *testing.T) {
36
+ packet := goldenPacket(t, tc.Name)
37
+ packetData, err := packet.MarshalVT()
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ if want := decodeHex(t, tc.PacketHex); !bytes.Equal(packetData, want) {
42
+ t.Fatalf("packet = %x, want %x", packetData, want)
43
+ }
44
+ stream := &packetTestStream{}
45
+ if err := NewPacketReadWriter(stream).WritePacket(packet); err != nil {
46
+ t.Fatal(err)
47
+ }
48
+ if want := decodeHex(t, tc.FrameHex); !bytes.Equal(stream.writes, want) {
49
+ t.Fatalf("frame = %x, want %x", stream.writes, want)
50
+ }
51
+ })
52
+ }
53
+ }
54
+
55
+ func goldenPacket(t *testing.T, name string) *Packet {
56
+ t.Helper()
57
+ switch name {
58
+ case "call_start_data":
59
+ return NewCallStartPacket("svc", "method", []byte("abc"), false)
60
+ case "call_start_absent_empty":
61
+ return NewCallStartPacket("svc", "method", nil, false)
62
+ case "call_start_present_empty":
63
+ return NewCallStartPacket("svc", "method", nil, true)
64
+ case "call_data_terminal":
65
+ return NewCallDataPacket([]byte("out"), false, true, nil)
66
+ case "call_data_error":
67
+ return NewCallDataPacket(nil, false, false, errors.New("failed"))
68
+ case "call_cancel":
69
+ return NewCallCancelPacket()
70
+ default:
71
+ t.Fatalf("unknown golden packet %q", name)
72
+ return nil
73
+ }
74
+ }
75
+
76
+ func decodeHex(t *testing.T, value string) []byte {
77
+ t.Helper()
78
+ data, err := hex.DecodeString(value)
79
+ if err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ return data
83
+ }
84
+
85
+ func framePacket(t *testing.T, packet *Packet) []byte {
86
+ t.Helper()
87
+ packetSize := packet.SizeVT()
88
+ if packetSize > maxMessageSize {
89
+ t.Fatalf("packet size %d exceeds maximum %d", packetSize, maxMessageSize)
90
+ }
91
+ frame := make([]byte, 4+packetSize)
92
+ binary.LittleEndian.PutUint32(frame, uint32(packetSize)) //nolint:gosec // bounded by maxMessageSize
93
+ if _, err := packet.MarshalToSizedBufferVT(frame[4:]); err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ return frame
97
+ }
98
+
99
+ type packetTestStream struct {
100
+ reads bytes.Buffer
101
+ writes []byte
102
+ maxRead int
103
+ }
104
+
105
+ func (s *packetTestStream) Read(data []byte) (int, error) {
106
+ if s.maxRead > 0 && len(data) > s.maxRead {
107
+ data = data[:s.maxRead]
108
+ }
109
+ return s.reads.Read(data)
110
+ }
111
+
112
+ func (s *packetTestStream) Write(data []byte) (int, error) {
113
+ s.writes = append(s.writes, data...)
114
+ return len(data), nil
115
+ }
116
+
117
+ func (s *packetTestStream) Close() error { return nil }
118
+
119
+ func TestPacketCodecReadFragmentsAndCoalesces(t *testing.T) {
120
+ first := framePacket(t, NewCallCancelPacket())
121
+ second := framePacket(t, NewCallDataPacket([]byte("x"), false, true, nil))
122
+ input := append(first, second...)
123
+ stream := &packetTestStream{reads: *bytes.NewBuffer(input)}
124
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
125
+ var count int
126
+ err := NewPacketReadWriter(stream).ReadToHandler(func(data []byte) error {
127
+ count++
128
+ return decode(data)
129
+ })
130
+ if err != nil || count != 2 {
131
+ t.Fatalf("ReadToHandler() = %v, packets=%d", err, count)
132
+ }
133
+ }
134
+
135
+ func TestPacketCodecReadEveryFragmentBoundary(t *testing.T) {
136
+ frame := framePacket(t, NewCallCancelPacket())
137
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
138
+ for size := 1; size <= len(frame); size++ {
139
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame), maxRead: size}
140
+ var count int
141
+ err := NewPacketReadWriter(stream).ReadToHandler(func(data []byte) error {
142
+ count++
143
+ return decode(data)
144
+ })
145
+ if err != nil || count != 1 {
146
+ t.Fatalf("chunk %d: err=%v count=%d", size, err, count)
147
+ }
148
+ }
149
+ }
150
+
151
+ func TestPacketCodecRejectsInvalidPrefixesAndMalformedPacket(t *testing.T) {
152
+ for name, frame := range map[string][]byte{
153
+ "zero": {0, 0, 0, 0},
154
+ "oversized": {0x81, 0x96, 0x98, 0x00},
155
+ } {
156
+ t.Run(name, func(t *testing.T) {
157
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
158
+ if err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error { return nil }); err == nil {
159
+ t.Fatal("accepted invalid prefix")
160
+ }
161
+ })
162
+ }
163
+ frame := []byte{3, 0, 0, 0, 0x0a, 0x01, 0xff}
164
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
165
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
166
+ if err := NewPacketReadWriter(stream).ReadToHandler(decode); err == nil {
167
+ t.Fatal("accepted malformed protobuf")
168
+ }
169
+ }
170
+
171
+ func TestPacketCodecCleanEOFReturnsNil(t *testing.T) {
172
+ stream := &packetTestStream{reads: *bytes.NewBuffer(nil)}
173
+ if err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error { return nil }); err != nil {
174
+ t.Fatalf("clean EOF: %v", err)
175
+ }
176
+ }
177
+
178
+ func TestPacketCodecTruncatedBodyAtEOF(t *testing.T) {
179
+ frame := []byte{4, 0, 0, 0, 0x0a, 0x01}
180
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
181
+ var count int
182
+ err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error {
183
+ count++
184
+ return nil
185
+ })
186
+ if err != io.ErrUnexpectedEOF || count != 0 {
187
+ t.Fatalf("err=%v count=%d", err, count)
188
+ }
189
+ }
190
+
191
+ func TestPacketCodecWriteRejectsZeroPacket(t *testing.T) {
192
+ if err := NewPacketReadWriter(&packetTestStream{}).WritePacket(&Packet{}); err == nil {
193
+ t.Fatal("accepted zero-size packet")
194
+ }
195
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import vectors from '../testdata/packet-codec-vectors.json'
3
+ import { Packet } from './rpcproto.pb.js'
4
+ import {
5
+ decodePacketSource,
6
+ encodePacketSource,
7
+ lengthPrefixDecode,
8
+ prependLengthPrefixTransform,
9
+ uint32LEDecode,
10
+ } from './packet.js'
11
+
12
+ const bytes = (hex: string) =>
13
+ Uint8Array.from(hex.match(/../g) ?? [], (b) => parseInt(b, 16))
14
+ const hex = (
15
+ data: Uint8Array | { subarray: (start?: number, end?: number) => Uint8Array },
16
+ ) => Buffer.from(data.subarray()).toString('hex')
17
+ const collect = async <T>(source: AsyncIterable<T> | Iterable<T>) => {
18
+ const out: T[] = []
19
+ for await (const value of source) out.push(value)
20
+ return out
21
+ }
22
+
23
+ type ValidVector = (typeof vectors.cases)[number] & {
24
+ packet_hex: string
25
+ frame_hex: string
26
+ }
27
+ const validCases: ValidVector[] = vectors.cases.filter(
28
+ (entry): entry is ValidVector => Boolean(entry.packet_hex && entry.frame_hex),
29
+ )
30
+
31
+ describe('packet codec golden vectors', () => {
32
+ it.each(validCases)(
33
+ '$name has exact protobuf and frame bytes',
34
+ async (entry) => {
35
+ const packet = Packet.fromBinary(bytes(entry.packet_hex))
36
+ const encoded = (
37
+ await collect(
38
+ encodePacketSource(
39
+ (async function* () {
40
+ yield packet
41
+ })(),
42
+ ),
43
+ )
44
+ )[0]
45
+ expect(hex(encoded)).toBe(entry.packet_hex)
46
+
47
+ const framed = (
48
+ await collect(
49
+ prependLengthPrefixTransform()(
50
+ (async function* () {
51
+ yield encoded
52
+ })(),
53
+ ),
54
+ )
55
+ )[0]
56
+ expect(hex(framed)).toBe(entry.frame_hex)
57
+ },
58
+ )
59
+
60
+ it('rejects zero and oversized encoded chunks', async () => {
61
+ const zero = (async function* () {
62
+ yield new Uint8Array()
63
+ })()
64
+ await expect(collect(prependLengthPrefixTransform()(zero))).rejects.toThrow(
65
+ 'invalid packet length',
66
+ )
67
+ const oversized = (async function* () {
68
+ yield new Uint8Array(10_000_001)
69
+ })()
70
+ await expect(
71
+ collect(prependLengthPrefixTransform()(oversized)),
72
+ ).rejects.toThrow('invalid packet length')
73
+ })
74
+
75
+ it('rejects zero and oversized lengths, and truncated bodies', async () => {
76
+ const zero = (async function* () {
77
+ yield bytes('00000000')
78
+ })()
79
+ await expect(
80
+ collect(lengthPrefixDecode(zero, uint32LEDecode)),
81
+ ).rejects.toThrow('invalid packet length')
82
+ const oversized = (async function* () {
83
+ yield bytes('81969800')
84
+ })()
85
+ await expect(
86
+ collect(lengthPrefixDecode(oversized, uint32LEDecode)),
87
+ ).rejects.toThrow('invalid packet length')
88
+ const truncated = (async function* () {
89
+ yield bytes('040000000a01')
90
+ })()
91
+ await expect(
92
+ collect(lengthPrefixDecode(truncated, uint32LEDecode)),
93
+ ).rejects.toThrow('truncated packet frame')
94
+ })
95
+
96
+ it('decodes fragmented and coalesced frames', async () => {
97
+ const frames = validCases.map((entry) => bytes(entry.frame_hex))
98
+ const combined = new Uint8Array(
99
+ frames.reduce((n, frame) => n + frame.length, 0),
100
+ )
101
+ let offset = 0
102
+ for (const frame of frames) {
103
+ combined.set(frame, offset)
104
+ offset += frame.length
105
+ }
106
+ const payloads = await collect(
107
+ lengthPrefixDecode(
108
+ (async function* () {
109
+ yield combined.subarray(0, 3)
110
+ yield combined.subarray(3, 11)
111
+ yield combined.subarray(11)
112
+ })(),
113
+ uint32LEDecode,
114
+ ),
115
+ )
116
+ expect(payloads.map(hex)).toEqual(
117
+ validCases.map((entry) => entry.packet_hex),
118
+ )
119
+ const decoded = await collect(
120
+ decodePacketSource(payloads.map((payload) => payload.slice())),
121
+ )
122
+ expect(decoded).toHaveLength(validCases.length)
123
+ })
124
+
125
+ it('rejects malformed protobuf and incomplete frame prefix', async () => {
126
+ const malformed = vectors.cases.find(
127
+ (entry) => entry.name === 'malformed_complete',
128
+ )!
129
+ expect(() =>
130
+ Packet.fromBinary(bytes(malformed.frame_hex!.slice(8))),
131
+ ).toThrow()
132
+ const incomplete = (async function* () {
133
+ yield bytes('010203')
134
+ })()
135
+ await expect(
136
+ collect(lengthPrefixDecode(incomplete, uint32LEDecode)),
137
+ ).rejects.toThrow('truncated packet frame')
138
+ })
139
+ })
package/srpc/packet-rw.go CHANGED
@@ -65,7 +65,10 @@ func (r *PacketReadWriter) WritePacket(p *Packet) error {
65
65
  defer r.writeMtx.Unlock()
66
66
 
67
67
  msgSize := p.SizeVT()
68
- if msgSize < 0 || msgSize > maxMessageSize {
68
+ if msgSize <= 0 {
69
+ return errors.New("unexpected zero message size")
70
+ }
71
+ if msgSize > maxMessageSize {
69
72
  return errors.Errorf("message size %v greater than maximum %v", msgSize, maxMessageSize)
70
73
  }
71
74
 
@@ -163,7 +166,11 @@ func (r *PacketReadWriter) ReadToHandler(cb PacketDataHandler) error {
163
166
  }
164
167
  }
165
168
 
166
- // closed
169
+ // closed: a clean frame boundary has no buffered bytes; otherwise EOF
170
+ // ended a prefix or packet body before completion.
171
+ if currLen != 0 || r.buf.Len() != 0 {
172
+ return io.ErrUnexpectedEOF
173
+ }
167
174
  return nil
168
175
  }
169
176
 
package/srpc/packet.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  buildEncodeMessageTransform,
8
8
  } from './message.js'
9
9
 
10
+ const MAX_MESSAGE_SIZE = 10_000_000
11
+
10
12
  // decodePacketSource decodes packets from a binary data stream.
11
13
  export const decodePacketSource = buildDecodeMessageTransform<Packet>(Packet)
12
14
 
@@ -39,6 +41,9 @@ export async function* lengthPrefixEncode(
39
41
  for await (const chunk of source) {
40
42
  // Encode the length of the chunk.
41
43
  const length = chunk instanceof Uint8Array ? chunk.length : chunk.byteLength
44
+ if (length === 0 || length > MAX_MESSAGE_SIZE) {
45
+ throw RangeError(`invalid packet length: ${length}`)
46
+ }
42
47
  const lengthEncoded = lengthEncoder(length)
43
48
 
44
49
  // Concatenate the length prefix and the data.
@@ -50,7 +55,7 @@ export async function* lengthPrefixEncode(
50
55
  export async function* lengthPrefixDecode(
51
56
  source: Source<Uint8Array | Uint8ArrayList>,
52
57
  lengthDecoder: typeof uint32LEDecode,
53
- ) {
58
+ ): AsyncGenerator<Uint8ArrayList> {
54
59
  const buffer = new Uint8ArrayList()
55
60
 
56
61
  for await (const chunk of source) {
@@ -59,18 +64,26 @@ export async function* lengthPrefixDecode(
59
64
  // Continue extracting messages while buffer contains enough data for decoding.
60
65
  while (buffer.length >= lengthDecoder.bytes) {
61
66
  const messageLength = lengthDecoder(buffer)
67
+ if (messageLength === 0 || messageLength > MAX_MESSAGE_SIZE) {
68
+ throw RangeError(`invalid packet length: ${messageLength}`)
69
+ }
62
70
  const totalLength = lengthDecoder.bytes + messageLength
63
71
 
64
72
  if (buffer.length < totalLength) break // Wait for more data if the full message hasn't arrived.
65
73
 
66
74
  // Extract the message excluding the length prefix.
67
- const message = buffer.sublist(lengthDecoder.bytes, totalLength)
75
+ const message = new Uint8ArrayList(
76
+ buffer.slice(lengthDecoder.bytes, totalLength),
77
+ )
68
78
  yield message
69
79
 
70
80
  // Remove the processed message from the buffer.
71
81
  buffer.consume(totalLength)
72
82
  }
73
83
  }
84
+ if (buffer.length !== 0) {
85
+ throw new RangeError('truncated packet frame')
86
+ }
74
87
  }
75
88
 
76
89
  // prependLengthPrefixTransform adds a length prefix to a message source.
package/srpc/py.typed ADDED
File without changes
@@ -0,0 +1,40 @@
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/srpc/rpcproto.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/srpc/rpcproto.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'\n6github.com/aperturerobotics/starpc/srpc/rpcproto.proto\x12\x04srpc\"s\n\x06Packet\x12%\n\ncall_start\x18\x01 \x01(\x0b\x32\x0f.srpc.CallStartH\x00\x12#\n\tcall_data\x18\x02 \x01(\x0b\x32\x0e.srpc.CallDataH\x00\x12\x15\n\x0b\x63\x61ll_cancel\x18\x03 \x01(\x08H\x00\x42\x06\n\x04\x62ody\"X\n\tCallStart\x12\x13\n\x0brpc_service\x18\x01 \x01(\t\x12\x12\n\nrpc_method\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x14\n\x0c\x64\x61ta_is_zero\x18\x04 \x01(\x08\"O\n\x08\x43\x61llData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x14\n\x0c\x64\x61ta_is_zero\x18\x02 \x01(\x08\x12\x10\n\x08\x63omplete\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\tb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.aperturerobotics.starpc.srpc.rpcproto_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_PACKET']._serialized_start=64
35
+ _globals['_PACKET']._serialized_end=179
36
+ _globals['_CALLSTART']._serialized_start=181
37
+ _globals['_CALLSTART']._serialized_end=269
38
+ _globals['_CALLDATA']._serialized_start=271
39
+ _globals['_CALLDATA']._serialized_end=350
40
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,40 @@
1
+ from google.protobuf import descriptor as _descriptor
2
+ from google.protobuf import message as _message
3
+ from collections.abc import Mapping as _Mapping
4
+ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
5
+
6
+ DESCRIPTOR: _descriptor.FileDescriptor
7
+
8
+ class Packet(_message.Message):
9
+ __slots__ = ("call_start", "call_data", "call_cancel")
10
+ CALL_START_FIELD_NUMBER: _ClassVar[int]
11
+ CALL_DATA_FIELD_NUMBER: _ClassVar[int]
12
+ CALL_CANCEL_FIELD_NUMBER: _ClassVar[int]
13
+ call_start: CallStart
14
+ call_data: CallData
15
+ call_cancel: bool
16
+ def __init__(self, call_start: _Optional[_Union[CallStart, _Mapping]] = ..., call_data: _Optional[_Union[CallData, _Mapping]] = ..., call_cancel: _Optional[bool] = ...) -> None: ...
17
+
18
+ class CallStart(_message.Message):
19
+ __slots__ = ("rpc_service", "rpc_method", "data", "data_is_zero")
20
+ RPC_SERVICE_FIELD_NUMBER: _ClassVar[int]
21
+ RPC_METHOD_FIELD_NUMBER: _ClassVar[int]
22
+ DATA_FIELD_NUMBER: _ClassVar[int]
23
+ DATA_IS_ZERO_FIELD_NUMBER: _ClassVar[int]
24
+ rpc_service: str
25
+ rpc_method: str
26
+ data: bytes
27
+ data_is_zero: bool
28
+ def __init__(self, rpc_service: _Optional[str] = ..., rpc_method: _Optional[str] = ..., data: _Optional[bytes] = ..., data_is_zero: _Optional[bool] = ...) -> None: ...
29
+
30
+ class CallData(_message.Message):
31
+ __slots__ = ("data", "data_is_zero", "complete", "error")
32
+ DATA_FIELD_NUMBER: _ClassVar[int]
33
+ DATA_IS_ZERO_FIELD_NUMBER: _ClassVar[int]
34
+ COMPLETE_FIELD_NUMBER: _ClassVar[int]
35
+ ERROR_FIELD_NUMBER: _ClassVar[int]
36
+ data: bytes
37
+ data_is_zero: bool
38
+ complete: bool
39
+ error: str
40
+ def __init__(self, data: _Optional[bytes] = ..., data_is_zero: _Optional[bool] = ..., complete: _Optional[bool] = ..., error: _Optional[str] = ...) -> None: ...
@@ -0,0 +1,55 @@
1
+ declare const contextKeyValue: unique symbol
2
+ const contextParent = Symbol('server context parent')
3
+ const contextKey = Symbol('server context key')
4
+ const contextStoredValue = Symbol('server context value')
5
+
6
+ // ContextKey identifies one typed server-context value.
7
+ export interface ContextKey<T> {
8
+ readonly [contextKeyValue]?: T
9
+ }
10
+
11
+ // ServerContext carries cancellation for one server invocation.
12
+ export interface ServerContext {
13
+ readonly signal: AbortSignal
14
+ }
15
+
16
+ type StoredServerContext = ServerContext & {
17
+ readonly [contextParent]?: ServerContext
18
+ readonly [contextKey]?: ContextKey<unknown>
19
+ readonly [contextStoredValue]?: unknown
20
+ }
21
+
22
+ // createContextKey constructs an identity key for one server-context value.
23
+ export function createContextKey<T>(): ContextKey<T> {
24
+ return {}
25
+ }
26
+
27
+ // withServerContextValue derives a context with one immutable typed value.
28
+ export function withServerContextValue<T>(
29
+ context: ServerContext,
30
+ key: ContextKey<T>,
31
+ value: T,
32
+ ): ServerContext {
33
+ const derived: StoredServerContext = {
34
+ signal: context.signal,
35
+ [contextParent]: context,
36
+ [contextKey]: key as ContextKey<unknown>,
37
+ [contextStoredValue]: value,
38
+ }
39
+ return derived
40
+ }
41
+
42
+ // serverContextValue retrieves the nearest value for a typed identity key.
43
+ export function serverContextValue<T>(
44
+ context: ServerContext,
45
+ key: ContextKey<T>,
46
+ ): T | undefined {
47
+ let current: StoredServerContext | undefined = context as StoredServerContext
48
+ while (current) {
49
+ if (current[contextKey] === key) {
50
+ return current[contextStoredValue] as T
51
+ }
52
+ current = current[contextParent] as StoredServerContext | undefined
53
+ }
54
+ return undefined
55
+ }
@@ -4,6 +4,7 @@ import type { CallData, CallStart } from './rpcproto.pb.js'
4
4
  import { CommonRPC } from './common-rpc.js'
5
5
  import { InvokeFn } from './handler.js'
6
6
  import { LookupMethod } from './mux.js'
7
+ import type { ServerContext } from './server-context.js'
7
8
 
8
9
  // ServerRPC is an ongoing RPC from the server side.
9
10
  export class ServerRPC extends CommonRPC {
@@ -47,7 +48,9 @@ export class ServerRPC extends CommonRPC {
47
48
  private async invokeRPC(invokeFn: InvokeFn) {
48
49
  const dataSink = this._createDataSink()
49
50
  try {
50
- await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal)
51
+ await invokeFn(this.rpcDataSource, dataSink, {
52
+ signal: this.invocationSignal,
53
+ } satisfies ServerContext)
51
54
  } catch (err) {
52
55
  this.close(err as Error)
53
56
  }