starpc 0.49.18 → 0.49.20

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 (69) hide show
  1. package/README.md +1 -3
  2. package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
  3. package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
  4. package/dist/echo/echo.pb.d.ts +1 -1
  5. package/dist/echo/echo.pb.js +3 -3
  6. package/dist/integration/cross-language/ts-client.js +84 -11
  7. package/dist/integration/cross-language/ts-server.js +84 -4
  8. package/dist/mock/mock.pb.d.ts +1 -1
  9. package/dist/mock/mock.pb.js +3 -3
  10. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  11. package/dist/rpcstream/rpcstream.pb.js +14 -8
  12. package/dist/srpc/call-receipt.d.ts +17 -0
  13. package/dist/srpc/call-receipt.js +105 -0
  14. package/dist/srpc/call-receipt.test.d.ts +1 -0
  15. package/dist/srpc/call-receipt.test.js +374 -0
  16. package/dist/srpc/client.d.ts +3 -1
  17. package/dist/srpc/client.js +20 -0
  18. package/dist/srpc/common-rpc.d.ts +13 -1
  19. package/dist/srpc/common-rpc.js +86 -6
  20. package/dist/srpc/handler.d.ts +2 -1
  21. package/dist/srpc/index.d.ts +4 -0
  22. package/dist/srpc/index.js +2 -0
  23. package/dist/srpc/invoker.d.ts +2 -1
  24. package/dist/srpc/invoker.js +2 -2
  25. package/dist/srpc/rpcproto.pb.d.ts +1 -1
  26. package/dist/srpc/rpcproto.pb.js +7 -7
  27. package/dist/srpc/server-invocation.d.ts +17 -0
  28. package/dist/srpc/server-invocation.js +37 -0
  29. package/dist/srpc/server-rpc.js +3 -1
  30. package/echo/echo.pb.go +1 -1
  31. package/echo/echo.pb.ts +6 -5
  32. package/echo/echo_srpc.pb.cpp +1 -1
  33. package/echo/echo_srpc.pb.go +1 -1
  34. package/echo/echo_srpc.pb.hpp +1 -1
  35. package/echo/echo_srpc.pb.rs +1 -1
  36. package/integration/cross-language/go-client/main.go +137 -5
  37. package/integration/cross-language/go-server/main.go +146 -3
  38. package/integration/cross-language/run.bash +117 -13
  39. package/integration/cross-language/ts-client.ts +94 -13
  40. package/integration/cross-language/ts-server.ts +94 -6
  41. package/mock/mock.pb.go +1 -1
  42. package/mock/mock.pb.ts +6 -5
  43. package/mock/mock_srpc.pb.cpp +1 -1
  44. package/mock/mock_srpc.pb.go +1 -1
  45. package/mock/mock_srpc.pb.hpp +1 -1
  46. package/mock/mock_srpc.pb.rs +1 -1
  47. package/package.json +1 -1
  48. package/srpc/call-receipt-e2e_test.go +111 -0
  49. package/srpc/call-receipt.go +112 -0
  50. package/srpc/call-receipt.test.ts +438 -0
  51. package/srpc/call-receipt.ts +130 -0
  52. package/srpc/call-receipt_test.go +536 -0
  53. package/srpc/client-rpc.go +1 -8
  54. package/srpc/client.ts +29 -2
  55. package/srpc/common-rpc.go +114 -29
  56. package/srpc/common-rpc.ts +98 -8
  57. package/srpc/handler.ts +2 -0
  58. package/srpc/index.ts +4 -0
  59. package/srpc/invoker.ts +10 -5
  60. package/srpc/msg-stream.go +8 -0
  61. package/srpc/muxed-conn.go +8 -3
  62. package/srpc/muxed-conn_test.go +79 -0
  63. package/srpc/rpcproto.pb.go +1 -1
  64. package/srpc/rpcproto.pb.ts +28 -27
  65. package/srpc/server-invocation.go +58 -0
  66. package/srpc/server-invocation.ts +82 -0
  67. package/srpc/server-rpc.go +1 -1
  68. package/srpc/server-rpc.ts +6 -2
  69. package/srpc/stream.go +20 -0
@@ -48,6 +48,14 @@ type commonRPC struct {
48
48
  dataClosed bool
49
49
  // remoteErr is an error set by the remote.
50
50
  remoteErr error
51
+ // remoteCanceled distinguishes a received CallCancel from transport loss.
52
+ remoteCanceled bool
53
+ // remoteCompleted is set only by an explicit remote CallData completion.
54
+ remoteCompleted bool
55
+ // remoteTerminal is the first valid remote terminal.
56
+ remoteTerminal TerminalKind
57
+ // remoteTerminalSet records whether remoteTerminal is valid.
58
+ remoteTerminalSet bool
51
59
  }
52
60
 
53
61
  // initCommonRPC initializes the commonRPC.
@@ -116,6 +124,41 @@ func (c *commonRPC) Wait(ctx context.Context) error {
116
124
  }
117
125
  }
118
126
 
127
+ // WaitTerminal waits for and classifies the remote terminal of a held unary
128
+ // invocation.
129
+ func (c *commonRPC) WaitTerminal(ownerCtx context.Context) (TerminalKind, error) {
130
+ var ownerDone bool
131
+ for {
132
+ locked := c.bcast.Lock()
133
+ if c.remoteTerminalSet {
134
+ terminal := c.remoteTerminal
135
+ locked.Unlock()
136
+ return terminal, nil
137
+ }
138
+ if ownerDone {
139
+ err := ownerCtx.Err()
140
+ locked.Unlock()
141
+ return TerminalAbandoned, err
142
+ }
143
+ waitCh := locked.WaitCh()
144
+ locked.Unlock()
145
+
146
+ select {
147
+ case <-ownerCtx.Done():
148
+ ownerDone = true
149
+ case <-waitCh:
150
+ }
151
+ }
152
+ }
153
+
154
+ // receiptTerminalKind returns the first valid remote terminal.
155
+ func (c *commonRPC) receiptTerminalKind() (TerminalKind, bool) {
156
+ locked := c.bcast.Lock()
157
+ terminal, ok := c.remoteTerminal, c.remoteTerminalSet
158
+ locked.Unlock()
159
+ return terminal, ok
160
+ }
161
+
119
162
  // ReadOne reads a single message and returns.
120
163
  //
121
164
  // returns io.EOF if the stream ended without a packet.
@@ -164,43 +207,26 @@ func (c *commonRPC) ReadOne() ([]byte, error) {
164
207
 
165
208
  // WriteCallData writes a call data packet.
166
209
  func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err error) error {
167
- // Check if already completed
168
- if c.localCompleted.Load() {
169
- // If we're just marking completion and already completed, allow it (no-op)
170
- if complete && len(data) == 0 && !dataIsZero {
171
- return nil
210
+ if complete || err != nil {
211
+ if c.localCompleted.Swap(true) {
212
+ // Repeated completion is an idempotent no-op.
213
+ if complete && len(data) == 0 && !dataIsZero {
214
+ return nil
215
+ }
216
+ return ErrCompleted
172
217
  }
173
- // Otherwise, return error for trying to send data after completion
218
+ } else if c.localCompleted.Load() {
174
219
  return ErrCompleted
175
220
  }
176
221
 
177
- // Mark as completed if this call completes the RPC
178
- if complete || err != nil {
179
- c.localCompleted.Store(true)
180
- }
181
-
182
222
  outPkt := NewCallDataPacket(data, len(data) == 0 && dataIsZero, complete, err)
183
223
  return c.writer.WritePacket(outPkt)
184
224
  }
185
225
 
186
226
  // HandleStreamClose handles the incoming stream closing w/ optional error.
187
227
  func (c *commonRPC) HandleStreamClose(closeErr error) {
188
- var writer PacketWriter
189
228
  locked := c.bcast.Lock()
190
- if c.dataClosed && c.writerClosed {
191
- locked.Unlock()
192
- return
193
- }
194
- normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
195
- if closeErr != nil && c.remoteErr == nil {
196
- c.remoteErr = closeErr
197
- }
198
- c.dataClosed = true
199
- if !normalRemoteCloseAfterLocalComplete {
200
- c.cancelContext()
201
- writer = c.closeWriterLocked()
202
- }
203
- locked.Broadcast()
229
+ writer := c.handleStreamCloseLocked(&locked, closeErr)
204
230
  locked.Unlock()
205
231
  if writer != nil {
206
232
  _ = writer.Close()
@@ -209,7 +235,15 @@ func (c *commonRPC) HandleStreamClose(closeErr error) {
209
235
 
210
236
  // HandleCallCancel handles the call cancel packet.
211
237
  func (c *commonRPC) HandleCallCancel() error {
212
- c.HandleStreamClose(context.Canceled)
238
+ locked := c.bcast.Lock()
239
+ if (!c.dataClosed || !c.writerClosed) && !c.remoteTerminalSet {
240
+ c.remoteCanceled = true
241
+ }
242
+ writer := c.handleStreamCloseLocked(&locked, context.Canceled)
243
+ locked.Unlock()
244
+ if writer != nil {
245
+ _ = writer.Close()
246
+ }
213
247
  return nil
214
248
  }
215
249
 
@@ -231,14 +265,24 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
231
265
  c.dataQueue = append(c.dataQueue, data)
232
266
  }
233
267
 
268
+ pktErr := pkt.GetError()
234
269
  complete := pkt.GetComplete()
235
- if pktErr := pkt.GetError(); len(pktErr) != 0 {
270
+ if len(pktErr) != 0 {
236
271
  complete = true
237
- c.remoteErr = errors.New(pktErr)
272
+ if c.remoteErr == nil {
273
+ c.remoteErr = errors.New(pktErr)
274
+ }
238
275
  }
239
276
 
240
277
  if complete {
241
278
  c.dataClosed = true
279
+ if len(pktErr) == 0 {
280
+ if c.recordRemoteTerminalLocked(TerminalCommitted) {
281
+ c.remoteCompleted = true
282
+ }
283
+ } else {
284
+ c.recordRemoteTerminalLocked(TerminalLost)
285
+ }
242
286
  }
243
287
 
244
288
  locked.Broadcast()
@@ -247,6 +291,47 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
247
291
  return err
248
292
  }
249
293
 
294
+ func (c *commonRPC) recordRemoteTerminalLocked(kind TerminalKind) bool {
295
+ if c.remoteTerminalSet {
296
+ return false
297
+ }
298
+ c.remoteTerminal = kind
299
+ c.remoteTerminalSet = true
300
+ return true
301
+ }
302
+
303
+ func (c *commonRPC) handleStreamCloseLocked(
304
+ locked *broadcast.Locked,
305
+ closeErr error,
306
+ ) PacketWriter {
307
+ if c.dataClosed && c.writerClosed {
308
+ return nil
309
+ }
310
+ normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
311
+ if closeErr != nil && c.remoteErr == nil {
312
+ c.remoteErr = closeErr
313
+ }
314
+ if !normalRemoteCloseAfterLocalComplete && !c.remoteTerminalSet {
315
+ terminal := TerminalClosed
316
+ if closeErr != nil {
317
+ terminal = TerminalLost
318
+ if c.remoteCanceled {
319
+ terminal = TerminalCanceled
320
+ }
321
+ }
322
+ c.recordRemoteTerminalLocked(terminal)
323
+ }
324
+ c.dataClosed = true
325
+ if !normalRemoteCloseAfterLocalComplete {
326
+ c.cancelContext()
327
+ writer := c.closeWriterLocked()
328
+ locked.Broadcast()
329
+ return writer
330
+ }
331
+ locked.Broadcast()
332
+ return nil
333
+ }
334
+
250
335
  // WriteCallCancel writes a call cancel packet.
251
336
  func (c *commonRPC) WriteCallCancel() error {
252
337
  // Use atomic swap to check and set completion atomically
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2024.promise" />
1
2
  import type { Sink, Source } from 'it-stream-types'
2
3
  import { pushable, type Pushable } from 'it-pushable'
3
4
  import { CompleteMessage } from '@aptre/protobuf-es-lite'
@@ -5,6 +6,7 @@ import { CompleteMessage } from '@aptre/protobuf-es-lite'
5
6
  import type { CallData, CallStart } from './rpcproto.pb.js'
6
7
  import { Packet } from './rpcproto.pb.js'
7
8
  import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js'
9
+ import type { TerminalKind } from './server-invocation.js'
8
10
 
9
11
  const maxBufferedOutgoingPackets = 1
10
12
 
@@ -34,10 +36,27 @@ export class CommonRPC {
34
36
 
35
37
  // closed indicates this rpc has been closed already.
36
38
  private closed?: true | Error
39
+ // remoteCompleted is set only by an explicit remote CallData completion.
40
+ private remoteCompleted = false
41
+ // remoteError records a remote error or transport failure.
42
+ private remoteError?: Error
43
+ // remoteSourceClosed records an incoming source ending without a packet error.
44
+ private remoteSourceClosed = false
45
+ // remoteTerminal is the first valid remote terminal.
46
+ private remoteTerminal?: TerminalKind
47
+ // invocationController cancels the server invocation on a remote terminal.
48
+ private readonly invocationController = new AbortController()
49
+ // terminalPromise resolves when a remote terminal is recorded.
50
+ private readonly terminalPromise: Promise<void>
51
+ private resolveTerminal!: () => void
52
+
37
53
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
38
54
  private readonly writeDrainAbort = new AbortController()
39
55
 
40
56
  constructor() {
57
+ const { promise, resolve } = Promise.withResolvers<void>()
58
+ this.terminalPromise = promise
59
+ this.resolveTerminal = resolve
41
60
  this.sink = this._createSink()
42
61
  this.source = this._source
43
62
  this.rpcDataSource = this._rpcDataSource
@@ -48,6 +67,57 @@ export class CommonRPC {
48
67
  return this.closed ?? false
49
68
  }
50
69
 
70
+ // invocationSignal is canceled when the RPC reaches a terminal.
71
+ protected get invocationSignal(): AbortSignal {
72
+ return this.invocationController.signal
73
+ }
74
+
75
+ // waitTerminal waits for the remote terminal or external owner cancellation.
76
+ protected async waitTerminal(
77
+ ownerSignal: AbortSignal,
78
+ ): Promise<TerminalKind> {
79
+ const { promise: ownerDone, resolve: resolveOwnerDone } =
80
+ Promise.withResolvers<void>()
81
+ const onAbort = () => resolveOwnerDone()
82
+ ownerSignal.addEventListener('abort', onAbort, { once: true })
83
+ let ownerAborted = ownerSignal.aborted
84
+ try {
85
+ for (;;) {
86
+ const terminal = this.getTerminalKind()
87
+ if (terminal !== undefined) {
88
+ if (
89
+ terminal === 'closed' &&
90
+ this.remoteSourceClosed &&
91
+ !this.closed
92
+ ) {
93
+ await this.close()
94
+ }
95
+ return terminal
96
+ }
97
+ if (ownerAborted) {
98
+ return 'abandoned'
99
+ }
100
+ await Promise.race([this.terminalPromise, ownerDone])
101
+ ownerAborted = ownerSignal.aborted
102
+ }
103
+ } finally {
104
+ ownerSignal.removeEventListener('abort', onAbort)
105
+ }
106
+ }
107
+
108
+ // getTerminalKind returns the observed remote terminal, if any.
109
+ public getTerminalKind(): TerminalKind | undefined {
110
+ return this.remoteTerminal
111
+ }
112
+
113
+ private recordRemoteTerminal(kind: TerminalKind) {
114
+ if (this.remoteTerminal !== undefined) {
115
+ return
116
+ }
117
+ this.remoteTerminal = kind
118
+ this.resolveTerminal()
119
+ }
120
+
51
121
  // writeCallData writes the call data packet.
52
122
  public async writeCallData(
53
123
  data?: Uint8Array,
@@ -82,7 +152,7 @@ export class CommonRPC {
82
152
  }
83
153
 
84
154
  // writeCallCancel writes the call cancel packet.
85
- public async writeCallCancel() {
155
+ public async writeCallCancel(waitForDrain = false) {
86
156
  await this.writePacket(
87
157
  {
88
158
  body: {
@@ -92,6 +162,9 @@ export class CommonRPC {
92
162
  },
93
163
  { waitForDrain: false },
94
164
  )
165
+ if (waitForDrain) {
166
+ await this._source.onEmpty({ signal: this.writeDrainAbort.signal })
167
+ }
95
168
  }
96
169
 
97
170
  // writeCallDataFromSource writes all call data from the iterable.
@@ -194,18 +267,28 @@ export class CommonRPC {
194
267
  }
195
268
 
196
269
  this.pushRpcData(packet.data, packet.dataIsZero)
197
- if (packet.error) {
198
- this._rpcDataSource.end(
199
- new RemoteRPCError(this.service, this.method, packet.error),
200
- )
201
- } else if (packet.complete) {
202
- this._rpcDataSource.end()
270
+ const remoteError =
271
+ packet.error ?
272
+ new RemoteRPCError(this.service, this.method, packet.error)
273
+ : undefined
274
+ if (remoteError) {
275
+ this.remoteError ??= remoteError
276
+ this.invocationController.abort()
277
+ this.recordRemoteTerminal('transportLost')
278
+ }
279
+ if (packet.complete && !remoteError) {
280
+ this.remoteCompleted = true
281
+ this.recordRemoteTerminal('committed')
282
+ this._rpcDataSource.end(remoteError)
283
+ } else if (remoteError) {
284
+ this._rpcDataSource.end(remoteError)
203
285
  }
204
286
  }
205
287
 
206
288
  // handleCallCancel handles a CallCancel packet.
207
289
  public async handleCallCancel() {
208
- this.close(new Error(ERR_RPC_ABORT))
290
+ this.recordRemoteTerminal('canceled')
291
+ await this.close(new Error(ERR_RPC_ABORT))
209
292
  }
210
293
 
211
294
  // close closes the call, optionally with an error.
@@ -214,6 +297,11 @@ export class CommonRPC {
214
297
  return
215
298
  }
216
299
  this.closed = err ?? true
300
+ if (!this.remoteError && err) {
301
+ this.remoteError = err
302
+ }
303
+ this.recordRemoteTerminal(err ? 'transportLost' : 'closed')
304
+ this.invocationController.abort()
217
305
  // note: this does nothing if _source is already ended.
218
306
  if (err && err.message) {
219
307
  await this.writeCallDataPacket(undefined, true, err.message, {
@@ -241,6 +329,8 @@ export class CommonRPC {
241
329
  await this.handlePacket(msg)
242
330
  }
243
331
  }
332
+ this.remoteSourceClosed = true
333
+ this.recordRemoteTerminal('closed')
244
334
  } catch (err) {
245
335
  this.close(err as Error)
246
336
  }
package/srpc/handler.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { Sink, Source } from 'it-stream-types'
2
2
  import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js'
3
3
  import { createInvokeFn } from './invoker.js'
4
+ import type { ServerInvocation } from './server-invocation.js'
4
5
 
5
6
  // InvokeFn describes an SRPC call method invoke function.
6
7
  export type InvokeFn = (
7
8
  dataSource: Source<Uint8Array>,
8
9
  dataSink: Sink<Source<Uint8Array>>,
10
+ invocation?: ServerInvocation,
9
11
  ) => Promise<void>
10
12
 
11
13
  // Handler describes a SRPC call handler implementation.
package/srpc/index.ts CHANGED
@@ -6,6 +6,10 @@ export {
6
6
  castToError,
7
7
  } from './errors.js'
8
8
  export { Client } from './client.js'
9
+ export { CallReceipt } from './call-receipt.js'
10
+ export type { HeldCall, ReceiptRpc } from './call-receipt.js'
11
+ export { ServerInvocation } from './server-invocation.js'
12
+ export type { TerminalKind } from './server-invocation.js'
9
13
  export { Server } from './server.js'
10
14
  export { StreamConn } from './conn.js'
11
15
  export type { StreamConnParams, StreamHandler } from './conn.js'
package/srpc/invoker.ts CHANGED
@@ -11,12 +11,16 @@ import { writeToPushable } from './pushable.js'
11
11
  import type { MessageType, Message } from '@aptre/protobuf-es-lite'
12
12
  import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite'
13
13
 
14
+ import type { ServerInvocation } from './server-invocation.js'
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) => Promise<O>)
17
- | ((request: R) => AsyncIterable<O>)
18
- | ((request: AsyncIterable<R>) => Promise<O>)
19
- | ((request: AsyncIterable<R>) => AsyncIterable<O>)
17
+ | ((request: R, invocation?: ServerInvocation) => Promise<O>)
18
+ | ((request: R, invocation?: ServerInvocation) => AsyncIterable<O>)
19
+ | ((request: AsyncIterable<R>, invocation?: ServerInvocation) => Promise<O>)
20
+ | ((
21
+ request: AsyncIterable<R>,
22
+ invocation?: ServerInvocation,
23
+ ) => AsyncIterable<O>)
20
24
 
21
25
  // createInvokeFn builds an InvokeFn from a method definition and a function prototype.
22
26
  export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
@@ -32,6 +36,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
32
36
  return async (
33
37
  dataSource: Source<Uint8Array>,
34
38
  dataSink: Sink<Source<Uint8Array>>,
39
+ invocation?: ServerInvocation,
35
40
  ) => {
36
41
  // responseSink is a Sink for response messages.
37
42
  const responseSink = pushable<O>({
@@ -66,7 +71,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
66
71
 
67
72
  // Call the implementation.
68
73
  try {
69
- const responseObj = methodProto(requestArg)
74
+ const responseObj = methodProto(requestArg, invocation)
70
75
  if (!responseObj) {
71
76
  throw new Error('return value was undefined')
72
77
  }
@@ -86,5 +86,13 @@ func (r *MsgStream) Close() error {
86
86
  return err
87
87
  }
88
88
 
89
+ func (r *MsgStream) receiptTerminalKind() (TerminalKind, bool) {
90
+ receipt, ok := r.rw.(receiptTerminalStream)
91
+ if !ok {
92
+ return 0, false
93
+ }
94
+ return receipt.receiptTerminalKind()
95
+ }
96
+
89
97
  // _ is a type assertion
90
98
  var _ Stream = ((*MsgStream)(nil))
@@ -9,8 +9,7 @@ import (
9
9
  yamux "github.com/libp2p/go-yamux/v4"
10
10
  )
11
11
 
12
- // NewYamuxConfig builds the default yamux configuration.
13
- func NewYamuxConfig() *yamux.Config {
12
+ var yamuxConfigTemplate = func() yamux.Config {
14
13
  config := yamux.DefaultConfig()
15
14
  config.MaxStreamWindowSize = 16 * 1024 * 1024
16
15
  config.LogOutput = io.Discard
@@ -18,7 +17,13 @@ func NewYamuxConfig() *yamux.Config {
18
17
  config.MaxIncomingStreams = math.MaxUint32
19
18
  config.AcceptBacklog = 512
20
19
  config.EnableKeepAlive = false
21
- return config
20
+ return *config
21
+ }()
22
+
23
+ // NewYamuxConfig returns a fresh copy of the default yamux configuration.
24
+ func NewYamuxConfig() *yamux.Config {
25
+ config := yamuxConfigTemplate
26
+ return &config
22
27
  }
23
28
 
24
29
  // NewMuxedConn constructs a new MuxedConn from a net.Conn.
@@ -0,0 +1,79 @@
1
+ package srpc
2
+
3
+ import (
4
+ "net"
5
+ "os"
6
+ "sync"
7
+ "testing"
8
+ )
9
+
10
+ func TestNewMuxedConnConcurrentConfigIsolation(t *testing.T) {
11
+ first := NewYamuxConfig()
12
+ second := NewYamuxConfig()
13
+ if first == second {
14
+ t.Fatal("NewYamuxConfig returned a shared config")
15
+ }
16
+ first.AcceptBacklog++
17
+ if first.AcceptBacklog == second.AcceptBacklog {
18
+ t.Fatal("mutating one yamux config changed another")
19
+ }
20
+
21
+ // go-yamux's default config reads os.Stderr. Starpc discards yamux logs, so
22
+ // constructing a connection must not observe concurrent stderr replacement.
23
+ originalStderr := os.Stderr
24
+ devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
25
+ if err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ defer devNull.Close()
29
+
30
+ stopReplacingStderr := make(chan struct{})
31
+ var replaceStderr sync.WaitGroup
32
+ replaceStderr.Go(func() {
33
+ for {
34
+ select {
35
+ case <-stopReplacingStderr:
36
+ return
37
+ default:
38
+ os.Stderr = devNull
39
+ os.Stderr = originalStderr
40
+ }
41
+ }
42
+ })
43
+
44
+ const iterations = 100
45
+ start := make(chan struct{})
46
+ errs := make(chan error, 2)
47
+ var builders sync.WaitGroup
48
+ builders.Add(2)
49
+ for range 2 {
50
+ go func() {
51
+ defer builders.Done()
52
+ <-start
53
+ for range iterations {
54
+ conn, peer := net.Pipe()
55
+ muxed, err := NewMuxedConn(conn, false, nil)
56
+ if err == nil {
57
+ err = muxed.Close()
58
+ } else {
59
+ _ = conn.Close()
60
+ }
61
+ _ = peer.Close()
62
+ if err != nil {
63
+ errs <- err
64
+ return
65
+ }
66
+ }
67
+ }()
68
+ }
69
+ close(start)
70
+ builders.Wait()
71
+ close(stopReplacingStderr)
72
+ replaceStderr.Wait()
73
+ os.Stderr = originalStderr
74
+ close(errs)
75
+
76
+ for err := range errs {
77
+ t.Fatal(err)
78
+ }
79
+ }
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-go-lite. DO NOT EDIT.
2
- // protoc-gen-go-lite version: v0.12.1
2
+ // protoc-gen-go-lite version: v0.13.0
3
3
  // source: github.com/aperturerobotics/starpc/srpc/rpcproto.proto
4
4
 
5
5
  package srpc
@@ -2,8 +2,10 @@
2
2
  // @generated from file github.com/aperturerobotics/starpc/srpc/rpcproto.proto (package srpc, syntax proto3)
3
3
  /* eslint-disable */
4
4
 
5
- import type { MessageType, PartialFieldInfo } from '@aptre/protobuf-es-lite'
6
- import { createMessageType, ScalarType } from '@aptre/protobuf-es-lite'
5
+ import type { MessageType } from '@aptre/protobuf-es-lite/message'
6
+ import { createMessageType } from '@aptre/protobuf-es-lite/message'
7
+ import { ScalarType } from '@aptre/protobuf-es-lite/scalar'
8
+ import type { PartialFieldInfo } from '@aptre/protobuf-es-lite/field'
7
9
 
8
10
  export const protobufPackage = 'srpc'
9
11
 
@@ -42,17 +44,17 @@ export interface CallStart {
42
44
  dataIsZero?: boolean
43
45
  }
44
46
 
45
- // CallStart contains the message type declaration for CallStart.
46
- export const CallStart: MessageType<CallStart> = createMessageType({
47
- typeName: 'srpc.CallStart',
48
- fields: [
49
- { no: 1, name: 'rpc_service', kind: 'scalar', T: ScalarType.STRING },
50
- { no: 2, name: 'rpc_method', kind: 'scalar', T: ScalarType.STRING },
51
- { no: 3, name: 'data', kind: 'scalar', T: ScalarType.BYTES },
52
- { no: 4, name: 'data_is_zero', kind: 'scalar', T: ScalarType.BOOL },
53
- ] as readonly PartialFieldInfo[],
54
- packedByDefault: true,
55
- })
47
+ export const CallStart: MessageType<CallStart> =
48
+ /* @__PURE__ */ createMessageType({
49
+ typeName: 'srpc.CallStart',
50
+ fields: [
51
+ { no: 1, name: 'rpc_service', kind: 'scalar', T: ScalarType.STRING },
52
+ { no: 2, name: 'rpc_method', kind: 'scalar', T: ScalarType.STRING },
53
+ { no: 3, name: 'data', kind: 'scalar', T: ScalarType.BYTES },
54
+ { no: 4, name: 'data_is_zero', kind: 'scalar', T: ScalarType.BOOL },
55
+ ] satisfies readonly PartialFieldInfo[],
56
+ packedByDefault: true,
57
+ })
56
58
 
57
59
  /**
58
60
  * CallData contains a message in a streaming RPC sequence.
@@ -87,17 +89,17 @@ export interface CallData {
87
89
  error?: string
88
90
  }
89
91
 
90
- // CallData contains the message type declaration for CallData.
91
- export const CallData: MessageType<CallData> = createMessageType({
92
- typeName: 'srpc.CallData',
93
- fields: [
94
- { no: 1, name: 'data', kind: 'scalar', T: ScalarType.BYTES },
95
- { no: 2, name: 'data_is_zero', kind: 'scalar', T: ScalarType.BOOL },
96
- { no: 3, name: 'complete', kind: 'scalar', T: ScalarType.BOOL },
97
- { no: 4, name: 'error', kind: 'scalar', T: ScalarType.STRING },
98
- ] as readonly PartialFieldInfo[],
99
- packedByDefault: true,
100
- })
92
+ export const CallData: MessageType<CallData> =
93
+ /* @__PURE__ */ createMessageType({
94
+ typeName: 'srpc.CallData',
95
+ fields: [
96
+ { no: 1, name: 'data', kind: 'scalar', T: ScalarType.BYTES },
97
+ { no: 2, name: 'data_is_zero', kind: 'scalar', T: ScalarType.BOOL },
98
+ { no: 3, name: 'complete', kind: 'scalar', T: ScalarType.BOOL },
99
+ { no: 4, name: 'error', kind: 'scalar', T: ScalarType.STRING },
100
+ ] satisfies readonly PartialFieldInfo[],
101
+ packedByDefault: true,
102
+ })
101
103
 
102
104
  /**
103
105
  * Packet is a message sent over a srpc packet connection.
@@ -144,8 +146,7 @@ export interface Packet {
144
146
  }
145
147
  }
146
148
 
147
- // Packet contains the message type declaration for Packet.
148
- export const Packet: MessageType<Packet> = createMessageType({
149
+ export const Packet: MessageType<Packet> = /* @__PURE__ */ createMessageType({
149
150
  typeName: 'srpc.Packet',
150
151
  fields: [
151
152
  {
@@ -169,6 +170,6 @@ export const Packet: MessageType<Packet> = createMessageType({
169
170
  T: ScalarType.BOOL,
170
171
  oneof: 'body',
171
172
  },
172
- ] as readonly PartialFieldInfo[],
173
+ ] satisfies readonly PartialFieldInfo[],
173
174
  packedByDefault: true,
174
175
  })