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.
- 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 +100 -4
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/receipt.test.d.ts +1 -0
- package/dist/rpcstream/receipt.test.js +41 -0
- 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 +106 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +375 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -3
- package/dist/srpc/common-rpc.js +87 -10
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +3 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +45 -1
- package/dist/srpc/rpcproto.pb.js +60 -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/go.mod +13 -12
- package/go.sum +24 -24
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
- package/integration/cross-language/go-server/main.go +169 -4
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +115 -7
- package/mock/index.ts +2 -2
- 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 +19 -26
- package/srpc/accept.go +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +441 -0
- package/srpc/call-receipt.ts +131 -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 +104 -13
- 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 +9 -4
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.cc +15 -4
- package/srpc/rpcproto.pb.go +97 -1
- package/srpc/rpcproto.pb.h +59 -0
- package/srpc/rpcproto.pb.rs +45 -0
- package/srpc/rpcproto.pb.ts +90 -27
- package/srpc/rpcproto.proto +16 -0
- package/srpc/schema-ownership_test.go +115 -0
- package/srpc/server-invocation.go +40 -0
- package/srpc/server-invocation.ts +76 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream-yamux.go +1 -1
- package/srpc/stream.go +20 -0
- package/srpc/websocket.go +1 -1
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 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
|
-
|
|
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(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
|
package/srpc/common-rpc.ts
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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
|
-
|
|
198
|
-
this.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
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.
|
|
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
|
|
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
|
@@ -6,11 +6,10 @@ import (
|
|
|
6
6
|
"math"
|
|
7
7
|
"net"
|
|
8
8
|
|
|
9
|
-
yamux "github.com/libp2p/go-yamux/
|
|
9
|
+
yamux "github.com/libp2p/go-yamux/v5"
|
|
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/muxed-yamux.go
CHANGED
package/srpc/rpcproto.pb.cc
CHANGED
|
@@ -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
|
|
124
|
-
file_level_enum_descriptors_github_2ecom_2faperturerobotics_2fstarpc_2fsrpc_2frpcproto_2eproto
|
|
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(\
|
|
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
|
-
|
|
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 {
|