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.
- package/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
- package/dist/echo/echo_srpc.pb.d.ts +44 -1
- package/dist/echo/server.d.ts +9 -8
- package/dist/echo/server.js +6 -6
- package/dist/integration/cross-language/ts-client.js +5 -79
- package/dist/integration/cross-language/ts-server.js +4 -100
- package/dist/mock/mock_srpc.pb.d.ts +14 -1
- package/dist/srpc/channel.js +6 -3
- package/dist/srpc/channel.test.js +20 -1
- package/dist/srpc/client.d.ts +1 -3
- package/dist/srpc/client.js +2 -22
- package/dist/srpc/common-rpc.d.ts +1 -9
- package/dist/srpc/common-rpc.js +4 -62
- package/dist/srpc/handler.d.ts +12 -4
- package/dist/srpc/index.d.ts +2 -4
- package/dist/srpc/index.js +1 -3
- package/dist/srpc/invoker.d.ts +2 -2
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +0 -44
- package/dist/srpc/rpcproto.pb.js +0 -53
- package/dist/srpc/server-context.d.ts +11 -0
- package/dist/srpc/server-context.js +28 -0
- package/dist/srpc/server-rpc.js +3 -3
- package/dist/srpc/server.test.js +65 -2
- package/dist/srpc/watchdog.test.js +1 -0
- package/echo/echo.go +8 -8
- package/echo/echo.pb.go +12 -36
- 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/echo/echo_srpc.pb.ts +74 -0
- package/echo/server.go +1 -1
- package/echo/server.ts +24 -5
- package/go.mod +1 -1
- package/go.sum +2 -0
- package/integration/cross-language/go-client/main.go +4 -142
- package/integration/cross-language/go-server/main.go +2 -170
- package/integration/cross-language/run.bash +5 -105
- package/integration/cross-language/ts-client.ts +5 -90
- package/integration/cross-language/ts-server.ts +4 -112
- package/mock/mock.go +1 -1
- package/mock/mock.pb.go +12 -36
- 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/mock/mock_srpc.pb.ts +19 -1
- package/package.json +2 -2
- package/srpc/channel.test.ts +21 -1
- package/srpc/channel.ts +7 -3
- package/srpc/client-prefix.go +1 -1
- package/srpc/client-set.go +1 -1
- package/srpc/client.go +1 -1
- package/srpc/client.ts +4 -31
- package/srpc/common-rpc.go +15 -69
- package/srpc/common-rpc.ts +4 -77
- package/srpc/common-rpc_test.go +81 -0
- package/srpc/errors.go +26 -1
- package/srpc/handler.ts +54 -5
- package/srpc/index.ts +7 -4
- package/srpc/invoker-prefix.go +1 -1
- package/srpc/invoker.go +2 -2
- package/srpc/invoker.ts +20 -7
- package/srpc/message.go +1 -1
- package/srpc/msg-stream.go +1 -9
- package/srpc/mux-verbose.go +1 -1
- package/srpc/mux.go +1 -1
- package/srpc/rpcproto.pb.cc +4 -15
- package/srpc/rpcproto.pb.go +102 -419
- package/srpc/rpcproto.pb.h +0 -59
- package/srpc/rpcproto.pb.rs +0 -45
- package/srpc/rpcproto.pb.ts +0 -62
- package/srpc/rpcproto.proto +0 -16
- package/srpc/server-context.ts +55 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +4 -6
- package/srpc/server.test.ts +94 -1
- package/srpc/stream-pipe.go +1 -1
- package/srpc/stream-rwc.go +1 -1
- package/srpc/stream.go +0 -20
- package/srpc/watchdog.test.ts +1 -0
- package/dist/rpcstream/receipt.test.d.ts +0 -1
- package/dist/rpcstream/receipt.test.js +0 -41
- package/dist/srpc/call-receipt.d.ts +0 -17
- package/dist/srpc/call-receipt.js +0 -106
- package/dist/srpc/call-receipt.test.d.ts +0 -1
- package/dist/srpc/call-receipt.test.js +0 -375
- package/dist/srpc/server-invocation.d.ts +0 -17
- package/dist/srpc/server-invocation.js +0 -37
- package/integration/cross-language/go-server/fixture-owner_test.go +0 -127
- package/srpc/call-receipt-e2e_test.go +0 -111
- package/srpc/call-receipt.go +0 -112
- package/srpc/call-receipt.test.ts +0 -441
- package/srpc/call-receipt.ts +0 -131
- package/srpc/call-receipt_test.go +0 -536
- package/srpc/schema-ownership_test.go +0 -115
- package/srpc/server-invocation.go +0 -40
- 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
|
|
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
|
|
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
|
|
229
|
+
if (isMessagePort(this.channel)) {
|
|
226
230
|
this.channel.onmessage = null
|
|
227
231
|
this.channel.close()
|
|
228
232
|
} else {
|
package/srpc/client-prefix.go
CHANGED
package/srpc/client-set.go
CHANGED
package/srpc/client.go
CHANGED
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
|
-
|
|
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
|
|
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)
|
package/srpc/common-rpc.go
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 {
|
package/srpc/common-rpc.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
}
|
package/srpc/common-rpc_test.go
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
>(
|
|
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'
|
package/srpc/invoker-prefix.go
CHANGED
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 =
|
|
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 =
|
|
55
|
+
var _ Invoker = InvokerFunc(nil)
|