starpc 0.49.20 → 0.51.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/dist/integration/cross-language/ts-client.js +5 -79
- package/dist/integration/cross-language/ts-server.js +4 -84
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/srpc/client.d.ts +1 -3
- package/dist/srpc/client.js +2 -22
- package/dist/srpc/common-rpc.d.ts +1 -11
- package/dist/srpc/common-rpc.js +5 -66
- package/dist/srpc/handler.d.ts +1 -2
- package/dist/srpc/index.d.ts +0 -4
- package/dist/srpc/index.js +0 -2
- package/dist/srpc/invoker.d.ts +1 -2
- package/dist/srpc/server-rpc.js +1 -3
- package/dist/srpc/server.test.js +35 -1
- 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/server.go +1 -1
- package/go.mod +13 -12
- package/go.sum +26 -24
- package/integration/cross-language/go-client/main.go +4 -142
- package/integration/cross-language/go-server/main.go +3 -149
- package/integration/cross-language/run.bash +5 -105
- package/integration/cross-language/ts-client.ts +5 -90
- package/integration/cross-language/ts-server.ts +6 -94
- package/mock/index.ts +2 -2
- 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/package.json +19 -26
- package/srpc/accept.go +1 -1
- 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 +6 -78
- package/srpc/common-rpc_test.go +81 -0
- package/srpc/errors.go +26 -1
- package/srpc/handler.ts +1 -2
- package/srpc/index.ts +0 -4
- package/srpc/invoker-prefix.go +1 -1
- package/srpc/invoker.go +2 -2
- package/srpc/invoker.ts +5 -9
- 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/muxed-conn.go +1 -1
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.go +102 -323
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +1 -6
- package/srpc/server.test.ts +49 -1
- package/srpc/stream-pipe.go +1 -1
- package/srpc/stream-rwc.go +1 -1
- package/srpc/stream-yamux.go +1 -1
- package/srpc/stream.go +0 -20
- package/srpc/websocket.go +1 -1
- package/dist/srpc/call-receipt.d.ts +0 -17
- package/dist/srpc/call-receipt.js +0 -105
- package/dist/srpc/call-receipt.test.d.ts +0 -1
- package/dist/srpc/call-receipt.test.js +0 -374
- package/dist/srpc/server-invocation.d.ts +0 -17
- package/dist/srpc/server-invocation.js +0 -37
- package/srpc/call-receipt-e2e_test.go +0 -111
- package/srpc/call-receipt.go +0 -112
- package/srpc/call-receipt.test.ts +0 -438
- package/srpc/call-receipt.ts +0 -130
- package/srpc/call-receipt_test.go +0 -536
- package/srpc/server-invocation.go +0 -58
- package/srpc/server-invocation.ts +0 -82
package/srpc/server-rpc.go
CHANGED
|
@@ -98,7 +98,7 @@ func (r *ServerRPC) HandleCallStart(pkt *CallStart) error {
|
|
|
98
98
|
// invokeRPC invokes the RPC after CallStart is received.
|
|
99
99
|
func (r *ServerRPC) invokeRPC(serviceID, methodID string) {
|
|
100
100
|
// on the server side, the writer is closed by invokeRPC.
|
|
101
|
-
strm := NewMsgStream(
|
|
101
|
+
strm := NewMsgStream(r.ctx, r, r.cancelContext)
|
|
102
102
|
ok, err := r.invoker.InvokeMethod(serviceID, methodID, strm)
|
|
103
103
|
if err == nil && !ok {
|
|
104
104
|
err = ErrUnimplemented
|
package/srpc/server-rpc.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { Sink, Source } from 'it-stream-types'
|
|
|
3
3
|
import type { CallData, CallStart } from './rpcproto.pb.js'
|
|
4
4
|
import { CommonRPC } from './common-rpc.js'
|
|
5
5
|
import { InvokeFn } from './handler.js'
|
|
6
|
-
import { ServerInvocation } from './server-invocation.js'
|
|
7
6
|
import { LookupMethod } from './mux.js'
|
|
8
7
|
|
|
9
8
|
// ServerRPC is an ongoing RPC from the server side.
|
|
@@ -47,12 +46,8 @@ export class ServerRPC extends CommonRPC {
|
|
|
47
46
|
// invokeRPC starts invoking the RPC handler.
|
|
48
47
|
private async invokeRPC(invokeFn: InvokeFn) {
|
|
49
48
|
const dataSink = this._createDataSink()
|
|
50
|
-
const invocation = new ServerInvocation(
|
|
51
|
-
this.invocationSignal,
|
|
52
|
-
(ownerSignal) => this.waitTerminal(ownerSignal),
|
|
53
|
-
)
|
|
54
49
|
try {
|
|
55
|
-
await invokeFn(this.rpcDataSource, dataSink,
|
|
50
|
+
await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal)
|
|
56
51
|
} catch (err) {
|
|
57
52
|
this.close(err as Error)
|
|
58
53
|
}
|
package/srpc/server.test.ts
CHANGED
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
EchoerDefinition,
|
|
16
16
|
EchoerServer,
|
|
17
17
|
EchoerServiceName,
|
|
18
|
+
EchoMsg,
|
|
18
19
|
runClientTest,
|
|
19
20
|
} from '../echo/index.js'
|
|
20
21
|
import {
|
|
21
22
|
runAbortControllerTest,
|
|
22
23
|
runRpcStreamTest,
|
|
23
24
|
} from '../echo/client-test.js'
|
|
24
|
-
|
|
25
|
+
import type { EchoMsg as EchoMsgType } from '../echo/echo.pb.js'
|
|
25
26
|
describe('srpc server', () => {
|
|
26
27
|
let client: Client
|
|
27
28
|
|
|
@@ -77,6 +78,37 @@ describe('srpc server', () => {
|
|
|
77
78
|
it('should pass rpc stream tests', async () => {
|
|
78
79
|
await runRpcStreamTest(client)
|
|
79
80
|
})
|
|
81
|
+
it('passes the exact invocation signal after async request decode', async () => {
|
|
82
|
+
const controller = new AbortController()
|
|
83
|
+
let observedSignal: AbortSignal | undefined
|
|
84
|
+
const handler = createHandler(EchoerDefinition, {
|
|
85
|
+
Echo: async (request: EchoMsgType, signal?: AbortSignal) => {
|
|
86
|
+
observedSignal = signal
|
|
87
|
+
return request
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo')
|
|
91
|
+
if (!invokeFn) {
|
|
92
|
+
throw new Error('Echo method was not found')
|
|
93
|
+
}
|
|
94
|
+
const request = EchoMsg.create({ body: 'signal identity' })
|
|
95
|
+
const drained = Promise.withResolvers<void>()
|
|
96
|
+
await invokeFn(
|
|
97
|
+
(async function* () {
|
|
98
|
+
await Promise.resolve()
|
|
99
|
+
yield EchoMsg.toBinary(request)
|
|
100
|
+
})(),
|
|
101
|
+
async (source) => {
|
|
102
|
+
for await (const _data of source) {
|
|
103
|
+
// Drain the encoded response so the invocation pipeline completes.
|
|
104
|
+
}
|
|
105
|
+
drained.resolve()
|
|
106
|
+
},
|
|
107
|
+
controller.signal,
|
|
108
|
+
)
|
|
109
|
+
await drained.promise
|
|
110
|
+
expect(observedSignal).toBe(controller.signal)
|
|
111
|
+
})
|
|
80
112
|
|
|
81
113
|
it('keeps detached server-streaming responses open after request source completes', async () => {
|
|
82
114
|
const mux = createMux()
|
|
@@ -249,6 +281,22 @@ describe('srpc server', () => {
|
|
|
249
281
|
)
|
|
250
282
|
})
|
|
251
283
|
|
|
284
|
+
it('settles a late abort after the call has already closed', async () => {
|
|
285
|
+
const controller = new AbortController()
|
|
286
|
+
vi.spyOn(controller.signal, 'removeEventListener').mockImplementation(
|
|
287
|
+
() => undefined,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
await client.request(
|
|
291
|
+
EchoerServiceName,
|
|
292
|
+
'Echo',
|
|
293
|
+
new TextEncoder().encode('late abort'),
|
|
294
|
+
controller.signal,
|
|
295
|
+
)
|
|
296
|
+
controller.abort()
|
|
297
|
+
await Promise.resolve()
|
|
298
|
+
})
|
|
299
|
+
|
|
252
300
|
it('tears down passive channel close state', async () => {
|
|
253
301
|
const { port1, port2 } = new MessageChannel()
|
|
254
302
|
const opts: ChannelStreamOpts = { idleTimeoutMs: 1000, keepAliveMs: 1000 }
|
package/srpc/stream-pipe.go
CHANGED
package/srpc/stream-rwc.go
CHANGED
package/srpc/stream-yamux.go
CHANGED
package/srpc/stream.go
CHANGED
|
@@ -23,10 +23,6 @@ type Stream interface {
|
|
|
23
23
|
Close() error
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
type receiptTerminalStream interface {
|
|
27
|
-
receiptTerminalKind() (TerminalKind, bool)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
26
|
// StreamRecv is a stream that can receive typed messages.
|
|
31
27
|
//
|
|
32
28
|
// T is the response type.
|
|
@@ -73,14 +69,6 @@ func (s *streamWithClose) Close() error {
|
|
|
73
69
|
return err2
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
func (s *streamWithClose) receiptTerminalKind() (TerminalKind, bool) {
|
|
77
|
-
receipt, ok := s.Stream.(receiptTerminalStream)
|
|
78
|
-
if !ok {
|
|
79
|
-
return 0, false
|
|
80
|
-
}
|
|
81
|
-
return receipt.receiptTerminalKind()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
72
|
// streamWithContext is a Stream with a wrapped Context function.
|
|
85
73
|
type streamWithContext struct {
|
|
86
74
|
Stream
|
|
@@ -97,14 +85,6 @@ func (s *streamWithContext) Context() context.Context {
|
|
|
97
85
|
return s.ctx
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
func (s *streamWithContext) receiptTerminalKind() (TerminalKind, bool) {
|
|
101
|
-
receipt, ok := s.Stream.(receiptTerminalStream)
|
|
102
|
-
if !ok {
|
|
103
|
-
return 0, false
|
|
104
|
-
}
|
|
105
|
-
return receipt.receiptTerminalKind()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
88
|
// _ is a type assertion
|
|
109
89
|
var (
|
|
110
90
|
_ Stream = (*streamWithClose)(nil)
|
package/srpc/websocket.go
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ClientRPC } from './client-rpc.js';
|
|
2
|
-
export interface ReceiptRpc {
|
|
3
|
-
requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
|
|
4
|
-
}
|
|
5
|
-
export interface HeldCall {
|
|
6
|
-
readonly response: Uint8Array;
|
|
7
|
-
readonly receipt: CallReceipt;
|
|
8
|
-
}
|
|
9
|
-
export declare class CallReceipt {
|
|
10
|
-
#private;
|
|
11
|
-
constructor(call: ClientRPC, iterator: AsyncIterator<Uint8Array>);
|
|
12
|
-
get done(): Promise<void>;
|
|
13
|
-
get settled(): boolean;
|
|
14
|
-
commit(): Promise<void>;
|
|
15
|
-
abort(reason?: Error): Promise<void>;
|
|
16
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
17
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/// <reference lib="es2024.promise" />
|
|
2
|
-
import { ERR_RPC_ABORT } from './errors.js';
|
|
3
|
-
// CallReceipt holds a unary call until it is committed or aborted.
|
|
4
|
-
export class CallReceipt {
|
|
5
|
-
#call;
|
|
6
|
-
#iterator;
|
|
7
|
-
#terminalPromise;
|
|
8
|
-
#terminal;
|
|
9
|
-
#done;
|
|
10
|
-
#requestCommitted = false;
|
|
11
|
-
#resolveDone;
|
|
12
|
-
#rejectDone;
|
|
13
|
-
constructor(call, iterator) {
|
|
14
|
-
this.#call = call;
|
|
15
|
-
this.#iterator = iterator;
|
|
16
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
17
|
-
this.#done = promise;
|
|
18
|
-
this.#resolveDone = resolve;
|
|
19
|
-
this.#rejectDone = reject;
|
|
20
|
-
this.#done.catch(() => undefined);
|
|
21
|
-
this.#terminalPromise = this.#observeTerminal();
|
|
22
|
-
this.#terminalPromise.catch(() => undefined);
|
|
23
|
-
}
|
|
24
|
-
async #observeTerminal() {
|
|
25
|
-
try {
|
|
26
|
-
const result = await this.#iterator.next();
|
|
27
|
-
if (!result.done) {
|
|
28
|
-
throw new Error('unexpected trailing response data');
|
|
29
|
-
}
|
|
30
|
-
const terminal = this.#call.getTerminalKind();
|
|
31
|
-
if (terminal !== 'committed' ||
|
|
32
|
-
!this.#requestCommitted ||
|
|
33
|
-
this.#terminal !== 'committed') {
|
|
34
|
-
throw new Error('receipt closed before commit');
|
|
35
|
-
}
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
this.#rejectDone(err);
|
|
40
|
-
const error = err instanceof Error ? err : new Error('receipt terminal failed');
|
|
41
|
-
try {
|
|
42
|
-
await this.#call.close(error);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// The primary terminal error is already recorded on the receipt.
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// done resolves after committed close and rejects on terminal failure.
|
|
51
|
-
get done() {
|
|
52
|
-
return this.#done;
|
|
53
|
-
}
|
|
54
|
-
// settled reports whether a terminal transition has been claimed.
|
|
55
|
-
get settled() {
|
|
56
|
-
return this.#terminal !== undefined;
|
|
57
|
-
}
|
|
58
|
-
// commit sends request completion and waits for server finalization.
|
|
59
|
-
async commit() {
|
|
60
|
-
if (this.#terminal === 'aborted') {
|
|
61
|
-
throw new Error(ERR_RPC_ABORT);
|
|
62
|
-
}
|
|
63
|
-
if (this.#terminal === 'committed') {
|
|
64
|
-
return this.#done;
|
|
65
|
-
}
|
|
66
|
-
this.#terminal = 'committed';
|
|
67
|
-
this.#requestCommitted = true;
|
|
68
|
-
try {
|
|
69
|
-
await this.#call.writeCallData(undefined, true);
|
|
70
|
-
await this.#terminalPromise;
|
|
71
|
-
await this.#call.close();
|
|
72
|
-
this.#resolveDone();
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
await this.#call.close(err instanceof Error ? err : new Error('receipt commit failed'));
|
|
76
|
-
this.#rejectDone(err);
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// abort sends request cancellation and never rejects.
|
|
81
|
-
async abort(reason) {
|
|
82
|
-
if (this.#terminal !== undefined) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
this.#terminal = 'aborted';
|
|
86
|
-
let terminalError = reason;
|
|
87
|
-
try {
|
|
88
|
-
await this.#call.writeCallCancel(true);
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
terminalError ??= err;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
await this.#call.close();
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
terminalError ??= err;
|
|
98
|
-
}
|
|
99
|
-
this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT));
|
|
100
|
-
}
|
|
101
|
-
// asyncDispose aborts a receipt that has not reached a terminal.
|
|
102
|
-
async [Symbol.asyncDispose]() {
|
|
103
|
-
await this.abort();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/// <reference lib="es2024.promise" />
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { pushable } from 'it-pushable';
|
|
4
|
-
import { EchoMsg } from '../echo/echo.pb.js';
|
|
5
|
-
import { Client } from './client.js';
|
|
6
|
-
import { ClientRPC } from './client-rpc.js';
|
|
7
|
-
import { Server } from './server.js';
|
|
8
|
-
import { CallReceipt } from './call-receipt.js';
|
|
9
|
-
import { Packet } from './rpcproto.pb.js';
|
|
10
|
-
import { ServerRPC } from './server-rpc.js';
|
|
11
|
-
const response = new Uint8Array([1, 2, 3]);
|
|
12
|
-
describe('generated service compatibility', () => {
|
|
13
|
-
it('accepts plain abort signals and terminal-capable handlers', async () => {
|
|
14
|
-
const server = {
|
|
15
|
-
Echo: async (request, invocation) => {
|
|
16
|
-
void invocation;
|
|
17
|
-
return request;
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
const request = EchoMsg.create({ body: 'compatibility' });
|
|
21
|
-
const signal = new AbortController().signal;
|
|
22
|
-
await expect(server.Echo(request, signal)).resolves.toEqual(request);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
describe('held unary receipt', () => {
|
|
26
|
-
it('waits for a server completion acknowledgment', async () => {
|
|
27
|
-
const sent = [];
|
|
28
|
-
const terminal = Promise.withResolvers();
|
|
29
|
-
const client = new Client(async () => ({
|
|
30
|
-
source: responseSource(terminal.promise),
|
|
31
|
-
sink: async (source) => {
|
|
32
|
-
for await (const data of source) {
|
|
33
|
-
const packet = Packet.fromBinary(data);
|
|
34
|
-
sent.push(packet);
|
|
35
|
-
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
36
|
-
terminal.resolve();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
}));
|
|
41
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
42
|
-
const commit = held.receipt.commit();
|
|
43
|
-
expect(held.receipt.settled).toBe(true);
|
|
44
|
-
await expect(commit).resolves.toBeUndefined();
|
|
45
|
-
const terminalPackets = sent.filter((packet) => {
|
|
46
|
-
const body = packet.body;
|
|
47
|
-
return (body?.case === 'callCancel' ||
|
|
48
|
-
(body?.case === 'callData' && body.value.complete));
|
|
49
|
-
});
|
|
50
|
-
expect(terminalPackets).toHaveLength(1);
|
|
51
|
-
expect(terminalPackets[0]?.body?.case).toBe('callData');
|
|
52
|
-
await expect(held.receipt.done).resolves.toBeUndefined();
|
|
53
|
-
});
|
|
54
|
-
it('commits through a real TypeScript server', async () => {
|
|
55
|
-
const terminal = Promise.withResolvers();
|
|
56
|
-
const server = new Server(async () => {
|
|
57
|
-
return async (_source, dataSink, invocation) => {
|
|
58
|
-
await dataSink((async function* () {
|
|
59
|
-
yield response;
|
|
60
|
-
if (!invocation) {
|
|
61
|
-
throw new Error('missing invocation');
|
|
62
|
-
}
|
|
63
|
-
terminal.resolve(await invocation.waitTerminal(new AbortController().signal));
|
|
64
|
-
})());
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
const client = new Client(async () => {
|
|
68
|
-
const clientToServer = pushable({ objectMode: true });
|
|
69
|
-
const serverToClient = pushable({ objectMode: true });
|
|
70
|
-
server.handlePacketStream({
|
|
71
|
-
source: clientToServer,
|
|
72
|
-
sink: async (source) => {
|
|
73
|
-
for await (const packet of source) {
|
|
74
|
-
serverToClient.push(packet);
|
|
75
|
-
}
|
|
76
|
-
serverToClient.end();
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
return {
|
|
80
|
-
source: serverToClient,
|
|
81
|
-
sink: async (source) => {
|
|
82
|
-
for await (const packet of source) {
|
|
83
|
-
clientToServer.push(packet);
|
|
84
|
-
}
|
|
85
|
-
clientToServer.end();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
90
|
-
await expect(held.receipt.commit()).resolves.toBeUndefined();
|
|
91
|
-
await expect(terminal.promise).resolves.toBe('committed');
|
|
92
|
-
});
|
|
93
|
-
it('rejects commit after a bare remote close', async () => {
|
|
94
|
-
const call = new ClientRPC('test.Service', 'Unary');
|
|
95
|
-
const iterator = call.rpcDataSource[Symbol.asyncIterator]();
|
|
96
|
-
await call.handleCallData({
|
|
97
|
-
data: response,
|
|
98
|
-
dataIsZero: false,
|
|
99
|
-
complete: false,
|
|
100
|
-
error: '',
|
|
101
|
-
});
|
|
102
|
-
const first = await iterator.next();
|
|
103
|
-
expect(first.done).toBe(false);
|
|
104
|
-
const receipt = new CallReceipt(call, iterator);
|
|
105
|
-
await call.close();
|
|
106
|
-
await expect(receipt.commit()).rejects.toThrow();
|
|
107
|
-
await expect(receipt.done).rejects.toThrow('receipt closed before commit');
|
|
108
|
-
});
|
|
109
|
-
it('aborts a held receipt with one cancel packet', async () => {
|
|
110
|
-
const sent = [];
|
|
111
|
-
const terminal = Promise.withResolvers();
|
|
112
|
-
const cancelSeen = Promise.withResolvers();
|
|
113
|
-
const client = new Client(async () => ({
|
|
114
|
-
source: responseSource(terminal.promise),
|
|
115
|
-
sink: async (source) => {
|
|
116
|
-
for await (const data of source) {
|
|
117
|
-
const packet = Packet.fromBinary(data);
|
|
118
|
-
sent.push(packet);
|
|
119
|
-
if (packet.body?.case === 'callCancel') {
|
|
120
|
-
cancelSeen.resolve();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
}));
|
|
125
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
126
|
-
await held.receipt.abort();
|
|
127
|
-
await cancelSeen.promise;
|
|
128
|
-
const terminalPackets = sent.filter((packet) => packet.body?.case === 'callCancel');
|
|
129
|
-
expect(terminalPackets).toHaveLength(1);
|
|
130
|
-
await expect(held.receipt.done).rejects.toThrow();
|
|
131
|
-
});
|
|
132
|
-
it('allows exactly one concurrent commit or abort terminal', async () => {
|
|
133
|
-
const sent = [];
|
|
134
|
-
const terminal = Promise.withResolvers();
|
|
135
|
-
const client = new Client(async () => ({
|
|
136
|
-
source: responseSource(terminal.promise),
|
|
137
|
-
sink: async (source) => {
|
|
138
|
-
for await (const data of source) {
|
|
139
|
-
const packet = Packet.fromBinary(data);
|
|
140
|
-
sent.push(packet);
|
|
141
|
-
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
142
|
-
terminal.resolve();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
}));
|
|
147
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
148
|
-
const results = await Promise.allSettled([
|
|
149
|
-
held.receipt.commit(),
|
|
150
|
-
held.receipt.abort(),
|
|
151
|
-
]);
|
|
152
|
-
const terminalPackets = sent.filter((packet) => {
|
|
153
|
-
const body = packet.body;
|
|
154
|
-
return (body?.case === 'callCancel' ||
|
|
155
|
-
(body?.case === 'callData' && body.value.complete));
|
|
156
|
-
});
|
|
157
|
-
expect(terminalPackets).toHaveLength(1);
|
|
158
|
-
expect(results).toHaveLength(2);
|
|
159
|
-
});
|
|
160
|
-
it('rejects a trailing response datum instead of committing', async () => {
|
|
161
|
-
const terminal = Promise.withResolvers();
|
|
162
|
-
const client = new Client(async () => ({
|
|
163
|
-
source: (async function* () {
|
|
164
|
-
yield Packet.toBinary({
|
|
165
|
-
body: {
|
|
166
|
-
case: 'callData',
|
|
167
|
-
value: {
|
|
168
|
-
data: response,
|
|
169
|
-
dataIsZero: false,
|
|
170
|
-
complete: false,
|
|
171
|
-
error: '',
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
await terminal.promise;
|
|
176
|
-
yield Packet.toBinary({
|
|
177
|
-
body: {
|
|
178
|
-
case: 'callData',
|
|
179
|
-
value: {
|
|
180
|
-
data: new Uint8Array([9]),
|
|
181
|
-
dataIsZero: false,
|
|
182
|
-
complete: false,
|
|
183
|
-
error: '',
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
})(),
|
|
188
|
-
sink: async (source) => {
|
|
189
|
-
for await (const data of source) {
|
|
190
|
-
const packet = Packet.fromBinary(data);
|
|
191
|
-
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
192
|
-
terminal.resolve();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
}));
|
|
197
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
198
|
-
await expect(held.receipt.commit()).rejects.toThrow('unexpected trailing response data');
|
|
199
|
-
});
|
|
200
|
-
it('rejects done on a remote terminal error', async () => {
|
|
201
|
-
const client = new Client(async () => ({
|
|
202
|
-
source: (async function* () {
|
|
203
|
-
yield Packet.toBinary({
|
|
204
|
-
body: {
|
|
205
|
-
case: 'callData',
|
|
206
|
-
value: {
|
|
207
|
-
data: response,
|
|
208
|
-
dataIsZero: false,
|
|
209
|
-
complete: false,
|
|
210
|
-
error: '',
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
yield Packet.toBinary({
|
|
215
|
-
body: {
|
|
216
|
-
case: 'callData',
|
|
217
|
-
value: {
|
|
218
|
-
data: new Uint8Array(),
|
|
219
|
-
dataIsZero: false,
|
|
220
|
-
complete: true,
|
|
221
|
-
error: 'remote failure',
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
})(),
|
|
226
|
-
sink: async (source) => {
|
|
227
|
-
for await (const _data of source) {
|
|
228
|
-
void _data;
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
}));
|
|
232
|
-
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
233
|
-
await expect(held.receipt.done).rejects.toThrow('remote failure');
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
describe('server invocation terminal', () => {
|
|
237
|
-
it.each([
|
|
238
|
-
['explicit completion', 'committed'],
|
|
239
|
-
['cancel', 'canceled'],
|
|
240
|
-
['transport loss', 'transportLost'],
|
|
241
|
-
['remote error packet', 'transportLost'],
|
|
242
|
-
['bare close', 'closed'],
|
|
243
|
-
['remote error packet with completion', 'transportLost'],
|
|
244
|
-
])('%s is classified distinctly', async (_name, expected) => {
|
|
245
|
-
const captured = Promise.withResolvers();
|
|
246
|
-
const owner = new AbortController();
|
|
247
|
-
const observed = Promise.withResolvers();
|
|
248
|
-
const rpc = new ServerRPC(async () => {
|
|
249
|
-
return async (_source, _sink, invocation) => {
|
|
250
|
-
if (!invocation) {
|
|
251
|
-
throw new Error('missing invocation');
|
|
252
|
-
}
|
|
253
|
-
captured.resolve(invocation);
|
|
254
|
-
observed.resolve(await invocation.waitTerminal(owner.signal));
|
|
255
|
-
};
|
|
256
|
-
});
|
|
257
|
-
await rpc.handleCallStart({
|
|
258
|
-
rpcService: 'test.Service',
|
|
259
|
-
rpcMethod: 'Unary',
|
|
260
|
-
data: new Uint8Array(),
|
|
261
|
-
dataIsZero: true,
|
|
262
|
-
});
|
|
263
|
-
const invocation = await captured.promise;
|
|
264
|
-
if (expected === 'committed') {
|
|
265
|
-
await rpc.handleCallData({ complete: true });
|
|
266
|
-
}
|
|
267
|
-
else if (expected === 'canceled') {
|
|
268
|
-
await rpc.handleCallCancel();
|
|
269
|
-
}
|
|
270
|
-
else if (expected === 'transportLost') {
|
|
271
|
-
if (_name === 'remote error packet' ||
|
|
272
|
-
_name === 'remote error packet with completion') {
|
|
273
|
-
await rpc.handleCallData({
|
|
274
|
-
complete: _name === 'remote error packet with completion',
|
|
275
|
-
error: 'remote failure',
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
await rpc.close(new Error('transport loss'));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
await rpc.close();
|
|
284
|
-
}
|
|
285
|
-
await expect(observed.promise).resolves.toBe(expected);
|
|
286
|
-
owner.abort();
|
|
287
|
-
if (_name === 'remote error packet with completion') {
|
|
288
|
-
const rpcState = rpc;
|
|
289
|
-
if (!('remoteCompleted' in rpcState)) {
|
|
290
|
-
throw new Error('remote completion discriminator is missing');
|
|
291
|
-
}
|
|
292
|
-
expect(rpcState.remoteCompleted).toBe(false);
|
|
293
|
-
}
|
|
294
|
-
if (expected === 'closed' || expected === 'transportLost') {
|
|
295
|
-
expect(invocation.signal.aborted).toBe(true);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
it.each(['transport loss', 'cancel'])('keeps completion after %s', async (followUp) => {
|
|
299
|
-
const captured = Promise.withResolvers();
|
|
300
|
-
const owner = new AbortController();
|
|
301
|
-
const rpc = new ServerRPC(async () => {
|
|
302
|
-
return async (_source, _sink, invocation) => {
|
|
303
|
-
if (!invocation) {
|
|
304
|
-
throw new Error('missing invocation');
|
|
305
|
-
}
|
|
306
|
-
captured.resolve(invocation);
|
|
307
|
-
await invocation.waitTerminal(owner.signal);
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
await rpc.handleCallStart({
|
|
311
|
-
rpcService: 'test.Service',
|
|
312
|
-
rpcMethod: 'Unary',
|
|
313
|
-
data: new Uint8Array(),
|
|
314
|
-
dataIsZero: true,
|
|
315
|
-
});
|
|
316
|
-
const invocation = await captured.promise;
|
|
317
|
-
await rpc.handleCallData({ complete: true });
|
|
318
|
-
if (followUp === 'transport loss') {
|
|
319
|
-
await rpc.close(new Error('transport loss'));
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
await rpc.handleCallCancel();
|
|
323
|
-
}
|
|
324
|
-
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe('committed');
|
|
325
|
-
owner.abort();
|
|
326
|
-
});
|
|
327
|
-
it('returns abandoned only for owner cancellation without a remote terminal', async () => {
|
|
328
|
-
const captured = Promise.withResolvers();
|
|
329
|
-
const owner = new AbortController();
|
|
330
|
-
const rpc = new ServerRPC(async () => {
|
|
331
|
-
return async (_source, _sink, invocation) => {
|
|
332
|
-
if (!invocation) {
|
|
333
|
-
throw new Error('missing invocation');
|
|
334
|
-
}
|
|
335
|
-
captured.resolve(invocation);
|
|
336
|
-
await invocation.waitTerminal(owner.signal);
|
|
337
|
-
};
|
|
338
|
-
});
|
|
339
|
-
await rpc.handleCallStart({
|
|
340
|
-
rpcService: 'test.Service',
|
|
341
|
-
rpcMethod: 'Unary',
|
|
342
|
-
data: new Uint8Array(),
|
|
343
|
-
dataIsZero: true,
|
|
344
|
-
});
|
|
345
|
-
const invocation = await captured.promise;
|
|
346
|
-
owner.abort();
|
|
347
|
-
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe('abandoned');
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
async function* responseSource(terminal) {
|
|
351
|
-
yield Packet.toBinary({
|
|
352
|
-
body: {
|
|
353
|
-
case: 'callData',
|
|
354
|
-
value: {
|
|
355
|
-
data: response,
|
|
356
|
-
dataIsZero: false,
|
|
357
|
-
complete: false,
|
|
358
|
-
error: '',
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
await terminal;
|
|
363
|
-
yield Packet.toBinary({
|
|
364
|
-
body: {
|
|
365
|
-
case: 'callData',
|
|
366
|
-
value: {
|
|
367
|
-
data: new Uint8Array(),
|
|
368
|
-
dataIsZero: false,
|
|
369
|
-
complete: true,
|
|
370
|
-
error: '',
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
}
|