starpc 0.50.0 → 0.52.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 (100) 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/echo_srpc.pb.d.ts +44 -1
  4. package/dist/echo/server.d.ts +9 -8
  5. package/dist/echo/server.js +6 -6
  6. package/dist/integration/cross-language/ts-client.js +5 -79
  7. package/dist/integration/cross-language/ts-server.js +4 -100
  8. package/dist/mock/mock_srpc.pb.d.ts +14 -1
  9. package/dist/srpc/channel.js +6 -3
  10. package/dist/srpc/channel.test.js +20 -1
  11. package/dist/srpc/client.d.ts +1 -3
  12. package/dist/srpc/client.js +2 -22
  13. package/dist/srpc/common-rpc.d.ts +1 -9
  14. package/dist/srpc/common-rpc.js +4 -62
  15. package/dist/srpc/handler.d.ts +12 -4
  16. package/dist/srpc/index.d.ts +2 -4
  17. package/dist/srpc/index.js +1 -3
  18. package/dist/srpc/invoker.d.ts +2 -2
  19. package/dist/srpc/invoker.js +2 -2
  20. package/dist/srpc/rpcproto.pb.d.ts +0 -44
  21. package/dist/srpc/rpcproto.pb.js +0 -53
  22. package/dist/srpc/server-context.d.ts +11 -0
  23. package/dist/srpc/server-context.js +28 -0
  24. package/dist/srpc/server-rpc.js +3 -3
  25. package/dist/srpc/server.test.js +65 -2
  26. package/dist/srpc/watchdog.test.js +1 -0
  27. package/echo/echo.go +8 -8
  28. package/echo/echo.pb.go +12 -36
  29. package/echo/echo_srpc.pb.cpp +1 -1
  30. package/echo/echo_srpc.pb.go +1 -1
  31. package/echo/echo_srpc.pb.hpp +1 -1
  32. package/echo/echo_srpc.pb.rs +1 -1
  33. package/echo/echo_srpc.pb.ts +74 -0
  34. package/echo/server.go +1 -1
  35. package/echo/server.ts +24 -5
  36. package/go.mod +1 -1
  37. package/go.sum +2 -0
  38. package/integration/cross-language/go-client/main.go +4 -142
  39. package/integration/cross-language/go-server/main.go +2 -170
  40. package/integration/cross-language/run.bash +5 -105
  41. package/integration/cross-language/ts-client.ts +5 -90
  42. package/integration/cross-language/ts-server.ts +4 -112
  43. package/mock/mock.go +1 -1
  44. package/mock/mock.pb.go +12 -36
  45. package/mock/mock_srpc.pb.cpp +1 -1
  46. package/mock/mock_srpc.pb.go +1 -1
  47. package/mock/mock_srpc.pb.hpp +1 -1
  48. package/mock/mock_srpc.pb.rs +1 -1
  49. package/mock/mock_srpc.pb.ts +19 -1
  50. package/package.json +2 -2
  51. package/srpc/channel.test.ts +21 -1
  52. package/srpc/channel.ts +7 -3
  53. package/srpc/client-prefix.go +1 -1
  54. package/srpc/client-set.go +1 -1
  55. package/srpc/client.go +1 -1
  56. package/srpc/client.ts +4 -31
  57. package/srpc/common-rpc.go +15 -69
  58. package/srpc/common-rpc.ts +4 -77
  59. package/srpc/common-rpc_test.go +81 -0
  60. package/srpc/errors.go +26 -1
  61. package/srpc/handler.ts +54 -5
  62. package/srpc/index.ts +7 -4
  63. package/srpc/invoker-prefix.go +1 -1
  64. package/srpc/invoker.go +2 -2
  65. package/srpc/invoker.ts +20 -7
  66. package/srpc/message.go +1 -1
  67. package/srpc/msg-stream.go +1 -9
  68. package/srpc/mux-verbose.go +1 -1
  69. package/srpc/mux.go +1 -1
  70. package/srpc/rpcproto.pb.cc +4 -15
  71. package/srpc/rpcproto.pb.go +102 -419
  72. package/srpc/rpcproto.pb.h +0 -59
  73. package/srpc/rpcproto.pb.rs +0 -45
  74. package/srpc/rpcproto.pb.ts +0 -62
  75. package/srpc/rpcproto.proto +0 -16
  76. package/srpc/server-context.ts +55 -0
  77. package/srpc/server-rpc.go +1 -1
  78. package/srpc/server-rpc.ts +4 -6
  79. package/srpc/server.test.ts +94 -1
  80. package/srpc/stream-pipe.go +1 -1
  81. package/srpc/stream-rwc.go +1 -1
  82. package/srpc/stream.go +0 -20
  83. package/srpc/watchdog.test.ts +1 -0
  84. package/dist/rpcstream/receipt.test.d.ts +0 -1
  85. package/dist/rpcstream/receipt.test.js +0 -41
  86. package/dist/srpc/call-receipt.d.ts +0 -17
  87. package/dist/srpc/call-receipt.js +0 -106
  88. package/dist/srpc/call-receipt.test.d.ts +0 -1
  89. package/dist/srpc/call-receipt.test.js +0 -375
  90. package/dist/srpc/server-invocation.d.ts +0 -17
  91. package/dist/srpc/server-invocation.js +0 -37
  92. package/integration/cross-language/go-server/fixture-owner_test.go +0 -127
  93. package/srpc/call-receipt-e2e_test.go +0 -111
  94. package/srpc/call-receipt.go +0 -112
  95. package/srpc/call-receipt.test.ts +0 -441
  96. package/srpc/call-receipt.ts +0 -131
  97. package/srpc/call-receipt_test.go +0 -536
  98. package/srpc/schema-ownership_test.go +0 -115
  99. package/srpc/server-invocation.go +0 -40
  100. package/srpc/server-invocation.ts +0 -76
package/srpc/channel.ts CHANGED
@@ -26,6 +26,10 @@ export type ChannelPort =
26
26
  | MessagePort
27
27
  | { tx: BroadcastChannel; rx: BroadcastChannel }
28
28
 
29
+ function isMessagePort(channel: ChannelPort): channel is MessagePort {
30
+ return 'postMessage' in channel && 'start' in channel
31
+ }
32
+
29
33
  // ChannelStreamOpts are options for ChannelStream.
30
34
  export interface ChannelStreamOpts {
31
35
  // remoteOpen indicates that the remote already knows the channel is open.
@@ -149,7 +153,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
149
153
 
150
154
  // wire up the message handlers
151
155
  const onMessage = this.onMessage.bind(this)
152
- if (channel instanceof MessagePort) {
156
+ if (isMessagePort(channel)) {
153
157
  // MessagePort
154
158
  channel.onmessage = onMessage
155
159
  channel.start()
@@ -180,7 +184,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
180
184
  return
181
185
  }
182
186
  msg.from = this.localId
183
- if (this.channel instanceof MessagePort) {
187
+ if (isMessagePort(this.channel)) {
184
188
  this.channel.postMessage(msg)
185
189
  } else {
186
190
  this.channel.tx.postMessage(msg)
@@ -222,7 +226,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
222
226
  this.localWriteClosed = true
223
227
  this.remoteWriteClosed = true
224
228
  // close channels
225
- if (this.channel instanceof MessagePort) {
229
+ if (isMessagePort(this.channel)) {
226
230
  this.channel.onmessage = null
227
231
  this.channel.close()
228
232
  } else {
@@ -55,4 +55,4 @@ func (i *PrefixClient) stripCheckServiceIDPrefix(service string) (string, error)
55
55
  }
56
56
 
57
57
  // _ is a type assertion
58
- var _ Client = ((*PrefixClient)(nil))
58
+ var _ Client = (*PrefixClient)(nil)
@@ -75,4 +75,4 @@ func (c *ClientSet) execCall(ctx context.Context, doCall func(client Client) err
75
75
  }
76
76
 
77
77
  // _ is a type assertion
78
- var _ Client = ((*ClientSet)(nil))
78
+ var _ Client = (*ClientSet)(nil)
package/srpc/client.go CHANGED
@@ -97,4 +97,4 @@ func (c *client) NewStream(ctx context.Context, service, method string, firstMsg
97
97
  }
98
98
 
99
99
  // _ is a type assertion
100
- var _ Client = ((*client)(nil))
100
+ var _ Client = (*client)(nil)
package/srpc/client.ts CHANGED
@@ -8,10 +8,9 @@ import { ClientRPC } from './client-rpc.js'
8
8
  import { writeToPushable } from './pushable.js'
9
9
  import { decodePacketSource, encodePacketSource } from './packet.js'
10
10
  import { OpenStreamCtr } from './open-stream-ctr.js'
11
- import { CallReceipt } from './call-receipt.js'
12
- import type { HeldCall, ReceiptRpc } from './call-receipt.js'
11
+
13
12
  // Client implements the ts-proto Rpc interface with the drpcproto protocol.
14
- export class Client implements ProtoRpc, ReceiptRpc {
13
+ export class Client implements ProtoRpc {
15
14
  // openStreamCtr contains the OpenStreamFunc.
16
15
  private openStreamCtr: OpenStreamCtr
17
16
 
@@ -41,32 +40,6 @@ export class Client implements ProtoRpc, ReceiptRpc {
41
40
  throw err
42
41
  }
43
42
 
44
- // requestWithReceipt reads one response and retains the call terminal.
45
- public async requestWithReceipt(
46
- service: string,
47
- method: string,
48
- data: Uint8Array,
49
- abortSignal?: AbortSignal,
50
- ): Promise<HeldCall> {
51
- const call = await this.startRpc(service, method, data, abortSignal)
52
- const iterator = call.rpcDataSource[Symbol.asyncIterator]()
53
- try {
54
- const result = await iterator.next()
55
- if (result.done) {
56
- throw new Error('empty response')
57
- }
58
- return {
59
- response: result.value,
60
- receipt: new CallReceipt(call, iterator),
61
- }
62
- } catch (err) {
63
- await call.close(
64
- err instanceof Error ? err : new Error('receipt read failed'),
65
- )
66
- throw err
67
- }
68
- }
69
-
70
43
  // clientStreamingRequest starts a client side streaming request.
71
44
  public async clientStreamingRequest(
72
45
  service: string,
@@ -144,8 +117,8 @@ export class Client implements ProtoRpc, ReceiptRpc {
144
117
  const stream = await openStreamFn()
145
118
  const call = new ClientRPC(rpcService, rpcMethod)
146
119
  const onAbort = () => {
147
- call.writeCallCancel()
148
- call.close(new Error(ERR_RPC_ABORT))
120
+ void call.writeCallCancel().catch(() => undefined)
121
+ void call.close(new Error(ERR_RPC_ABORT)).catch(() => undefined)
149
122
  }
150
123
  abortSignal?.addEventListener('abort', onAbort, { once: true })
151
124
  pipe(stream, decodePacketSource, call, encodePacketSource, stream)
@@ -31,8 +31,8 @@ type commonRPC struct {
31
31
  writer PacketWriter
32
32
  // writerClosed is set after writer has been closed locally.
33
33
  writerClosed bool
34
- // localCompleting is set while the local handler is publishing its terminal
35
- // packet and closing the writer.
34
+ // localCompleting is set while the local handler is publishing its
35
+ // completion packet and closing the writer.
36
36
  localCompleting bool
37
37
  // localActive is set while the local handler goroutine may still be inside
38
38
  // user code. Resource owners use Wait as a lifetime barrier, so cancellation
@@ -48,14 +48,8 @@ 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
51
  // remoteCompleted is set only by an explicit remote CallData completion.
54
52
  remoteCompleted bool
55
- // remoteTerminal is the first valid remote terminal.
56
- remoteTerminal TerminalKind
57
- // remoteTerminalSet records whether remoteTerminal is valid.
58
- remoteTerminalSet bool
59
53
  }
60
54
 
61
55
  // initCommonRPC initializes the commonRPC.
@@ -124,41 +118,6 @@ func (c *commonRPC) Wait(ctx context.Context) error {
124
118
  }
125
119
  }
126
120
 
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
-
162
121
  // ReadOne reads a single message and returns.
163
122
  //
164
123
  // returns io.EOF if the stream ended without a packet.
@@ -236,9 +195,6 @@ func (c *commonRPC) HandleStreamClose(closeErr error) {
236
195
  // HandleCallCancel handles the call cancel packet.
237
196
  func (c *commonRPC) HandleCallCancel() error {
238
197
  locked := c.bcast.Lock()
239
- if (!c.dataClosed || !c.writerClosed) && !c.remoteTerminalSet {
240
- c.remoteCanceled = true
241
- }
242
198
  writer := c.handleStreamCloseLocked(&locked, context.Canceled)
243
199
  locked.Unlock()
244
200
  if writer != nil {
@@ -277,11 +233,7 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
277
233
  if complete {
278
234
  c.dataClosed = true
279
235
  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)
236
+ c.remoteCompleted = true
285
237
  }
286
238
  }
287
239
 
@@ -291,15 +243,6 @@ func (c *commonRPC) HandleCallData(pkt *CallData) error {
291
243
  return err
292
244
  }
293
245
 
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
246
  func (c *commonRPC) handleStreamCloseLocked(
304
247
  locked *broadcast.Locked,
305
248
  closeErr error,
@@ -307,19 +250,22 @@ func (c *commonRPC) handleStreamCloseLocked(
307
250
  if c.dataClosed && c.writerClosed {
308
251
  return nil
309
252
  }
253
+ // A peer that closes its side reports the end of the stream. Transports
254
+ // disagree on whether they surface that as io.EOF or as no error at all,
255
+ // and it means the same thing either way, so settle it here rather than in
256
+ // each transport. It says nothing about whether a completion preceded it.
257
+ if errors.Is(closeErr, io.EOF) {
258
+ closeErr = nil
259
+ }
310
260
  normalRemoteCloseAfterLocalComplete := closeErr == nil && (c.localCompleting || c.localDone)
311
261
  if closeErr != nil && c.remoteErr == nil {
312
262
  c.remoteErr = closeErr
313
263
  }
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)
264
+ // A clean close that arrives with no completion behind it leaves the call
265
+ // without a verdict. Reading io.EOF there says the stream ended in good
266
+ // order, which is the one thing we do not know.
267
+ if closeErr == nil && !normalRemoteCloseAfterLocalComplete && !c.remoteCompleted && c.remoteErr == nil {
268
+ c.remoteErr = ErrClosedBeforeCompletion
323
269
  }
324
270
  c.dataClosed = true
325
271
  if !normalRemoteCloseAfterLocalComplete {
@@ -3,12 +3,7 @@ import type { Sink, Source } from 'it-stream-types'
3
3
  import { pushable, type Pushable } from 'it-pushable'
4
4
  import { CompleteMessage } from '@aptre/protobuf-es-lite'
5
5
 
6
- import {
7
- Packet,
8
- TerminalKind,
9
- type CallData,
10
- type CallStart,
11
- } from './rpcproto.pb.js'
6
+ import { Packet, type CallData, type CallStart } from './rpcproto.pb.js'
12
7
  import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js'
13
8
 
14
9
  const maxBufferedOutgoingPackets = 1
@@ -39,27 +34,15 @@ export class CommonRPC {
39
34
 
40
35
  // closed indicates this rpc has been closed already.
41
36
  private closed?: true | Error
42
- // remoteCompleted is set only by an explicit remote CallData completion.
43
- private remoteCompleted = false
44
37
  // remoteError records a remote error or transport failure.
45
38
  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.
39
+ // invocationController cancels the server invocation when the RPC closes.
51
40
  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
41
 
56
42
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
57
43
  private readonly writeDrainAbort = new AbortController()
58
44
 
59
45
  constructor() {
60
- const { promise, resolve } = Promise.withResolvers<void>()
61
- this.terminalPromise = promise
62
- this.resolveTerminal = resolve
63
46
  this.sink = this._createSink()
64
47
  this.source = this._source
65
48
  this.rpcDataSource = this._rpcDataSource
@@ -70,57 +53,11 @@ export class CommonRPC {
70
53
  return this.closed ?? false
71
54
  }
72
55
 
73
- // invocationSignal is canceled when the RPC reaches a terminal.
56
+ // invocationSignal is canceled when the RPC closes.
74
57
  protected get invocationSignal(): AbortSignal {
75
58
  return this.invocationController.signal
76
59
  }
77
60
 
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
-
124
61
  // writeCallData writes the call data packet.
125
62
  public async writeCallData(
126
63
  data?: Uint8Array,
@@ -276,20 +213,15 @@ export class CommonRPC {
276
213
  if (remoteError) {
277
214
  this.remoteError ??= remoteError
278
215
  this.invocationController.abort()
279
- this.recordRemoteTerminal(TerminalKind.TRANSPORT_LOST)
280
216
  }
281
217
  if (packet.complete && !remoteError) {
282
- this.remoteCompleted = true
283
- this.recordRemoteTerminal(TerminalKind.COMMITTED)
284
218
  this._rpcDataSource.end(remoteError)
285
219
  } else if (remoteError) {
286
220
  this._rpcDataSource.end(remoteError)
287
221
  }
288
222
  }
289
-
290
- // handleCallCancel handles a CallCancel packet.
223
+ // handleCallCancel aborts the invocation and closes the call.
291
224
  public async handleCallCancel() {
292
- this.recordRemoteTerminal(TerminalKind.CANCELED)
293
225
  await this.close(new Error(ERR_RPC_ABORT))
294
226
  }
295
227
 
@@ -302,9 +234,6 @@ export class CommonRPC {
302
234
  if (!this.remoteError && err) {
303
235
  this.remoteError = err
304
236
  }
305
- this.recordRemoteTerminal(
306
- err ? TerminalKind.TRANSPORT_LOST : TerminalKind.CLOSED,
307
- )
308
237
  this.invocationController.abort()
309
238
  // note: this does nothing if _source is already ended.
310
239
  if (err && err.message) {
@@ -330,8 +259,6 @@ export class CommonRPC {
330
259
  await this.handlePacket(msg)
331
260
  }
332
261
  }
333
- this.remoteSourceClosed = true
334
- this.recordRemoteTerminal(TerminalKind.CLOSED)
335
262
  } catch (err) {
336
263
  this.close(err as Error)
337
264
  }
@@ -3,6 +3,7 @@ package srpc
3
3
  import (
4
4
  "bytes"
5
5
  "context"
6
+ "errors"
6
7
  "io"
7
8
  "sync"
8
9
  "sync/atomic"
@@ -423,3 +424,83 @@ func TestCommonRPCReadOneQueuedDoesNotAllocate(t *testing.T) {
423
424
  t.Fatalf("expected queued ReadOne to avoid allocations, got %f", allocs)
424
425
  }
425
426
  }
427
+
428
+ func TestRemoteCloseWithoutCompletionNamesTheMissingCompletion(t *testing.T) {
429
+ writer := &closeCountingPacketWriter{}
430
+ rpc := NewClientRPC(context.Background(), "service", "method")
431
+ if err := rpc.Start(writer, false, nil); err != nil {
432
+ t.Fatalf("start: %v", err)
433
+ }
434
+
435
+ // the transport ends the stream in good order with nothing behind it,
436
+ // which is what a dropped completion packet looks like from here.
437
+ rpc.HandleStreamClose(nil)
438
+
439
+ _, err := rpc.ReadOne()
440
+ if !errors.Is(err, ErrClosedBeforeCompletion) {
441
+ t.Fatalf("read after a truncated stream reported %v, want the missing completion", err)
442
+ }
443
+ if errors.Is(err, io.EOF) {
444
+ t.Fatalf("read after a truncated stream claims the call ended in good order: %v", err)
445
+ }
446
+ if !errors.Is(err, context.Canceled) {
447
+ t.Fatalf("read after a truncated stream stopped satisfying a cancellation check: %v", err)
448
+ }
449
+ if got := err.Error(); got == context.Canceled.Error() {
450
+ t.Fatalf("read after a truncated stream still claims a cancellation happened: %q", got)
451
+ }
452
+ if err := rpc.Wait(context.Background()); !errors.Is(err, ErrClosedBeforeCompletion) {
453
+ t.Fatalf("wait after a truncated stream reported %v, want the missing completion", err)
454
+ }
455
+ }
456
+
457
+ func TestRemoteCloseReportingEOFNamesTheMissingCompletion(t *testing.T) {
458
+ writer := &closeCountingPacketWriter{}
459
+ rpc := NewClientRPC(context.Background(), "service", "method")
460
+ if err := rpc.Start(writer, false, nil); err != nil {
461
+ t.Fatalf("start: %v", err)
462
+ }
463
+
464
+ // rpcstream surfaces the peer's close as io.EOF rather than as no error.
465
+ rpc.HandleStreamClose(io.EOF)
466
+
467
+ _, err := rpc.ReadOne()
468
+ if !errors.Is(err, ErrClosedBeforeCompletion) {
469
+ t.Fatalf("read after an EOF close reported %v, want the missing completion", err)
470
+ }
471
+ }
472
+
473
+ func TestRemoteCloseAfterCompletionStillEndsTheStream(t *testing.T) {
474
+ writer := &closeCountingPacketWriter{}
475
+ rpc := NewClientRPC(context.Background(), "service", "method")
476
+ if err := rpc.Start(writer, false, nil); err != nil {
477
+ t.Fatalf("start: %v", err)
478
+ }
479
+
480
+ if err := rpc.HandleCallData(&CallData{Complete: true}); err != nil {
481
+ t.Fatalf("handle completion: %v", err)
482
+ }
483
+ rpc.HandleStreamClose(nil)
484
+
485
+ if _, err := rpc.ReadOne(); !errors.Is(err, io.EOF) {
486
+ t.Fatalf("read after a completed call reported %v, want the end of the stream", err)
487
+ }
488
+ }
489
+
490
+ func TestClientRPCCloseReportsALocalCancellation(t *testing.T) {
491
+ writer := &closeCountingPacketWriter{}
492
+ rpc := NewClientRPC(context.Background(), "service", "method")
493
+ if err := rpc.Start(writer, false, nil); err != nil {
494
+ t.Fatalf("start: %v", err)
495
+ }
496
+
497
+ rpc.Close()
498
+
499
+ _, err := rpc.ReadOne()
500
+ if !errors.Is(err, context.Canceled) {
501
+ t.Fatalf("read after a local close reported %v, want a cancellation", err)
502
+ }
503
+ if errors.Is(err, ErrClosedBeforeCompletion) {
504
+ t.Fatalf("a close this side performed was blamed on the remote: %v", err)
505
+ }
506
+ }
package/srpc/errors.go CHANGED
@@ -1,6 +1,9 @@
1
1
  package srpc
2
2
 
3
- import "errors"
3
+ import (
4
+ "context"
5
+ "errors"
6
+ )
4
7
 
5
8
  var (
6
9
  // ErrReset is returned when a stream is reset.
@@ -24,3 +27,25 @@ var (
24
27
  // ErrNilWriter is returned if the rpc writer is nil.
25
28
  ErrNilWriter = errors.New("writer cannot be nil")
26
29
  )
30
+
31
+ // ErrClosedBeforeCompletion is the error a call reports when its stream closed
32
+ // without the remote sending a completion or an error. The call has no verdict:
33
+ // the handler may have finished with its answer lost in the transport, or it may
34
+ // never have run. Anything read from the call after that point fails with this.
35
+ //
36
+ // It unwraps to context.Canceled, which is what this case reported before the
37
+ // distinction existed, so a caller that tests for cancellation keeps working.
38
+ var ErrClosedBeforeCompletion error = closedBeforeCompletion{}
39
+
40
+ // closedBeforeCompletion carries ErrClosedBeforeCompletion.
41
+ type closedBeforeCompletion struct{}
42
+
43
+ // Error returns the error message.
44
+ func (closedBeforeCompletion) Error() string {
45
+ return "stream closed before the remote reported completion"
46
+ }
47
+
48
+ // Unwrap returns the cancellation this case reported historically.
49
+ func (closedBeforeCompletion) Unwrap() error {
50
+ return context.Canceled
51
+ }
package/srpc/handler.ts CHANGED
@@ -1,13 +1,58 @@
1
1
  import type { Sink, Source } from 'it-stream-types'
2
- import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js'
2
+ import { MethodKind, type MessageType } from '@aptre/protobuf-es-lite'
3
+ import type { MessageStream } from './message.js'
4
+ import {
5
+ type MethodDefinition,
6
+ ServiceDefinition,
7
+ ServiceMethodDefinitions,
8
+ } from './definition.js'
3
9
  import { createInvokeFn } from './invoker.js'
4
- import type { ServerInvocation } from './server-invocation.js'
10
+ import type { ServerContext } from './server-context.js'
11
+
12
+ type MessageOf<T> = T extends MessageType<infer M> ? M : never
13
+
14
+ type ServerMethod<T> =
15
+ T extends MethodDefinition<
16
+ infer Request,
17
+ infer Response,
18
+ infer Kind,
19
+ infer _Idempotency
20
+ >
21
+ ? Kind extends MethodKind.Unary
22
+ ? (
23
+ request: MessageOf<Request>,
24
+ abortSignal: AbortSignal,
25
+ context: ServerContext,
26
+ ) => Promise<MessageOf<Response>>
27
+ : Kind extends MethodKind.ServerStreaming
28
+ ? (
29
+ request: MessageOf<Request>,
30
+ abortSignal: AbortSignal,
31
+ context: ServerContext,
32
+ ) => MessageStream<MessageOf<Response>>
33
+ : Kind extends MethodKind.ClientStreaming
34
+ ? (
35
+ request: MessageStream<MessageOf<Request>>,
36
+ abortSignal: AbortSignal,
37
+ context: ServerContext,
38
+ ) => Promise<MessageOf<Response>>
39
+ : (
40
+ request: MessageStream<MessageOf<Request>>,
41
+ abortSignal: AbortSignal,
42
+ context: ServerContext,
43
+ ) => MessageStream<MessageOf<Response>>
44
+ : never
45
+
46
+ export type HandlerImplementation<T extends ServiceMethodDefinitions> =
47
+ Partial<{
48
+ [Method in keyof T]: ServerMethod<T[Method]>
49
+ }>
5
50
 
6
51
  // InvokeFn describes an SRPC call method invoke function.
7
52
  export type InvokeFn = (
8
53
  dataSource: Source<Uint8Array>,
9
54
  dataSink: Sink<Source<Uint8Array>>,
10
- invocation?: ServerInvocation,
55
+ context: ServerContext,
11
56
  ) => Promise<void>
12
57
 
13
58
  // Handler describes a SRPC call handler implementation.
@@ -63,7 +108,11 @@ export class StaticHandler implements Handler {
63
108
  // if serviceID is not set, uses the fullName of the service as the identifier.
64
109
  export function createHandler<
65
110
  T extends ServiceMethodDefinitions = ServiceMethodDefinitions,
66
- >(definition: ServiceDefinition<T>, impl: any, serviceID?: string): Handler {
111
+ >(
112
+ definition: ServiceDefinition<T>,
113
+ impl: HandlerImplementation<T>,
114
+ serviceID?: string,
115
+ ): Handler {
67
116
  // serviceID defaults to the full name of the service from Protobuf.
68
117
  serviceID = serviceID || definition.typeName
69
118
 
@@ -71,7 +120,7 @@ export function createHandler<
71
120
  const methodMap: MethodMap = {}
72
121
  for (const methodInfo of Object.values(definition.methods)) {
73
122
  const methodName = methodInfo.name
74
- let methodProto = impl[methodName]
123
+ let methodProto = impl[methodName as keyof T] as any
75
124
  if (!methodProto) {
76
125
  continue
77
126
  }
package/srpc/index.ts CHANGED
@@ -6,10 +6,6 @@ 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'
13
9
  export { Server } from './server.js'
14
10
  export { StreamConn } from './conn.js'
15
11
  export type { StreamConnParams, StreamHandler } from './conn.js'
@@ -87,3 +83,10 @@ export {
87
83
  } from './pushable.js'
88
84
  export { Watchdog } from './watchdog.js'
89
85
  export type { ProtoRpc } from './proto-rpc.js'
86
+
87
+ export {
88
+ createContextKey,
89
+ serverContextValue,
90
+ withServerContextValue,
91
+ } from './server-context.js'
92
+ export type { ContextKey, ServerContext } from './server-context.js'
@@ -37,4 +37,4 @@ func (i *PrefixInvoker) InvokeMethod(serviceID, methodID string, strm Stream) (b
37
37
  }
38
38
 
39
39
  // _ is a type assertion
40
- var _ Invoker = ((*PrefixInvoker)(nil))
40
+ var _ Invoker = (*PrefixInvoker)(nil)
package/srpc/invoker.go CHANGED
@@ -37,7 +37,7 @@ func (s InvokerSlice) InvokeMethod(serviceID, methodID string, strm Stream) (boo
37
37
  }
38
38
 
39
39
  // _ is a type assertion
40
- var _ Invoker = (InvokerSlice)(nil)
40
+ var _ Invoker = InvokerSlice(nil)
41
41
 
42
42
  // InvokerFunc is a function implementing InvokeMethod.
43
43
  type InvokerFunc func(serviceID, methodID string, strm Stream) (bool, error)
@@ -52,4 +52,4 @@ func (f InvokerFunc) InvokeMethod(serviceID, methodID string, strm Stream) (bool
52
52
  return f(serviceID, methodID, strm)
53
53
  }
54
54
 
55
- var _ Invoker = (InvokerFunc)(nil)
55
+ var _ Invoker = InvokerFunc(nil)