starpc 0.49.18 → 0.50.0

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 (86) 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 +100 -4
  8. package/dist/mock/index.d.ts +2 -2
  9. package/dist/mock/index.js +2 -2
  10. package/dist/mock/mock.pb.d.ts +1 -1
  11. package/dist/mock/mock.pb.js +3 -3
  12. package/dist/rpcstream/receipt.test.d.ts +1 -0
  13. package/dist/rpcstream/receipt.test.js +41 -0
  14. package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
  15. package/dist/rpcstream/rpcstream.pb.js +14 -8
  16. package/dist/srpc/call-receipt.d.ts +17 -0
  17. package/dist/srpc/call-receipt.js +106 -0
  18. package/dist/srpc/call-receipt.test.d.ts +1 -0
  19. package/dist/srpc/call-receipt.test.js +375 -0
  20. package/dist/srpc/client.d.ts +3 -1
  21. package/dist/srpc/client.js +20 -0
  22. package/dist/srpc/common-rpc.d.ts +13 -3
  23. package/dist/srpc/common-rpc.js +87 -10
  24. package/dist/srpc/handler.d.ts +2 -1
  25. package/dist/srpc/index.d.ts +4 -0
  26. package/dist/srpc/index.js +3 -0
  27. package/dist/srpc/invoker.d.ts +2 -1
  28. package/dist/srpc/invoker.js +2 -2
  29. package/dist/srpc/rpcproto.pb.d.ts +45 -1
  30. package/dist/srpc/rpcproto.pb.js +60 -7
  31. package/dist/srpc/server-invocation.d.ts +17 -0
  32. package/dist/srpc/server-invocation.js +37 -0
  33. package/dist/srpc/server-rpc.js +3 -1
  34. package/echo/echo.pb.go +1 -1
  35. package/echo/echo.pb.ts +6 -5
  36. package/echo/echo_srpc.pb.cpp +1 -1
  37. package/echo/echo_srpc.pb.go +1 -1
  38. package/echo/echo_srpc.pb.hpp +1 -1
  39. package/echo/echo_srpc.pb.rs +1 -1
  40. package/go.mod +13 -12
  41. package/go.sum +24 -24
  42. package/integration/cross-language/go-client/main.go +137 -5
  43. package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
  44. package/integration/cross-language/go-server/main.go +169 -4
  45. package/integration/cross-language/run.bash +117 -13
  46. package/integration/cross-language/ts-client.ts +94 -13
  47. package/integration/cross-language/ts-server.ts +115 -7
  48. package/mock/index.ts +2 -2
  49. package/mock/mock.pb.go +1 -1
  50. package/mock/mock.pb.ts +6 -5
  51. package/mock/mock_srpc.pb.cpp +1 -1
  52. package/mock/mock_srpc.pb.go +1 -1
  53. package/mock/mock_srpc.pb.hpp +1 -1
  54. package/mock/mock_srpc.pb.rs +1 -1
  55. package/package.json +19 -26
  56. package/srpc/accept.go +1 -1
  57. package/srpc/call-receipt-e2e_test.go +111 -0
  58. package/srpc/call-receipt.go +112 -0
  59. package/srpc/call-receipt.test.ts +441 -0
  60. package/srpc/call-receipt.ts +131 -0
  61. package/srpc/call-receipt_test.go +536 -0
  62. package/srpc/client-rpc.go +1 -8
  63. package/srpc/client.ts +29 -2
  64. package/srpc/common-rpc.go +114 -29
  65. package/srpc/common-rpc.ts +104 -13
  66. package/srpc/handler.ts +2 -0
  67. package/srpc/index.ts +4 -0
  68. package/srpc/invoker.ts +10 -5
  69. package/srpc/msg-stream.go +8 -0
  70. package/srpc/muxed-conn.go +9 -4
  71. package/srpc/muxed-conn_test.go +79 -0
  72. package/srpc/muxed-yamux.go +1 -1
  73. package/srpc/rpcproto.pb.cc +15 -4
  74. package/srpc/rpcproto.pb.go +97 -1
  75. package/srpc/rpcproto.pb.h +59 -0
  76. package/srpc/rpcproto.pb.rs +45 -0
  77. package/srpc/rpcproto.pb.ts +90 -27
  78. package/srpc/rpcproto.proto +16 -0
  79. package/srpc/schema-ownership_test.go +115 -0
  80. package/srpc/server-invocation.go +40 -0
  81. package/srpc/server-invocation.ts +76 -0
  82. package/srpc/server-rpc.go +1 -1
  83. package/srpc/server-rpc.ts +6 -2
  84. package/srpc/stream-yamux.go +1 -1
  85. package/srpc/stream.go +20 -0
  86. package/srpc/websocket.go +1 -1
@@ -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 TerminalKind_TERMINAL_KIND_ABANDONED, 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(TerminalKind_TERMINAL_KIND_COMMITTED) {
281
+ c.remoteCompleted = true
282
+ }
283
+ } else {
284
+ c.recordRemoteTerminalLocked(TerminalKind_TERMINAL_KIND_TRANSPORT_LOST)
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 := TerminalKind_TERMINAL_KIND_CLOSED
316
+ if closeErr != nil {
317
+ terminal = TerminalKind_TERMINAL_KIND_TRANSPORT_LOST
318
+ if c.remoteCanceled {
319
+ terminal = TerminalKind_TERMINAL_KIND_CANCELED
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,9 +1,14 @@
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'
4
5
 
5
- import type { CallData, CallStart } from './rpcproto.pb.js'
6
- import { Packet } from './rpcproto.pb.js'
6
+ import {
7
+ Packet,
8
+ TerminalKind,
9
+ type CallData,
10
+ type CallStart,
11
+ } from './rpcproto.pb.js'
7
12
  import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js'
8
13
 
9
14
  const maxBufferedOutgoingPackets = 1
@@ -34,10 +39,27 @@ export class CommonRPC {
34
39
 
35
40
  // closed indicates this rpc has been closed already.
36
41
  private closed?: true | Error
42
+ // remoteCompleted is set only by an explicit remote CallData completion.
43
+ private remoteCompleted = false
44
+ // remoteError records a remote error or transport failure.
45
+ private remoteError?: Error
46
+ // remoteSourceClosed records an incoming source ending without a packet error.
47
+ private remoteSourceClosed = false
48
+ // remoteTerminal is the first valid remote terminal.
49
+ private remoteTerminal?: TerminalKind
50
+ // invocationController cancels the server invocation on a remote terminal.
51
+ private readonly invocationController = new AbortController()
52
+ // terminalPromise resolves when a remote terminal is recorded.
53
+ private readonly terminalPromise: Promise<void>
54
+ private resolveTerminal!: () => void
55
+
37
56
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
38
57
  private readonly writeDrainAbort = new AbortController()
39
58
 
40
59
  constructor() {
60
+ const { promise, resolve } = Promise.withResolvers<void>()
61
+ this.terminalPromise = promise
62
+ this.resolveTerminal = resolve
41
63
  this.sink = this._createSink()
42
64
  this.source = this._source
43
65
  this.rpcDataSource = this._rpcDataSource
@@ -48,6 +70,57 @@ export class CommonRPC {
48
70
  return this.closed ?? false
49
71
  }
50
72
 
73
+ // invocationSignal is canceled when the RPC reaches a terminal.
74
+ protected get invocationSignal(): AbortSignal {
75
+ return this.invocationController.signal
76
+ }
77
+
78
+ // waitTerminal waits for the remote terminal or external owner cancellation.
79
+ protected async waitTerminal(
80
+ ownerSignal: AbortSignal,
81
+ ): Promise<TerminalKind> {
82
+ const { promise: ownerDone, resolve: resolveOwnerDone } =
83
+ Promise.withResolvers<void>()
84
+ const onAbort = () => resolveOwnerDone()
85
+ ownerSignal.addEventListener('abort', onAbort, { once: true })
86
+ let ownerAborted = ownerSignal.aborted
87
+ try {
88
+ for (;;) {
89
+ const terminal = this.getTerminalKind()
90
+ if (terminal !== undefined) {
91
+ if (
92
+ terminal === TerminalKind.CLOSED &&
93
+ this.remoteSourceClosed &&
94
+ !this.closed
95
+ ) {
96
+ await this.close()
97
+ }
98
+ return terminal
99
+ }
100
+ if (ownerAborted) {
101
+ return TerminalKind.ABANDONED
102
+ }
103
+ await Promise.race([this.terminalPromise, ownerDone])
104
+ ownerAborted = ownerSignal.aborted
105
+ }
106
+ } finally {
107
+ ownerSignal.removeEventListener('abort', onAbort)
108
+ }
109
+ }
110
+
111
+ // getTerminalKind returns the observed remote terminal, if any.
112
+ public getTerminalKind(): TerminalKind | undefined {
113
+ return this.remoteTerminal
114
+ }
115
+
116
+ private recordRemoteTerminal(kind: TerminalKind) {
117
+ if (this.remoteTerminal !== undefined) {
118
+ return
119
+ }
120
+ this.remoteTerminal = kind
121
+ this.resolveTerminal()
122
+ }
123
+
51
124
  // writeCallData writes the call data packet.
52
125
  public async writeCallData(
53
126
  data?: Uint8Array,
@@ -82,7 +155,7 @@ export class CommonRPC {
82
155
  }
83
156
 
84
157
  // writeCallCancel writes the call cancel packet.
85
- public async writeCallCancel() {
158
+ public async writeCallCancel(waitForDrain = false) {
86
159
  await this.writePacket(
87
160
  {
88
161
  body: {
@@ -92,6 +165,9 @@ export class CommonRPC {
92
165
  },
93
166
  { waitForDrain: false },
94
167
  )
168
+ if (waitForDrain) {
169
+ await this._source.onEmpty({ signal: this.writeDrainAbort.signal })
170
+ }
95
171
  }
96
172
 
97
173
  // writeCallDataFromSource writes all call data from the iterable.
@@ -194,18 +270,27 @@ export class CommonRPC {
194
270
  }
195
271
 
196
272
  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()
273
+ const remoteError = packet.error
274
+ ? new RemoteRPCError(this.service, this.method, packet.error)
275
+ : undefined
276
+ if (remoteError) {
277
+ this.remoteError ??= remoteError
278
+ this.invocationController.abort()
279
+ this.recordRemoteTerminal(TerminalKind.TRANSPORT_LOST)
280
+ }
281
+ if (packet.complete && !remoteError) {
282
+ this.remoteCompleted = true
283
+ this.recordRemoteTerminal(TerminalKind.COMMITTED)
284
+ this._rpcDataSource.end(remoteError)
285
+ } else if (remoteError) {
286
+ this._rpcDataSource.end(remoteError)
203
287
  }
204
288
  }
205
289
 
206
290
  // handleCallCancel handles a CallCancel packet.
207
291
  public async handleCallCancel() {
208
- this.close(new Error(ERR_RPC_ABORT))
292
+ this.recordRemoteTerminal(TerminalKind.CANCELED)
293
+ await this.close(new Error(ERR_RPC_ABORT))
209
294
  }
210
295
 
211
296
  // close closes the call, optionally with an error.
@@ -214,6 +299,13 @@ export class CommonRPC {
214
299
  return
215
300
  }
216
301
  this.closed = err ?? true
302
+ if (!this.remoteError && err) {
303
+ this.remoteError = err
304
+ }
305
+ this.recordRemoteTerminal(
306
+ err ? TerminalKind.TRANSPORT_LOST : TerminalKind.CLOSED,
307
+ )
308
+ this.invocationController.abort()
217
309
  // note: this does nothing if _source is already ended.
218
310
  if (err && err.message) {
219
311
  await this.writeCallDataPacket(undefined, true, err.message, {
@@ -226,21 +318,20 @@ export class CommonRPC {
226
318
  this._rpcDataSource.end(err)
227
319
  }
228
320
 
229
- // _createSink returns a value for the sink field.
230
321
  private _createSink(): Sink<Source<Packet>> {
231
322
  return async (source: Source<Packet>) => {
232
323
  try {
233
324
  if (Symbol.asyncIterator in source) {
234
- // Handle async source
235
325
  for await (const msg of source) {
236
326
  await this.handlePacket(msg)
237
327
  }
238
328
  } else {
239
- // Handle sync source
240
329
  for (const msg of source) {
241
330
  await this.handlePacket(msg)
242
331
  }
243
332
  }
333
+ this.remoteSourceClosed = true
334
+ this.recordRemoteTerminal(TerminalKind.CLOSED)
244
335
  } catch (err) {
245
336
  this.close(err as Error)
246
337
  }
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 { TerminalKind } from './rpcproto.pb.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))
@@ -6,11 +6,10 @@ import (
6
6
  "math"
7
7
  "net"
8
8
 
9
- yamux "github.com/libp2p/go-yamux/v4"
9
+ yamux "github.com/libp2p/go-yamux/v5"
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
+ }
@@ -3,7 +3,7 @@ package srpc
3
3
  import (
4
4
  "context"
5
5
 
6
- yamux "github.com/libp2p/go-yamux/v4"
6
+ yamux "github.com/libp2p/go-yamux/v5"
7
7
  )
8
8
 
9
9
  // yamuxConn wraps a yamux.Session to implement MuxedConn.
@@ -120,8 +120,8 @@ struct PacketDefaultTypeInternal {
120
120
  PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT
121
121
  PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 PacketDefaultTypeInternal _Packet_default_instance_;
122
122
  } // namespace srpc
123
- static constexpr const ::_pb::EnumDescriptor* PROTOBUF_NONNULL* PROTOBUF_NULLABLE
124
- file_level_enum_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto = nullptr;
123
+ static const ::_pb::EnumDescriptor* PROTOBUF_NONNULL
124
+ file_level_enum_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto[1];
125
125
  static constexpr const ::_pb::ServiceDescriptor* PROTOBUF_NONNULL* PROTOBUF_NULLABLE
126
126
  file_level_service_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto = nullptr;
127
127
  const ::uint32_t
@@ -178,13 +178,18 @@ const char descriptor_table_protodef_github_2ecom_2faperturerobotics_2fstarpc_2f
178
178
  "service\030\001 \001(\t\022\022\n\nrpc_method\030\002 \001(\t\022\014\n\004dat"
179
179
  "a\030\003 \001(\014\022\024\n\014data_is_zero\030\004 \001(\010\"O\n\010CallDat"
180
180
  "a\022\014\n\004data\030\001 \001(\014\022\024\n\014data_is_zero\030\002 \001(\010\022\020\n"
181
- "\010complete\030\003 \001(\010\022\r\n\005error\030\004 \001(\tb\006proto3"
181
+ "\010complete\030\003 \001(\010\022\r\n\005error\030\004 \001(\t*\277\001\n\014Termi"
182
+ "nalKind\022\035\n\031TERMINAL_KIND_UNSPECIFIED\020\000\022\033"
183
+ "\n\027TERMINAL_KIND_COMMITTED\020\001\022\032\n\026TERMINAL_"
184
+ "KIND_CANCELED\020\002\022 \n\034TERMINAL_KIND_TRANSPO"
185
+ "RT_LOST\020\003\022\030\n\024TERMINAL_KIND_CLOSED\020\004\022\033\n\027T"
186
+ "ERMINAL_KIND_ABANDONED\020\005b\006proto3"
182
187
  };
183
188
  static ::absl::once_flag descriptor_table_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto_once;
184
189
  PROTOBUF_CONSTINIT const ::_pbi::DescriptorTable descriptor_table_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto = {
185
190
  false,
186
191
  false,
187
- 358,
192
+ 552,
188
193
  descriptor_table_protodef_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto,
189
194
  "github.com/aperturerobotics/starpc/srpc/rpcproto.proto",
190
195
  &descriptor_table_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto_once,
@@ -198,6 +203,12 @@ PROTOBUF_CONSTINIT const ::_pbi::DescriptorTable descriptor_table_github_2ecom_2
198
203
  file_level_service_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto,
199
204
  };
200
205
  namespace srpc {
206
+ const ::google::protobuf::EnumDescriptor* PROTOBUF_NONNULL TerminalKind_descriptor() {
207
+ ::google::protobuf::internal::AssignDescriptors(&descriptor_table_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto);
208
+ return file_level_enum_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto[0];
209
+ }
210
+ PROTOBUF_CONSTINIT const uint32_t TerminalKind_internal_data_[] = {
211
+ 393216u, 0u, };
201
212
  // ===================================================================
202
213
 
203
214
  class Packet::_Internal {