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.
- package/README.md +1 -3
- package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
- package/dist/echo/echo.pb.d.ts +1 -1
- package/dist/echo/echo.pb.js +3 -3
- package/dist/integration/cross-language/ts-client.js +84 -11
- package/dist/integration/cross-language/ts-server.js +84 -4
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
- package/dist/rpcstream/rpcstream.pb.js +14 -8
- package/dist/srpc/call-receipt.d.ts +17 -0
- package/dist/srpc/call-receipt.js +105 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +374 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -1
- package/dist/srpc/common-rpc.js +86 -6
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +2 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +1 -1
- package/dist/srpc/rpcproto.pb.js +7 -7
- package/dist/srpc/server-invocation.d.ts +17 -0
- package/dist/srpc/server-invocation.js +37 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/echo/echo.pb.go +1 -1
- package/echo/echo.pb.ts +6 -5
- package/echo/echo_srpc.pb.cpp +1 -1
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/echo_srpc.pb.hpp +1 -1
- package/echo/echo_srpc.pb.rs +1 -1
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/main.go +146 -3
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +94 -6
- package/mock/mock.pb.go +1 -1
- package/mock/mock.pb.ts +6 -5
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +1 -1
- package/package.json +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +438 -0
- package/srpc/call-receipt.ts +130 -0
- package/srpc/call-receipt_test.go +536 -0
- package/srpc/client-rpc.go +1 -8
- package/srpc/client.ts +29 -2
- package/srpc/common-rpc.go +114 -29
- package/srpc/common-rpc.ts +98 -8
- package/srpc/handler.ts +2 -0
- package/srpc/index.ts +4 -0
- package/srpc/invoker.ts +10 -5
- package/srpc/msg-stream.go +8 -0
- package/srpc/muxed-conn.go +8 -3
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/rpcproto.pb.go +1 -1
- package/srpc/rpcproto.pb.ts +28 -27
- package/srpc/server-invocation.go +58 -0
- package/srpc/server-invocation.ts +82 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream.go +20 -0
package/srpc/common-rpc.go
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
270
|
+
if len(pktErr) != 0 {
|
|
236
271
|
complete = true
|
|
237
|
-
c.remoteErr
|
|
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
|
package/srpc/common-rpc.ts
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
new RemoteRPCError(this.service, this.method, packet.error)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
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.
|
|
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
|
|
19
|
-
| ((
|
|
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
|
}
|
package/srpc/msg-stream.go
CHANGED
|
@@ -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))
|
package/srpc/muxed-conn.go
CHANGED
|
@@ -9,8 +9,7 @@ import (
|
|
|
9
9
|
yamux "github.com/libp2p/go-yamux/v4"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
|
|
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
|
+
}
|
package/srpc/rpcproto.pb.go
CHANGED
package/srpc/rpcproto.pb.ts
CHANGED
|
@@ -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
|
|
6
|
-
import { createMessageType
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
173
|
+
] satisfies readonly PartialFieldInfo[],
|
|
173
174
|
packedByDefault: true,
|
|
174
175
|
})
|