starpc 0.51.0 → 0.52.1
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/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- 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/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/mock/mock_srpc.pb.d.ts +14 -1
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/channel.js +6 -3
- package/dist/srpc/channel.test.js +20 -1
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/handler.d.ts +12 -3
- package/dist/srpc/index.d.ts +2 -0
- package/dist/srpc/index.js +1 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server-context.d.ts +11 -0
- package/dist/srpc/server-context.js +28 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +78 -6
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/srpc/watchdog.test.js +1 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.ts +74 -0
- package/echo/echo_srpc.py +306 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/server.ts +24 -5
- package/go.mod +2 -2
- package/go.sum +14 -0
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.pb.ts +19 -1
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +20 -6
- package/srpc/__init__.py +0 -0
- package/srpc/channel.test.ts +21 -1
- package/srpc/channel.ts +7 -3
- package/srpc/client.ts +20 -4
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/handler.ts +54 -4
- package/srpc/index.ts +7 -0
- package/srpc/invoker.ts +23 -6
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-context.ts +55 -0
- package/srpc/server-rpc.ts +4 -1
- package/srpc/server.test.ts +100 -5
- package/srpc/server.ts +22 -6
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
- package/srpc/watchdog.test.ts +1 -0
|
@@ -1,48 +1,51 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
|
-
import { pipe } from 'it-pipe';
|
|
3
2
|
import { pushable } from 'it-pushable';
|
|
4
3
|
import { Client } from '../../srpc/client.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
4
|
+
import { runClientTest, runAbortControllerTest, runRpcStreamTest, } from '../../echo/client-test.js';
|
|
5
|
+
import { EchoerClient } from '../../echo/echo_srpc.pb.js';
|
|
6
|
+
import { tcpSocketToPacketStream } from './tcp-packet-stream.js';
|
|
7
|
+
async function runEchoBidiStreamTest(client) {
|
|
8
|
+
const request = pushable({ objectMode: true });
|
|
9
|
+
const stream = new EchoerClient(client).EchoBidiStream(request);
|
|
10
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
11
|
+
const initial = await iterator.next();
|
|
12
|
+
if (initial.done || initial.value.body !== 'hello from server') {
|
|
13
|
+
throw new Error('expected initial bidi message "hello from server"');
|
|
14
|
+
}
|
|
15
|
+
const body = 'hello from TypeScript bidi client';
|
|
16
|
+
request.push({ body });
|
|
17
|
+
request.end();
|
|
18
|
+
const echo = await iterator.next();
|
|
19
|
+
if (echo.done || echo.value.body !== body) {
|
|
20
|
+
throw new Error(`expected bidi echo ${JSON.stringify(body)}`);
|
|
21
|
+
}
|
|
22
|
+
const terminal = await iterator.next();
|
|
23
|
+
if (!terminal.done) {
|
|
24
|
+
throw new Error('expected bidi stream to terminate after input closes');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseAddr(addr) {
|
|
28
|
+
const match = /^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/.exec(addr);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`invalid host:port address: ${addr}`);
|
|
31
|
+
}
|
|
32
|
+
const port = Number(match[3]);
|
|
33
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
34
|
+
throw new Error(`invalid port: ${match[3]}`);
|
|
35
|
+
}
|
|
36
|
+
return { host: match[1] ?? match[2], port };
|
|
37
37
|
}
|
|
38
38
|
async function main() {
|
|
39
|
-
const
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const lifecycle = args.includes('lifecycle');
|
|
41
|
+
const nested = args.includes('--nested') || args.includes('--nested-release');
|
|
42
|
+
const nestedRelease = args.includes('--nested-release');
|
|
43
|
+
const addr = args.find((arg) => arg !== 'lifecycle' && arg !== '--nested' && arg !== '--nested-release');
|
|
40
44
|
if (!addr) {
|
|
41
|
-
console.error('usage: ts-client <host:port>');
|
|
45
|
+
console.error('usage: ts-client [--nested] [--nested-release] [lifecycle] <host:port>');
|
|
42
46
|
process.exit(1);
|
|
43
47
|
}
|
|
44
|
-
const
|
|
45
|
-
const port = parseInt(portStr, 10);
|
|
48
|
+
const { host, port } = parseAddr(addr);
|
|
46
49
|
const openStream = async () => {
|
|
47
50
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
48
51
|
const socket = net.connect(port, host, () => {
|
|
@@ -54,6 +57,16 @@ async function main() {
|
|
|
54
57
|
const client = new Client(openStream);
|
|
55
58
|
console.log('Running client test via TCP...');
|
|
56
59
|
await runClientTest(client);
|
|
60
|
+
console.log('Running EchoBidiStream test via TCP...');
|
|
61
|
+
await runEchoBidiStreamTest(client);
|
|
62
|
+
if (lifecycle) {
|
|
63
|
+
console.log('Running abort controller test via TCP...');
|
|
64
|
+
await runAbortControllerTest(client);
|
|
65
|
+
}
|
|
66
|
+
if (nested) {
|
|
67
|
+
console.log('Running RpcStream test via TCP...');
|
|
68
|
+
await runRpcStreamTest(client, nestedRelease);
|
|
69
|
+
}
|
|
57
70
|
console.log('All tests passed.');
|
|
58
71
|
}
|
|
59
72
|
process.on('unhandledRejection', (ev) => {
|
|
@@ -1,43 +1,8 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
|
-
import { pipe } from 'it-pipe';
|
|
3
|
-
import { pushable } from 'it-pushable';
|
|
4
2
|
import { createMux, createHandler, Server } from '../../srpc/index.js';
|
|
5
|
-
import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
|
|
6
|
-
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
|
|
7
3
|
import { EchoerServer } from '../../echo/index.js';
|
|
8
4
|
import { EchoerDefinition } from '../../echo/echo_srpc.pb.js';
|
|
9
|
-
|
|
10
|
-
// Each Uint8Array in source/sink is one packet (no length prefix).
|
|
11
|
-
function tcpSocketToPacketStream(socket) {
|
|
12
|
-
// Source: read from socket, strip length prefix, yield individual packets.
|
|
13
|
-
const socketSource = async function* () {
|
|
14
|
-
const source = pushable({ objectMode: true });
|
|
15
|
-
socket.on('data', (data) => {
|
|
16
|
-
source.push(new Uint8Array(data));
|
|
17
|
-
});
|
|
18
|
-
socket.on('end', () => source.end());
|
|
19
|
-
socket.on('error', (err) => source.end(err));
|
|
20
|
-
socket.on('close', () => source.end());
|
|
21
|
-
yield* pipe(source, parseLengthPrefixTransform(), combineUint8ArrayListTransform());
|
|
22
|
-
};
|
|
23
|
-
return {
|
|
24
|
-
source: socketSource(),
|
|
25
|
-
sink: async (source) => {
|
|
26
|
-
for await (const chunk of pipe(source, prependLengthPrefixTransform())) {
|
|
27
|
-
const data = chunk instanceof Uint8Array ? chunk : chunk.subarray();
|
|
28
|
-
await new Promise((resolve, reject) => {
|
|
29
|
-
socket.write(data, (err) => {
|
|
30
|
-
if (err)
|
|
31
|
-
reject(err);
|
|
32
|
-
else
|
|
33
|
-
resolve();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
socket.end();
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
5
|
+
import { tcpSocketToPacketStream } from './tcp-packet-stream.js';
|
|
41
6
|
const mux = createMux();
|
|
42
7
|
const server = new Server(mux.lookupMethod);
|
|
43
8
|
const echoer = new EchoerServer(server);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MockMsg } from './mock.pb.js';
|
|
2
2
|
import { MethodKind } from '@aptre/protobuf-es-lite';
|
|
3
|
-
import { ProtoRpc } from 'starpc';
|
|
3
|
+
import { ProtoRpc, ServerContext } from 'starpc';
|
|
4
4
|
/**
|
|
5
5
|
* Mock service mocks some RPCs for the e2e tests.
|
|
6
6
|
*
|
|
@@ -35,6 +35,19 @@ export interface Mock {
|
|
|
35
35
|
*/
|
|
36
36
|
MockRequest(request: MockMsg, abortSignal?: AbortSignal): Promise<MockMsg>;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Mock service mocks some RPCs for the e2e tests.
|
|
40
|
+
*
|
|
41
|
+
* @generated from service e2e.mock.Mock
|
|
42
|
+
*/
|
|
43
|
+
export interface MockHandler {
|
|
44
|
+
/**
|
|
45
|
+
* MockRequest runs a mock unary request.
|
|
46
|
+
*
|
|
47
|
+
* @generated from rpc e2e.mock.Mock.MockRequest
|
|
48
|
+
*/
|
|
49
|
+
MockRequest(request: MockMsg, abortSignal: AbortSignal, context: ServerContext): Promise<MockMsg>;
|
|
50
|
+
}
|
|
38
51
|
export declare const MockServiceName: "e2e.mock.Mock";
|
|
39
52
|
export declare class MockClient implements Mock {
|
|
40
53
|
private readonly rpc;
|
|
@@ -15,7 +15,11 @@ export declare class RpcStream implements PacketStream {
|
|
|
15
15
|
readonly sink: Sink<Source<Uint8Array>, Promise<void>>;
|
|
16
16
|
private readonly _packetRx;
|
|
17
17
|
private readonly _packetTx;
|
|
18
|
-
|
|
18
|
+
private readonly _termination;
|
|
19
|
+
private readonly _cancelRpc;
|
|
20
|
+
constructor(packetTx: Pushable<Message<RpcStreamPacket>>, packetRx: AsyncIterator<Message<RpcStreamPacket>>, cancelRpc?: () => void);
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
abort(err: Error): void;
|
|
19
23
|
private _createSink;
|
|
20
24
|
private _createSource;
|
|
21
25
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
|
+
import { closeIterator, sourceIterator, TerminationGate, } from '../srpc/termination.js';
|
|
2
3
|
// openRpcStream attempts to open a stream over a RPC call.
|
|
3
4
|
// if waitAck is set, waits for the remote to ack the stream before returning.
|
|
4
5
|
export async function openRpcStream(componentId, caller, waitAck) {
|
|
@@ -33,7 +34,7 @@ export async function openRpcStream(componentId, caller, waitAck) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
// build & return the data stream
|
|
36
|
-
return new RpcStream(packetTx, packetIt);
|
|
37
|
+
return new RpcStream(packetTx, packetIt, () => closeIterator(packetIt));
|
|
37
38
|
}
|
|
38
39
|
// buildRpcStreamOpenStream builds a OpenStream func with a RpcStream.
|
|
39
40
|
export function buildRpcStreamOpenStream(componentId, caller) {
|
|
@@ -87,7 +88,7 @@ export async function* handleRpcStream(packetRx, getter) {
|
|
|
87
88
|
// build the outgoing packet sink & the packet source
|
|
88
89
|
const packetTx = pushable({ objectMode: true });
|
|
89
90
|
// start the handler
|
|
90
|
-
const rpcStream = new RpcStream(packetTx, packetRx);
|
|
91
|
+
const rpcStream = new RpcStream(packetTx, packetRx, () => closeIterator(packetRx));
|
|
91
92
|
handler(rpcStream)
|
|
92
93
|
.catch((err) => packetTx.end(err))
|
|
93
94
|
.then(() => packetTx.end());
|
|
@@ -107,54 +108,100 @@ export class RpcStream {
|
|
|
107
108
|
_packetRx;
|
|
108
109
|
// _packetTx writes packets to the remote.
|
|
109
110
|
_packetTx;
|
|
111
|
+
_termination = new TerminationGate();
|
|
112
|
+
_cancelRpc;
|
|
110
113
|
// packetTx writes packets to the remote.
|
|
111
114
|
// packetRx receives packets from the remote.
|
|
112
|
-
constructor(packetTx, packetRx) {
|
|
115
|
+
constructor(packetTx, packetRx, cancelRpc = () => closeIterator(packetRx)) {
|
|
113
116
|
this._packetTx = packetTx;
|
|
114
117
|
this._packetRx = packetRx;
|
|
118
|
+
this._cancelRpc = cancelRpc;
|
|
115
119
|
this.sink = this._createSink();
|
|
116
120
|
this.source = this._createSource();
|
|
117
121
|
}
|
|
122
|
+
// close cleanly ends both directions of the stream.
|
|
123
|
+
async close() {
|
|
124
|
+
if (this._termination.terminate()) {
|
|
125
|
+
this._packetTx.end();
|
|
126
|
+
this._cancelRpc();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// abort ends both directions of the stream with err.
|
|
130
|
+
abort(err) {
|
|
131
|
+
if (this._termination.terminate(err)) {
|
|
132
|
+
this._packetTx.end(err);
|
|
133
|
+
this._cancelRpc();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
118
136
|
// _createSink initializes the sink field.
|
|
119
137
|
_createSink() {
|
|
120
138
|
return async (source) => {
|
|
139
|
+
const iterator = sourceIterator(source);
|
|
121
140
|
try {
|
|
122
|
-
|
|
141
|
+
while (true) {
|
|
142
|
+
const next = await this._termination.next(iterator);
|
|
143
|
+
if ('terminated' in next) {
|
|
144
|
+
if (next.error)
|
|
145
|
+
throw next.error;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if ('error' in next)
|
|
149
|
+
throw next.error;
|
|
150
|
+
if (next.result.done) {
|
|
151
|
+
this._packetTx.end();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this._termination.terminated)
|
|
155
|
+
return;
|
|
123
156
|
this._packetTx.push({
|
|
124
|
-
body: { case: 'data', value:
|
|
157
|
+
body: { case: 'data', value: next.result.value },
|
|
125
158
|
});
|
|
126
159
|
}
|
|
127
|
-
this._packetTx.end();
|
|
128
160
|
}
|
|
129
161
|
catch (err) {
|
|
130
|
-
|
|
162
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
163
|
+
this.abort(error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
closeIterator(iterator);
|
|
131
168
|
}
|
|
132
169
|
};
|
|
133
170
|
}
|
|
134
171
|
// _createSource initializes the source field.
|
|
135
172
|
_createSource() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
const packetRx = this._packetRx;
|
|
174
|
+
const termination = this._termination;
|
|
175
|
+
return (async function* () {
|
|
176
|
+
try {
|
|
177
|
+
while (true) {
|
|
178
|
+
const next = await termination.next(packetRx);
|
|
179
|
+
if ('terminated' in next) {
|
|
180
|
+
if (next.error)
|
|
181
|
+
throw next.error;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if ('error' in next)
|
|
185
|
+
throw next.error;
|
|
186
|
+
if (next.result.done)
|
|
187
|
+
return;
|
|
188
|
+
const body = next.result.value?.body;
|
|
189
|
+
if (!body)
|
|
190
|
+
continue;
|
|
191
|
+
switch (body.case) {
|
|
192
|
+
case 'ack':
|
|
193
|
+
if (body.value.error?.length)
|
|
194
|
+
throw new Error(body.value.error);
|
|
195
|
+
break;
|
|
196
|
+
case 'data':
|
|
197
|
+
yield body.value;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
156
200
|
}
|
|
157
201
|
}
|
|
158
|
-
|
|
202
|
+
finally {
|
|
203
|
+
closeIterator(packetRx);
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
159
206
|
}
|
|
160
207
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { pushable } from 'it-pushable';
|
|
3
|
+
import { RpcStream } from './rpcstream.js';
|
|
4
|
+
describe('RpcStream lifecycle', () => {
|
|
5
|
+
it('closes while its source is blocked', async () => {
|
|
6
|
+
const tx = pushable({ objectMode: true });
|
|
7
|
+
const rx = pushable({ objectMode: true });
|
|
8
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
9
|
+
const pending = stream.source.next();
|
|
10
|
+
await stream.close();
|
|
11
|
+
await expect(pending).resolves.toEqual({ done: true, value: undefined });
|
|
12
|
+
await expect(tx.next()).resolves.toEqual({ done: true, value: undefined });
|
|
13
|
+
});
|
|
14
|
+
it('aborts while its source is blocked', async () => {
|
|
15
|
+
const tx = pushable({ objectMode: true });
|
|
16
|
+
const rx = pushable({ objectMode: true });
|
|
17
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
18
|
+
const pending = stream.source.next();
|
|
19
|
+
const error = new Error('stopped');
|
|
20
|
+
stream.abort(error);
|
|
21
|
+
await expect(pending).rejects.toBe(error);
|
|
22
|
+
await expect(tx.next()).rejects.toBe(error);
|
|
23
|
+
});
|
|
24
|
+
it('closes while its sink source is blocked', async () => {
|
|
25
|
+
const tx = pushable({ objectMode: true });
|
|
26
|
+
const rx = pushable({ objectMode: true });
|
|
27
|
+
const input = pushable({ objectMode: true });
|
|
28
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
29
|
+
const pending = stream.sink(input);
|
|
30
|
+
await stream.close();
|
|
31
|
+
await expect(pending).resolves.toBeUndefined();
|
|
32
|
+
await expect(tx.next()).resolves.toEqual({ done: true, value: undefined });
|
|
33
|
+
});
|
|
34
|
+
it('aborts while its sink source is blocked', async () => {
|
|
35
|
+
const tx = pushable({ objectMode: true });
|
|
36
|
+
const rx = pushable({ objectMode: true });
|
|
37
|
+
const input = pushable({ objectMode: true });
|
|
38
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
39
|
+
const pending = stream.sink(input);
|
|
40
|
+
const error = new Error('stopped');
|
|
41
|
+
stream.abort(error);
|
|
42
|
+
await expect(pending).rejects.toBe(error);
|
|
43
|
+
await expect(tx.next()).rejects.toBe(error);
|
|
44
|
+
});
|
|
45
|
+
it('does not write input that becomes ready as the stream closes', async () => {
|
|
46
|
+
const tx = pushable({ objectMode: true });
|
|
47
|
+
const rx = pushable({ objectMode: true });
|
|
48
|
+
const input = pushable({ objectMode: true });
|
|
49
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
50
|
+
input.push(new Uint8Array([1]));
|
|
51
|
+
const pending = stream.sink(input);
|
|
52
|
+
await stream.close();
|
|
53
|
+
await expect(pending).resolves.toBeUndefined();
|
|
54
|
+
await expect(tx.next()).resolves.toEqual({ done: true, value: undefined });
|
|
55
|
+
});
|
|
56
|
+
it('does not close when its sink completes', async () => {
|
|
57
|
+
const tx = pushable({ objectMode: true });
|
|
58
|
+
const rx = pushable({ objectMode: true });
|
|
59
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
60
|
+
await stream.sink((async function* () { })());
|
|
61
|
+
rx.push({ body: { case: 'data', value: new Uint8Array([1]) } });
|
|
62
|
+
await expect(stream.source.next()).resolves.toMatchObject({ done: false });
|
|
63
|
+
});
|
|
64
|
+
it('finalizes a sink input iterator when closed', async () => {
|
|
65
|
+
const tx = pushable({ objectMode: true });
|
|
66
|
+
const rx = pushable({ objectMode: true });
|
|
67
|
+
const returned = vi.fn(() => Promise.resolve({ done: true, value: undefined }));
|
|
68
|
+
const input = {
|
|
69
|
+
[Symbol.asyncIterator]: () => ({
|
|
70
|
+
next: () => new Promise(() => { }),
|
|
71
|
+
return: returned,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
const stream = new RpcStream(tx, rx[Symbol.asyncIterator]());
|
|
75
|
+
const pending = stream.sink(input);
|
|
76
|
+
await stream.close();
|
|
77
|
+
await pending;
|
|
78
|
+
expect(returned).toHaveBeenCalledOnce();
|
|
79
|
+
});
|
|
80
|
+
it('cancels the outer RPC when aborted after sink completion', async () => {
|
|
81
|
+
const tx = pushable({ objectMode: true });
|
|
82
|
+
const returned = vi.fn(() => Promise.resolve({ done: true, value: undefined }));
|
|
83
|
+
const rx = {
|
|
84
|
+
next: () => new Promise(() => { }),
|
|
85
|
+
return: returned,
|
|
86
|
+
};
|
|
87
|
+
const stream = new RpcStream(tx, rx);
|
|
88
|
+
await stream.sink((async function* () { })());
|
|
89
|
+
stream.abort(new Error('stopped'));
|
|
90
|
+
expect(returned).toHaveBeenCalledOnce();
|
|
91
|
+
});
|
|
92
|
+
});
|
package/dist/srpc/channel.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
2
|
import { Watchdog } from './watchdog.js';
|
|
3
3
|
import { ERR_STREAM_IDLE } from './errors.js';
|
|
4
|
+
function isMessagePort(channel) {
|
|
5
|
+
return 'postMessage' in channel && 'start' in channel;
|
|
6
|
+
}
|
|
4
7
|
// ChannelStream implements a Stream over a BroadcastChannel duplex or MessagePort.
|
|
5
8
|
//
|
|
6
9
|
// NOTE: there is no way to tell if a BroadcastChannel or MessagePort is closed.
|
|
@@ -99,7 +102,7 @@ export class ChannelStream {
|
|
|
99
102
|
this._source = source;
|
|
100
103
|
// wire up the message handlers
|
|
101
104
|
const onMessage = this.onMessage.bind(this);
|
|
102
|
-
if (channel
|
|
105
|
+
if (isMessagePort(channel)) {
|
|
103
106
|
// MessagePort
|
|
104
107
|
channel.onmessage = onMessage;
|
|
105
108
|
channel.start();
|
|
@@ -124,7 +127,7 @@ export class ChannelStream {
|
|
|
124
127
|
return;
|
|
125
128
|
}
|
|
126
129
|
msg.from = this.localId;
|
|
127
|
-
if (this.channel
|
|
130
|
+
if (isMessagePort(this.channel)) {
|
|
128
131
|
this.channel.postMessage(msg);
|
|
129
132
|
}
|
|
130
133
|
else {
|
|
@@ -165,7 +168,7 @@ export class ChannelStream {
|
|
|
165
168
|
this.localWriteClosed = true;
|
|
166
169
|
this.remoteWriteClosed = true;
|
|
167
170
|
// close channels
|
|
168
|
-
if (this.channel
|
|
171
|
+
if (isMessagePort(this.channel)) {
|
|
169
172
|
this.channel.onmessage = null;
|
|
170
173
|
this.channel.close();
|
|
171
174
|
}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { pushable } from 'it-pushable';
|
|
3
3
|
import { ChannelStream } from './channel.js';
|
|
4
4
|
describe('ChannelStream', () => {
|
|
5
|
+
it('recognizes MessagePort implementations from another realm', () => {
|
|
6
|
+
const port = {
|
|
7
|
+
close: vi.fn(),
|
|
8
|
+
onmessage: null,
|
|
9
|
+
postMessage: vi.fn(),
|
|
10
|
+
start: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
const stream = new ChannelStream('client', port);
|
|
13
|
+
try {
|
|
14
|
+
expect(port.start).toHaveBeenCalledOnce();
|
|
15
|
+
expect(port.postMessage).toHaveBeenCalledWith({
|
|
16
|
+
ack: true,
|
|
17
|
+
from: 'client',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
stream.close();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
5
24
|
it('keeps MessagePort peer writes open after local source completes normally', async () => {
|
|
6
25
|
const { port1, port2 } = new MessageChannel();
|
|
7
26
|
const client = new ChannelStream('client', port1);
|
package/dist/srpc/client.js
CHANGED
|
@@ -82,13 +82,27 @@ export class Client {
|
|
|
82
82
|
const stream = await openStreamFn();
|
|
83
83
|
const call = new ClientRPC(rpcService, rpcMethod);
|
|
84
84
|
const onAbort = () => {
|
|
85
|
+
if (call.isClosed)
|
|
86
|
+
return;
|
|
87
|
+
const error = new Error(ERR_RPC_ABORT);
|
|
85
88
|
void call.writeCallCancel().catch(() => undefined);
|
|
86
|
-
|
|
89
|
+
stream.abort(error);
|
|
90
|
+
void call.close(error).catch(() => undefined);
|
|
87
91
|
};
|
|
88
92
|
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
89
|
-
pipe(stream, decodePacketSource, call, encodePacketSource, stream)
|
|
90
|
-
.
|
|
91
|
-
.
|
|
93
|
+
void pipe(stream, decodePacketSource, call, encodePacketSource, stream)
|
|
94
|
+
.then(async () => {
|
|
95
|
+
if (call.isClosed instanceof Error) {
|
|
96
|
+
stream.abort(call.isClosed);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await stream.close();
|
|
100
|
+
await call.close();
|
|
101
|
+
}, async (err) => {
|
|
102
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
103
|
+
stream.abort(error);
|
|
104
|
+
await call.close(error);
|
|
105
|
+
})
|
|
92
106
|
.finally(() => {
|
|
93
107
|
abortSignal?.removeEventListener('abort', onAbort);
|
|
94
108
|
});
|
|
@@ -101,6 +101,8 @@ describe('CommonRPC', () => {
|
|
|
101
101
|
const responseGate = deferred();
|
|
102
102
|
const response = new Uint8Array([7]);
|
|
103
103
|
const client = new Client(async () => ({
|
|
104
|
+
close: async () => { },
|
|
105
|
+
abort: () => { },
|
|
104
106
|
source: (async function* () {
|
|
105
107
|
await responseGate.promise;
|
|
106
108
|
yield Packet.toBinary({
|
package/dist/srpc/handler.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { MethodKind, type MessageType } from '@aptre/protobuf-es-lite';
|
|
3
|
+
import type { MessageStream } from './message.js';
|
|
4
|
+
import { type MethodDefinition, ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
|
|
5
|
+
import type { ServerContext } from './server-context.js';
|
|
6
|
+
type MessageOf<T> = T extends MessageType<infer M> ? M : never;
|
|
7
|
+
type ServerMethod<T> = T extends MethodDefinition<infer Request, infer Response, infer Kind, infer _Idempotency> ? Kind extends MethodKind.Unary ? (request: MessageOf<Request>, abortSignal: AbortSignal, context: ServerContext) => Promise<MessageOf<Response>> : Kind extends MethodKind.ServerStreaming ? (request: MessageOf<Request>, abortSignal: AbortSignal, context: ServerContext) => MessageStream<MessageOf<Response>> : Kind extends MethodKind.ClientStreaming ? (request: MessageStream<MessageOf<Request>>, abortSignal: AbortSignal, context: ServerContext) => Promise<MessageOf<Response>> : (request: MessageStream<MessageOf<Request>>, abortSignal: AbortSignal, context: ServerContext) => MessageStream<MessageOf<Response>> : never;
|
|
8
|
+
export type HandlerImplementation<T extends ServiceMethodDefinitions> = Partial<{
|
|
9
|
+
[Method in keyof T]: ServerMethod<T[Method]>;
|
|
10
|
+
}>;
|
|
11
|
+
export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, context: ServerContext) => Promise<void>;
|
|
4
12
|
export interface Handler {
|
|
5
13
|
getServiceID(): string;
|
|
6
14
|
getMethodIDs(): string[];
|
|
@@ -17,4 +25,5 @@ export declare class StaticHandler implements Handler {
|
|
|
17
25
|
getMethodIDs(): string[];
|
|
18
26
|
lookupMethod(serviceID: string, methodID: string): Promise<InvokeFn | null>;
|
|
19
27
|
}
|
|
20
|
-
export declare function createHandler<T extends ServiceMethodDefinitions = ServiceMethodDefinitions>(definition: ServiceDefinition<T>, impl:
|
|
28
|
+
export declare function createHandler<T extends ServiceMethodDefinitions = ServiceMethodDefinitions>(definition: ServiceDefinition<T>, impl: HandlerImplementation<T>, serviceID?: string): Handler;
|
|
29
|
+
export {};
|
package/dist/srpc/index.d.ts
CHANGED
|
@@ -27,3 +27,5 @@ export { HandleStreamCtr } from './handle-stream-ctr.js';
|
|
|
27
27
|
export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
|
|
28
28
|
export { Watchdog } from './watchdog.js';
|
|
29
29
|
export type { ProtoRpc } from './proto-rpc.js';
|
|
30
|
+
export { createContextKey, serverContextValue, withServerContextValue, } from './server-context.js';
|
|
31
|
+
export type { ContextKey, ServerContext } from './server-context.js';
|
package/dist/srpc/index.js
CHANGED
|
@@ -18,3 +18,4 @@ export { OpenStreamCtr } from './open-stream-ctr.js';
|
|
|
18
18
|
export { HandleStreamCtr } from './handle-stream-ctr.js';
|
|
19
19
|
export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
|
|
20
20
|
export { Watchdog } from './watchdog.js';
|
|
21
|
+
export { createContextKey, serverContextValue, withServerContextValue, } from './server-context.js';
|
package/dist/srpc/invoker.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MethodDefinition } from './definition.js';
|
|
2
|
+
import type { ServerContext } from './server-context.js';
|
|
2
3
|
import { InvokeFn } from './handler.js';
|
|
3
4
|
import type { MessageType, Message } from '@aptre/protobuf-es-lite';
|
|
4
5
|
import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite';
|
|
5
|
-
export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R,
|
|
6
|
+
export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, abortSignal: AbortSignal, context: ServerContext) => Promise<O>) | ((request: R, abortSignal: AbortSignal, context: ServerContext) => AsyncIterable<O>) | ((request: AsyncIterable<R>, abortSignal: AbortSignal, context: ServerContext) => Promise<O>) | ((request: AsyncIterable<R>, abortSignal: AbortSignal, context: ServerContext) => AsyncIterable<O>);
|
|
6
7
|
export declare function createInvokeFn<R extends Message<R>, O extends Message<O>>(methodInfo: MethodDefinition<MessageType<R>, MessageType<O>, MethodKind, MethodIdempotency | undefined>, methodProto: MethodProto<R, O>): InvokeFn;
|
package/dist/srpc/invoker.js
CHANGED
|
@@ -6,7 +6,7 @@ import { MethodKind } from '@aptre/protobuf-es-lite';
|
|
|
6
6
|
// createInvokeFn builds an InvokeFn from a method definition and a function prototype.
|
|
7
7
|
export function createInvokeFn(methodInfo, methodProto) {
|
|
8
8
|
const requestDecode = buildDecodeMessageTransform(methodInfo.I);
|
|
9
|
-
return async (dataSource, dataSink,
|
|
9
|
+
return async (dataSource, dataSink, context) => {
|
|
10
10
|
// responseSink is a Sink for response messages.
|
|
11
11
|
const responseSink = pushable({
|
|
12
12
|
objectMode: true,
|
|
@@ -34,7 +34,7 @@ export function createInvokeFn(methodInfo, methodProto) {
|
|
|
34
34
|
}
|
|
35
35
|
// Call the implementation.
|
|
36
36
|
try {
|
|
37
|
-
const responseObj = methodProto(requestArg,
|
|
37
|
+
const responseObj = methodProto(requestArg, context.signal, context);
|
|
38
38
|
if (!responseObj) {
|
|
39
39
|
throw new Error('return value was undefined');
|
|
40
40
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|