starpc 0.52.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/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- 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/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/client.js +18 -4
- package/dist/srpc/common-rpc.test.js +2 -0
- 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.js +19 -6
- package/dist/srpc/server.test.js +43 -0
- 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/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.py +306 -0
- package/echo/echo_srpc.pyi +85 -0
- 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.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +19 -5
- package/srpc/__init__.py +0 -0
- package/srpc/client.ts +20 -4
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.test.ts +2 -0
- 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.test.ts +50 -0
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Client } from '../srpc/index.js';
|
|
2
2
|
export declare function runClientTest(client: Client): Promise<void>;
|
|
3
3
|
export declare function runAbortControllerTest(client: Client): Promise<void>;
|
|
4
|
-
export declare function runRpcStreamTest(client: Client): Promise<void>;
|
|
4
|
+
export declare function runRpcStreamTest(client: Client, release?: boolean): Promise<void>;
|
package/dist/echo/client-test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Client, ERR_RPC_ABORT } from '../srpc/index.js';
|
|
2
2
|
import { EchoerClient } from './echo_srpc.pb.js';
|
|
3
3
|
import { pushable } from 'it-pushable';
|
|
4
|
-
import { buildRpcStreamOpenStream } from '../rpcstream/rpcstream.js';
|
|
4
|
+
import { buildRpcStreamOpenStream, openRpcStream, } from '../rpcstream/rpcstream.js';
|
|
5
5
|
export async function runClientTest(client) {
|
|
6
6
|
const demoServiceClient = new EchoerClient(client);
|
|
7
7
|
console.log('Calling Echo: unary call...');
|
|
@@ -77,8 +77,14 @@ export async function runAbortControllerTest(client) {
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
function requireError(error, label, fragments) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
if (!fragments.some((fragment) => message.includes(fragment))) {
|
|
83
|
+
throw new Error(`${label} returned unexpected error: ${message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
// runRpcStreamTest tests a RPCStream.
|
|
81
|
-
export async function runRpcStreamTest(client) {
|
|
87
|
+
export async function runRpcStreamTest(client, release = false) {
|
|
82
88
|
console.log('Calling RpcStream to open a RPC stream client...');
|
|
83
89
|
const service = new EchoerClient(client);
|
|
84
90
|
const openStreamFn = buildRpcStreamOpenStream('test', service.RpcStream.bind(service));
|
|
@@ -89,4 +95,106 @@ export async function runRpcStreamTest(client) {
|
|
|
89
95
|
console.log('rpc stream test: succeeded: response: ' + resp.body);
|
|
90
96
|
console.log('Running client test over RPC stream...');
|
|
91
97
|
await runClientTest(proxiedClient);
|
|
98
|
+
if (release) {
|
|
99
|
+
let unknownRejected = false;
|
|
100
|
+
try {
|
|
101
|
+
await openRpcStream('missing', service.RpcStream.bind(service), true);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
unknownRejected = true;
|
|
105
|
+
requireError(error, 'unknown component', ['unknown component: missing']);
|
|
106
|
+
}
|
|
107
|
+
if (!unknownRejected) {
|
|
108
|
+
throw new Error('unknown component unexpectedly succeeded');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let methodRejected = false;
|
|
112
|
+
try {
|
|
113
|
+
await proxiedClient.request('missing.Service', 'Missing', new Uint8Array());
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
methodRejected = true;
|
|
117
|
+
requireError(error, 'unknown nested method', [
|
|
118
|
+
'missing.Service',
|
|
119
|
+
'unimplemented',
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
if (!methodRejected) {
|
|
123
|
+
throw new Error('unknown nested method unexpectedly succeeded');
|
|
124
|
+
}
|
|
125
|
+
if (release) {
|
|
126
|
+
const terminalService = new EchoerClient(proxiedClient);
|
|
127
|
+
let terminalFailed = false;
|
|
128
|
+
try {
|
|
129
|
+
await terminalService.Echo({ body: '__nested_error__' });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
terminalFailed = true;
|
|
133
|
+
requireError(error, 'terminal nested error', ['nested terminal error']);
|
|
134
|
+
}
|
|
135
|
+
if (!terminalFailed) {
|
|
136
|
+
throw new Error('terminal nested error unexpectedly succeeded');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (release) {
|
|
140
|
+
const releaseClient = new Client(buildRpcStreamOpenStream('release', service.RpcStream.bind(service)));
|
|
141
|
+
let releaseFailed = false;
|
|
142
|
+
const releaseService = new EchoerClient(releaseClient);
|
|
143
|
+
try {
|
|
144
|
+
await releaseService.Echo({ body: '__nested_release__' });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
releaseFailed = true;
|
|
148
|
+
requireError(error, 'release during active call', [
|
|
149
|
+
'closed before completion',
|
|
150
|
+
'stream closed',
|
|
151
|
+
'abort',
|
|
152
|
+
'cancel',
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
if (!releaseFailed) {
|
|
156
|
+
throw new Error('release during active call unexpectedly succeeded');
|
|
157
|
+
}
|
|
158
|
+
const releaseStatus = await service.Echo({
|
|
159
|
+
body: '__nested_release_status__',
|
|
160
|
+
});
|
|
161
|
+
if (releaseStatus.body !== 'released') {
|
|
162
|
+
throw new Error(`release completion returned ${releaseStatus.body}`);
|
|
163
|
+
}
|
|
164
|
+
let releasedRejected = false;
|
|
165
|
+
try {
|
|
166
|
+
await releaseService.Echo({});
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
releasedRejected = true;
|
|
170
|
+
requireError(error, 'released component', ['unknown component: release']);
|
|
171
|
+
}
|
|
172
|
+
if (!releasedRejected) {
|
|
173
|
+
throw new Error('released component unexpectedly remained available');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const cancelled = new AbortController();
|
|
177
|
+
const cancelledStream = proxiedClient.bidirectionalStreamingRequest('echo.Echoer', 'EchoBidiStream', (async function* () {
|
|
178
|
+
yield new Uint8Array();
|
|
179
|
+
await new Promise(() => undefined);
|
|
180
|
+
})(), cancelled.signal);
|
|
181
|
+
const cancelledIterator = cancelledStream[Symbol.asyncIterator]();
|
|
182
|
+
const firstNestedResponse = await cancelledIterator.next();
|
|
183
|
+
if (firstNestedResponse.done) {
|
|
184
|
+
throw new Error('nested cancellation call ended before its first response');
|
|
185
|
+
}
|
|
186
|
+
cancelled.abort();
|
|
187
|
+
let cancelRejected = false;
|
|
188
|
+
try {
|
|
189
|
+
while (!(await cancelledIterator.next()).done) {
|
|
190
|
+
// Drain until the abort reaches the nested call.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
cancelRejected = true;
|
|
195
|
+
requireError(error, 'nested cancellation', [ERR_RPC_ABORT]);
|
|
196
|
+
}
|
|
197
|
+
if (!cancelRejected) {
|
|
198
|
+
throw new Error('nested cancellation unexpectedly completed normally');
|
|
199
|
+
}
|
|
92
200
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { pipe } from 'it-pipe';
|
|
2
|
+
import { pushable } from 'it-pushable';
|
|
3
|
+
import { combineUint8ArrayListTransform } from '../../srpc/array-list.js';
|
|
4
|
+
import { parseLengthPrefixTransform, prependLengthPrefixTransform, } from '../../srpc/packet.js';
|
|
5
|
+
import { closeIterator, sourceIterator, TerminationGate, } from '../../srpc/termination.js';
|
|
6
|
+
// tcpSocketToPacketStream wraps a Node.js TCP socket into a PacketStream.
|
|
7
|
+
export function tcpSocketToPacketStream(socket) {
|
|
8
|
+
const sourceTermination = new TerminationGate();
|
|
9
|
+
const sinkTermination = new TerminationGate();
|
|
10
|
+
const bytes = pushable({ objectMode: true });
|
|
11
|
+
let bytesEnded = false;
|
|
12
|
+
const endBytes = (error) => {
|
|
13
|
+
if (bytesEnded)
|
|
14
|
+
return;
|
|
15
|
+
bytesEnded = true;
|
|
16
|
+
bytes.end(error);
|
|
17
|
+
};
|
|
18
|
+
const terminate = (error) => {
|
|
19
|
+
const first = sourceTermination.terminate(error);
|
|
20
|
+
sinkTermination.terminate(error);
|
|
21
|
+
endBytes(error);
|
|
22
|
+
return first;
|
|
23
|
+
};
|
|
24
|
+
socket.on('data', (data) => {
|
|
25
|
+
if (!bytesEnded)
|
|
26
|
+
bytes.push(new Uint8Array(data));
|
|
27
|
+
});
|
|
28
|
+
socket.on('end', () => endBytes());
|
|
29
|
+
socket.on('error', (error) => terminate(error));
|
|
30
|
+
socket.on('close', () => {
|
|
31
|
+
endBytes();
|
|
32
|
+
sinkTermination.terminate();
|
|
33
|
+
});
|
|
34
|
+
const close = async () => {
|
|
35
|
+
if (terminate())
|
|
36
|
+
socket.destroy();
|
|
37
|
+
};
|
|
38
|
+
const abort = (error) => {
|
|
39
|
+
if (terminate(error))
|
|
40
|
+
socket.destroy(error);
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
close,
|
|
44
|
+
abort,
|
|
45
|
+
source: (async function* () {
|
|
46
|
+
const packets = pipe(bytes, parseLengthPrefixTransform(), combineUint8ArrayListTransform())[Symbol.asyncIterator]();
|
|
47
|
+
try {
|
|
48
|
+
while (true) {
|
|
49
|
+
const next = await sourceTermination.next(packets);
|
|
50
|
+
if ('terminated' in next) {
|
|
51
|
+
if (next.error)
|
|
52
|
+
throw next.error;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if ('error' in next)
|
|
56
|
+
throw next.error;
|
|
57
|
+
if (next.result.done)
|
|
58
|
+
return;
|
|
59
|
+
yield next.result.value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
closeIterator(packets);
|
|
64
|
+
}
|
|
65
|
+
})(),
|
|
66
|
+
sink: async (source) => {
|
|
67
|
+
const iterator = sourceIterator(pipe(source, prependLengthPrefixTransform()));
|
|
68
|
+
try {
|
|
69
|
+
while (true) {
|
|
70
|
+
const next = await sinkTermination.next(iterator);
|
|
71
|
+
if ('terminated' in next) {
|
|
72
|
+
if (next.error)
|
|
73
|
+
throw next.error;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if ('error' in next)
|
|
77
|
+
throw next.error;
|
|
78
|
+
if (next.result.done) {
|
|
79
|
+
socket.end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const data = next.result.value instanceof Uint8Array
|
|
83
|
+
? next.result.value
|
|
84
|
+
: next.result.value.subarray();
|
|
85
|
+
const written = await sinkTermination.wait(new Promise((resolve, reject) => {
|
|
86
|
+
socket.write(data, (error) => {
|
|
87
|
+
if (error)
|
|
88
|
+
reject(error);
|
|
89
|
+
else
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
}));
|
|
93
|
+
if ('terminated' in written) {
|
|
94
|
+
if (written.error)
|
|
95
|
+
throw written.error;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if ('error' in written)
|
|
99
|
+
throw written.error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
104
|
+
abort(error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
closeIterator(iterator);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { pushable } from 'it-pushable';
|
|
4
|
+
import { tcpSocketToPacketStream } from './tcp-packet-stream.js';
|
|
5
|
+
const sockets = [];
|
|
6
|
+
const servers = [];
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
for (const socket of sockets)
|
|
9
|
+
socket.destroy();
|
|
10
|
+
for (const server of servers)
|
|
11
|
+
server.close();
|
|
12
|
+
sockets.length = 0;
|
|
13
|
+
servers.length = 0;
|
|
14
|
+
});
|
|
15
|
+
describe('TCP PacketStream lifecycle', () => {
|
|
16
|
+
it('settles a blocked source when the peer ends cleanly', async () => {
|
|
17
|
+
const { local, peer } = await connectSockets();
|
|
18
|
+
const stream = tcpSocketToPacketStream(local);
|
|
19
|
+
const pending = stream.source.next();
|
|
20
|
+
peer.end();
|
|
21
|
+
await expect(settleBeforeTimeout(pending)).resolves.toEqual({
|
|
22
|
+
done: true,
|
|
23
|
+
value: undefined,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it('reports a truncated frame when the peer ends', async () => {
|
|
27
|
+
const { local, peer } = await connectSockets();
|
|
28
|
+
const stream = tcpSocketToPacketStream(local);
|
|
29
|
+
const pending = stream.source.next();
|
|
30
|
+
peer.end(Buffer.from([4, 0, 0, 0, 10, 1]));
|
|
31
|
+
await expect(settleBeforeTimeout(pending)).rejects.toThrow('truncated packet frame');
|
|
32
|
+
});
|
|
33
|
+
it('rejects a blocked source with the abort error', async () => {
|
|
34
|
+
const { local } = await connectSockets();
|
|
35
|
+
const stream = tcpSocketToPacketStream(local);
|
|
36
|
+
const pending = stream.source.next();
|
|
37
|
+
const error = new Error('stopped');
|
|
38
|
+
stream.abort(error);
|
|
39
|
+
await expect(settleBeforeTimeout(pending)).rejects.toBe(error);
|
|
40
|
+
});
|
|
41
|
+
it('settles a blocked sink when closed', async () => {
|
|
42
|
+
const { local } = await connectSockets();
|
|
43
|
+
const stream = tcpSocketToPacketStream(local);
|
|
44
|
+
const input = pushable({ objectMode: true });
|
|
45
|
+
const pending = stream.sink(input);
|
|
46
|
+
await stream.close();
|
|
47
|
+
await expect(pending).resolves.toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it('settles an in-flight socket write when closed', async () => {
|
|
50
|
+
const { local } = await connectSockets();
|
|
51
|
+
const stream = tcpSocketToPacketStream(local);
|
|
52
|
+
const input = pushable({ objectMode: true });
|
|
53
|
+
const writeStarted = Promise.withResolvers();
|
|
54
|
+
vi.spyOn(local, 'write').mockImplementation(((_data, _callback) => {
|
|
55
|
+
writeStarted.resolve();
|
|
56
|
+
return true;
|
|
57
|
+
}));
|
|
58
|
+
input.push(new Uint8Array([1]));
|
|
59
|
+
const pending = stream.sink(input);
|
|
60
|
+
await writeStarted.promise;
|
|
61
|
+
await stream.close();
|
|
62
|
+
await expect(settleBeforeTimeout(pending)).resolves.toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
it('rejects a blocked sink with the abort error', async () => {
|
|
65
|
+
const { local } = await connectSockets();
|
|
66
|
+
const stream = tcpSocketToPacketStream(local);
|
|
67
|
+
const input = pushable({ objectMode: true });
|
|
68
|
+
const pending = stream.sink(input);
|
|
69
|
+
const error = new Error('stopped');
|
|
70
|
+
stream.abort(error);
|
|
71
|
+
await expect(pending).rejects.toBe(error);
|
|
72
|
+
});
|
|
73
|
+
it('does not write ready input after close', async () => {
|
|
74
|
+
const { local, peer } = await connectSockets();
|
|
75
|
+
const stream = tcpSocketToPacketStream(local);
|
|
76
|
+
const input = pushable({ objectMode: true });
|
|
77
|
+
const received = [];
|
|
78
|
+
peer.on('data', (data) => {
|
|
79
|
+
if (typeof data !== 'string')
|
|
80
|
+
received.push(data);
|
|
81
|
+
});
|
|
82
|
+
input.push(new Uint8Array([1]));
|
|
83
|
+
const pending = stream.sink(input);
|
|
84
|
+
await stream.close();
|
|
85
|
+
await pending;
|
|
86
|
+
await new Promise((resolve) => peer.once('close', () => resolve()));
|
|
87
|
+
expect(received).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
async function connectSockets() {
|
|
91
|
+
const accepted = Promise.withResolvers();
|
|
92
|
+
const server = net.createServer((socket) => accepted.resolve(socket));
|
|
93
|
+
servers.push(server);
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
server.once('error', reject);
|
|
96
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
97
|
+
});
|
|
98
|
+
const address = server.address();
|
|
99
|
+
const local = net.connect(address.port, address.address);
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
local.once('connect', resolve);
|
|
102
|
+
local.once('error', reject);
|
|
103
|
+
});
|
|
104
|
+
const peer = await accepted.promise;
|
|
105
|
+
sockets.push(local, peer);
|
|
106
|
+
return { local, peer };
|
|
107
|
+
}
|
|
108
|
+
async function settleBeforeTimeout(promise) {
|
|
109
|
+
let timeout;
|
|
110
|
+
try {
|
|
111
|
+
return await Promise.race([
|
|
112
|
+
promise,
|
|
113
|
+
new Promise((_resolve, reject) => {
|
|
114
|
+
timeout = setTimeout(() => reject(new Error('source stayed blocked')), 500);
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -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);
|
|
@@ -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
|
}
|