starpc 0.49.17 → 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 (73) 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 +134 -29
  56. package/srpc/common-rpc.ts +98 -8
  57. package/srpc/common-rpc_test.go +52 -0
  58. package/srpc/handler.ts +2 -0
  59. package/srpc/index.ts +4 -0
  60. package/srpc/invoker.ts +10 -5
  61. package/srpc/msg-stream.go +8 -0
  62. package/srpc/muxed-conn.go +8 -3
  63. package/srpc/muxed-conn_test.go +79 -0
  64. package/srpc/rpcproto.pb.go +1 -1
  65. package/srpc/rpcproto.pb.ts +28 -27
  66. package/srpc/server-invocation.go +58 -0
  67. package/srpc/server-invocation.ts +82 -0
  68. package/srpc/server-rpc-invoke.go +7 -0
  69. package/srpc/server-rpc-invoke_goscript.go +12 -0
  70. package/srpc/server-rpc-invoke_goscript_test.go +27 -0
  71. package/srpc/server-rpc.go +9 -2
  72. package/srpc/server-rpc.ts +6 -2
  73. package/srpc/stream.go +20 -0
@@ -34,6 +34,10 @@ type commonRPC struct {
34
34
  // localCompleting is set while the local handler is publishing its terminal
35
35
  // packet and closing the writer.
36
36
  localCompleting bool
37
+ // localActive is set while the local handler goroutine may still be inside
38
+ // user code. Resource owners use Wait as a lifetime barrier, so cancellation
39
+ // must not make Wait return while a handler can still touch mux-owned state.
40
+ localActive bool
37
41
  // localDone is set after the local handler has completed normally.
38
42
  localDone bool
39
43
  // dataQueue contains incoming data packets.
@@ -44,6 +48,14 @@ type commonRPC struct {
44
48
  dataClosed bool
45
49
  // remoteErr is an error set by the remote.
46
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
47
59
  }
48
60
 
49
61
  // initCommonRPC initializes the commonRPC.
@@ -73,6 +85,21 @@ func (c *commonRPC) Wait(ctx context.Context) error {
73
85
  locked := c.bcast.Lock()
74
86
  err = c.remoteErr
75
87
  rpcCanceled = c.ctx.Err() != nil
88
+ // A canceled stream tells the handler to stop, but it is not proof that
89
+ // the handler has returned. Keep waiting while localActive is true so a
90
+ // caller that releases resources after Wait cannot race in-flight user
91
+ // code still running on the canceled Stream context.
92
+ if c.localActive {
93
+ waitCh = locked.WaitCh()
94
+ locked.Unlock()
95
+
96
+ select {
97
+ case <-ctx.Done():
98
+ return context.Canceled
99
+ case <-waitCh:
100
+ continue
101
+ }
102
+ }
76
103
  localDone = c.localDone
77
104
  if err == nil && !rpcCanceled && !localDone {
78
105
  waitCh = locked.WaitCh()
@@ -97,6 +124,41 @@ func (c *commonRPC) Wait(ctx context.Context) error {
97
124
  }
98
125
  }
99
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
+
100
162
  // ReadOne reads a single message and returns.
101
163
  //
102
164
  // returns io.EOF if the stream ended without a packet.
@@ -145,43 +207,26 @@ func (c *commonRPC) ReadOne() ([]byte, error) {
145
207
 
146
208
  // WriteCallData writes a call data packet.
147
209
  func (c *commonRPC) WriteCallData(data []byte, dataIsZero, complete bool, err error) error {
148
- // Check if already completed
149
- if c.localCompleted.Load() {
150
- // If we're just marking completion and already completed, allow it (no-op)
151
- if complete && len(data) == 0 && !dataIsZero {
152
- 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
153
217
  }
154
- // Otherwise, return error for trying to send data after completion
218
+ } else if c.localCompleted.Load() {
155
219
  return ErrCompleted
156
220
  }
157
221
 
158
- // Mark as completed if this call completes the RPC
159
- if complete || err != nil {
160
- c.localCompleted.Store(true)
161
- }
162
-
163
222
  outPkt := NewCallDataPacket(data, len(data) == 0 && dataIsZero, complete, err)
164
223
  return c.writer.WritePacket(outPkt)
165
224
  }
166
225
 
167
226
  // HandleStreamClose handles the incoming stream closing w/ optional error.
168
227
  func (c *commonRPC) HandleStreamClose(closeErr error) {
169
- var writer PacketWriter
170
228
  locked := c.bcast.Lock()
171
- if c.dataClosed && c.writerClosed {
172
- locked.Unlock()
173
- return
174
- }
175
- normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
176
- if closeErr != nil && c.remoteErr == nil {
177
- c.remoteErr = closeErr
178
- }
179
- c.dataClosed = true
180
- if !normalRemoteCloseAfterLocalComplete {
181
- c.cancelContext()
182
- writer = c.closeWriterLocked()
183
- }
184
- locked.Broadcast()
229
+ writer := c.handleStreamCloseLocked(&locked, closeErr)
185
230
  locked.Unlock()
186
231
  if writer != nil {
187
232
  _ = writer.Close()
@@ -190,7 +235,15 @@ func (c *commonRPC) HandleStreamClose(closeErr error) {
190
235
 
191
236
  // HandleCallCancel handles the call cancel packet.
192
237
  func (c *commonRPC) HandleCallCancel() error {
193
- 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
+ }
194
247
  return nil
195
248
  }
196
249
 
@@ -212,14 +265,24 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
212
265
  c.dataQueue = append(c.dataQueue, data)
213
266
  }
214
267
 
268
+ pktErr := pkt.GetError()
215
269
  complete := pkt.GetComplete()
216
- if pktErr := pkt.GetError(); len(pktErr) != 0 {
270
+ if len(pktErr) != 0 {
217
271
  complete = true
218
- c.remoteErr = errors.New(pktErr)
272
+ if c.remoteErr == nil {
273
+ c.remoteErr = errors.New(pktErr)
274
+ }
219
275
  }
220
276
 
221
277
  if complete {
222
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
+ }
223
286
  }
224
287
 
225
288
  locked.Broadcast()
@@ -228,6 +291,47 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
228
291
  return err
229
292
  }
230
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
+
231
335
  // WriteCallCancel writes a call cancel packet.
232
336
  func (c *commonRPC) WriteCallCancel() error {
233
337
  // Use atomic swap to check and set completion atomically
@@ -276,6 +380,7 @@ func (c *commonRPC) finishLocalCompletion() {
276
380
  }
277
381
  locked = c.bcast.Lock()
278
382
  c.localCompleting = false
383
+ c.localActive = false
279
384
  c.localDone = true
280
385
  locked.Broadcast()
281
386
  locked.Unlock()
@@ -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
  }
@@ -232,6 +232,58 @@ func TestServerRPCRemoteCloseAfterLocalCompletionDoesNotCancelStreamContext(t *t
232
232
  }
233
233
  }
234
234
 
235
+ func TestServerRPCWaitDoesNotReturnUntilCanceledInvokeExits(t *testing.T) {
236
+ writer := newPacketRecordingWriter()
237
+ invoked := make(chan struct{})
238
+ ctxCanceled := make(chan struct{})
239
+ releaseInvoke := make(chan struct{})
240
+ rpc := NewServerRPC(context.Background(), InvokerFunc(func(serviceID, methodID string, strm Stream) (bool, error) {
241
+ close(invoked)
242
+ <-strm.Context().Done()
243
+ close(ctxCanceled)
244
+ <-releaseInvoke
245
+ return true, nil
246
+ }), writer)
247
+
248
+ if err := rpc.HandleCallStart(NewCallStartPacket("service", "method", nil, false).GetCallStart()); err != nil {
249
+ t.Fatalf("handle call start: %v", err)
250
+ }
251
+
252
+ select {
253
+ case <-invoked:
254
+ case <-time.After(time.Second):
255
+ t.Fatal("invoker did not start")
256
+ }
257
+
258
+ done := make(chan error, 1)
259
+ go func() {
260
+ done <- rpc.Wait(context.Background())
261
+ }()
262
+
263
+ rpc.cancelContext()
264
+ select {
265
+ case <-ctxCanceled:
266
+ case <-time.After(time.Second):
267
+ t.Fatal("invoke context was not canceled")
268
+ }
269
+
270
+ select {
271
+ case err := <-done:
272
+ t.Fatalf("wait returned before canceled invoke exited: %v", err)
273
+ default:
274
+ }
275
+
276
+ close(releaseInvoke)
277
+ select {
278
+ case err := <-done:
279
+ if err != nil {
280
+ t.Fatalf("wait after invoke exit: %v", err)
281
+ }
282
+ case <-time.After(time.Second):
283
+ t.Fatal("wait did not return after invoke exited")
284
+ }
285
+ }
286
+
235
287
  func TestServerRPCRemoteCloseDuringLocalCompletionDoesNotCancelStreamContext(t *testing.T) {
236
288
  writer := newBlockingPacketWriter()
237
289
  streamCtxCh := make(chan context.Context, 1)
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