@replit/river 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/__tests__/bandwidth.bench.js +1 -1
- package/dist/__tests__/e2e.test.js +119 -5
- package/dist/__tests__/fixtures/cleanup.d.ts +12 -0
- package/dist/__tests__/fixtures/cleanup.d.ts.map +1 -0
- package/dist/__tests__/fixtures/cleanup.js +39 -0
- package/dist/__tests__/fixtures/services.d.ts +72 -0
- package/dist/__tests__/fixtures/services.d.ts.map +1 -1
- package/dist/__tests__/fixtures/services.js +55 -0
- package/dist/__tests__/handler.test.js +35 -2
- package/dist/__tests__/invariants.test.d.ts +2 -0
- package/dist/__tests__/invariants.test.d.ts.map +1 -0
- package/dist/__tests__/invariants.test.js +136 -0
- package/dist/__tests__/serialize.test.js +40 -0
- package/dist/__tests__/typescript-stress.test.d.ts +392 -196
- package/dist/__tests__/typescript-stress.test.d.ts.map +1 -1
- package/dist/__tests__/typescript-stress.test.js +13 -3
- package/dist/router/builder.d.ts +49 -11
- package/dist/router/builder.d.ts.map +1 -1
- package/dist/router/builder.js +8 -2
- package/dist/router/client.d.ts +18 -2
- package/dist/router/client.d.ts.map +1 -1
- package/dist/router/client.js +52 -11
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/server.d.ts +15 -1
- package/dist/router/server.d.ts.map +1 -1
- package/dist/router/server.js +106 -44
- package/dist/transport/events.d.ts +19 -0
- package/dist/transport/events.d.ts.map +1 -0
- package/dist/transport/events.js +26 -0
- package/dist/transport/impls/stdio/stdio.d.ts +0 -4
- package/dist/transport/impls/stdio/stdio.d.ts.map +1 -1
- package/dist/transport/impls/stdio/stdio.js +0 -5
- package/dist/transport/impls/stdio/stdio.test.js +5 -0
- package/dist/transport/impls/ws/client.d.ts.map +1 -1
- package/dist/transport/impls/ws/client.js +2 -2
- package/dist/transport/impls/ws/connection.d.ts.map +1 -1
- package/dist/transport/impls/ws/connection.js +2 -1
- package/dist/transport/impls/ws/server.d.ts +0 -2
- package/dist/transport/impls/ws/server.d.ts.map +1 -1
- package/dist/transport/impls/ws/server.js +4 -9
- package/dist/transport/impls/ws/ws.test.js +30 -10
- package/dist/transport/index.d.ts +3 -3
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/index.js +3 -3
- package/dist/transport/message.d.ts +10 -0
- package/dist/transport/message.d.ts.map +1 -1
- package/dist/transport/message.js +15 -0
- package/dist/transport/transport.d.ts +48 -19
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/transport/transport.js +60 -27
- package/dist/util/testHelpers.d.ts +58 -7
- package/dist/util/testHelpers.d.ts.map +1 -1
- package/dist/util/testHelpers.js +133 -3
- package/package.json +12 -13
- /package/dist/__tests__/{largePayload.json → fixtures/largePayload.json} +0 -0
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ It's like tRPC but...
|
|
|
8
8
|
- with Result types and error handling
|
|
9
9
|
- over WebSockets
|
|
10
10
|
|
|
11
|
+
To use River, you must be on least Typescript 5 with `"moduleResolution": "bundler"`.
|
|
12
|
+
|
|
11
13
|
## Developing
|
|
12
14
|
|
|
13
15
|
[](https://replit.com/new/github/replit/river)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { assert, bench, describe } from 'vitest';
|
|
3
3
|
import { createWebSocketServer, createWsTransports, onServerReady, } from '../util/testHelpers';
|
|
4
|
-
import largePayload from './largePayload.json';
|
|
4
|
+
import largePayload from './fixtures/largePayload.json';
|
|
5
5
|
import { TestServiceConstructor } from './fixtures/services';
|
|
6
6
|
import { createServer } from '../router/server';
|
|
7
7
|
import { createClient } from '../router/client';
|
|
@@ -3,20 +3,19 @@ import { createLocalWebSocketClient, createWebSocketServer, createWsTransports,
|
|
|
3
3
|
import { createServer } from '../router/server';
|
|
4
4
|
import { createClient } from '../router/client';
|
|
5
5
|
import http from 'http';
|
|
6
|
-
import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
6
|
+
import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, UploadableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
7
7
|
import { UNCAUGHT_ERROR } from '../router/result';
|
|
8
8
|
import { codecs } from '../codec/codec.test';
|
|
9
9
|
import { WebSocketClientTransport } from '../transport/impls/ws/client';
|
|
10
10
|
import { WebSocketServerTransport } from '../transport/impls/ws/server';
|
|
11
|
+
import { testFinishesCleanly } from './fixtures/cleanup';
|
|
11
12
|
describe.each(codecs)('client <-> server integration test ($name codec)', async ({ codec }) => {
|
|
12
13
|
const httpServer = http.createServer();
|
|
13
14
|
const port = await onServerReady(httpServer);
|
|
14
15
|
const webSocketServer = await createWebSocketServer(httpServer);
|
|
15
16
|
const getTransports = () => createWsTransports(port, webSocketServer, codec);
|
|
16
17
|
afterAll(() => {
|
|
17
|
-
webSocketServer.
|
|
18
|
-
socket.close();
|
|
19
|
-
});
|
|
18
|
+
webSocketServer.close();
|
|
20
19
|
httpServer.close();
|
|
21
20
|
});
|
|
22
21
|
test('rpc', async () => {
|
|
@@ -27,6 +26,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
27
26
|
const result = await client.test.add.rpc({ n: 3 });
|
|
28
27
|
assert(result.ok);
|
|
29
28
|
expect(result.payload).toStrictEqual({ result: 3 });
|
|
29
|
+
await testFinishesCleanly({
|
|
30
|
+
clientTransports: [clientTransport],
|
|
31
|
+
serverTransport,
|
|
32
|
+
server,
|
|
33
|
+
});
|
|
30
34
|
});
|
|
31
35
|
test('fallible rpc', async () => {
|
|
32
36
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -45,6 +49,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
45
49
|
test: 'abc',
|
|
46
50
|
},
|
|
47
51
|
});
|
|
52
|
+
await testFinishesCleanly({
|
|
53
|
+
clientTransports: [clientTransport],
|
|
54
|
+
serverTransport,
|
|
55
|
+
server,
|
|
56
|
+
});
|
|
48
57
|
});
|
|
49
58
|
test('rpc with binary (uint8array)', async () => {
|
|
50
59
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -55,6 +64,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
55
64
|
assert(result.ok);
|
|
56
65
|
assert(result.payload.contents instanceof Uint8Array);
|
|
57
66
|
expect(new TextDecoder().decode(result.payload.contents)).toStrictEqual('contents for file test.py');
|
|
67
|
+
await testFinishesCleanly({
|
|
68
|
+
clientTransports: [clientTransport],
|
|
69
|
+
serverTransport,
|
|
70
|
+
server,
|
|
71
|
+
});
|
|
58
72
|
});
|
|
59
73
|
test('stream', async () => {
|
|
60
74
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -65,6 +79,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
65
79
|
input.push({ msg: 'abc', ignore: false });
|
|
66
80
|
input.push({ msg: 'def', ignore: true });
|
|
67
81
|
input.push({ msg: 'ghi', ignore: false });
|
|
82
|
+
input.push({ msg: 'end', ignore: false, end: true });
|
|
68
83
|
input.end();
|
|
69
84
|
const result1 = await iterNext(output);
|
|
70
85
|
assert(result1.ok);
|
|
@@ -72,7 +87,43 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
72
87
|
const result2 = await iterNext(output);
|
|
73
88
|
assert(result2.ok);
|
|
74
89
|
expect(result2.payload).toStrictEqual({ response: 'ghi' });
|
|
90
|
+
const result3 = await iterNext(output);
|
|
91
|
+
assert(result3.ok);
|
|
92
|
+
expect(result3.payload).toStrictEqual({ response: 'end' });
|
|
93
|
+
// after the server stream is ended, the client stream should be ended too
|
|
94
|
+
const result4 = await output.next();
|
|
95
|
+
assert(result4.done);
|
|
75
96
|
close();
|
|
97
|
+
await testFinishesCleanly({
|
|
98
|
+
clientTransports: [clientTransport],
|
|
99
|
+
serverTransport,
|
|
100
|
+
server,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
test('stream with init message', async () => {
|
|
104
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
105
|
+
const serviceDefs = { test: TestServiceConstructor() };
|
|
106
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
107
|
+
const client = createClient(clientTransport);
|
|
108
|
+
const [input, output, close] = await client.test.echoWithPrefix.stream({
|
|
109
|
+
prefix: 'test',
|
|
110
|
+
});
|
|
111
|
+
input.push({ msg: 'abc', ignore: false });
|
|
112
|
+
input.push({ msg: 'def', ignore: true });
|
|
113
|
+
input.push({ msg: 'ghi', ignore: false });
|
|
114
|
+
input.end();
|
|
115
|
+
const result1 = await iterNext(output);
|
|
116
|
+
assert(result1.ok);
|
|
117
|
+
expect(result1.payload).toStrictEqual({ response: 'test abc' });
|
|
118
|
+
const result2 = await iterNext(output);
|
|
119
|
+
assert(result2.ok);
|
|
120
|
+
expect(result2.payload).toStrictEqual({ response: 'test ghi' });
|
|
121
|
+
close();
|
|
122
|
+
await testFinishesCleanly({
|
|
123
|
+
clientTransports: [clientTransport],
|
|
124
|
+
serverTransport,
|
|
125
|
+
server,
|
|
126
|
+
});
|
|
76
127
|
});
|
|
77
128
|
test('fallible stream', async () => {
|
|
78
129
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -96,6 +147,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
96
147
|
message: 'some message',
|
|
97
148
|
});
|
|
98
149
|
close();
|
|
150
|
+
await testFinishesCleanly({
|
|
151
|
+
clientTransports: [clientTransport],
|
|
152
|
+
serverTransport,
|
|
153
|
+
server,
|
|
154
|
+
});
|
|
99
155
|
});
|
|
100
156
|
test('subscription', async () => {
|
|
101
157
|
const options = { codec };
|
|
@@ -132,6 +188,49 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
132
188
|
expect(result.payload).toStrictEqual({ result: 4 });
|
|
133
189
|
close1();
|
|
134
190
|
close2();
|
|
191
|
+
await testFinishesCleanly({
|
|
192
|
+
clientTransports: [client1Transport, client2Transport],
|
|
193
|
+
serverTransport,
|
|
194
|
+
server,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
test('upload', async () => {
|
|
198
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
199
|
+
const serviceDefs = { uploadable: UploadableServiceConstructor() };
|
|
200
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
201
|
+
const client = createClient(clientTransport);
|
|
202
|
+
const [addStream, addResult] = await client.uploadable.addMultiple.upload();
|
|
203
|
+
addStream.push({ n: 1 });
|
|
204
|
+
addStream.push({ n: 2 });
|
|
205
|
+
addStream.end();
|
|
206
|
+
const result = await addResult;
|
|
207
|
+
assert(result.ok);
|
|
208
|
+
expect(result.payload).toStrictEqual({ result: 3 });
|
|
209
|
+
await testFinishesCleanly({
|
|
210
|
+
clientTransports: [clientTransport],
|
|
211
|
+
serverTransport,
|
|
212
|
+
server,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
test('upload with init message', async () => {
|
|
216
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
217
|
+
const serviceDefs = { uploadable: UploadableServiceConstructor() };
|
|
218
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
219
|
+
const client = createClient(clientTransport);
|
|
220
|
+
const [addStream, addResult] = await client.uploadable.addMultipleWithPrefix.upload({
|
|
221
|
+
prefix: 'test',
|
|
222
|
+
});
|
|
223
|
+
addStream.push({ n: 1 });
|
|
224
|
+
addStream.push({ n: 2 });
|
|
225
|
+
addStream.end();
|
|
226
|
+
const result = await addResult;
|
|
227
|
+
assert(result.ok);
|
|
228
|
+
expect(result.payload).toStrictEqual({ result: 'test 3' });
|
|
229
|
+
await testFinishesCleanly({
|
|
230
|
+
clientTransports: [clientTransport],
|
|
231
|
+
serverTransport,
|
|
232
|
+
server,
|
|
233
|
+
});
|
|
135
234
|
});
|
|
136
235
|
test('message order is preserved in the face of disconnects', async () => {
|
|
137
236
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -153,7 +252,12 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
153
252
|
}
|
|
154
253
|
const res = await client.test.getAll.rpc({});
|
|
155
254
|
assert(res.ok);
|
|
156
|
-
|
|
255
|
+
expect(res.payload.msgs).toStrictEqual(expected);
|
|
256
|
+
await testFinishesCleanly({
|
|
257
|
+
clientTransports: [clientTransport],
|
|
258
|
+
serverTransport,
|
|
259
|
+
server,
|
|
260
|
+
});
|
|
157
261
|
});
|
|
158
262
|
const CONCURRENCY = 10;
|
|
159
263
|
test('concurrent rpcs', async () => {
|
|
@@ -170,6 +274,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
170
274
|
assert(result.ok);
|
|
171
275
|
expect(result.payload).toStrictEqual({ n: i });
|
|
172
276
|
}
|
|
277
|
+
await testFinishesCleanly({
|
|
278
|
+
clientTransports: [clientTransport],
|
|
279
|
+
serverTransport,
|
|
280
|
+
server,
|
|
281
|
+
});
|
|
173
282
|
});
|
|
174
283
|
test('concurrent streams', async () => {
|
|
175
284
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -198,5 +307,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
198
307
|
const [_input, _output, close] = openStreams[i];
|
|
199
308
|
close();
|
|
200
309
|
}
|
|
310
|
+
await testFinishesCleanly({
|
|
311
|
+
clientTransports: [clientTransport],
|
|
312
|
+
serverTransport,
|
|
313
|
+
server,
|
|
314
|
+
});
|
|
201
315
|
});
|
|
202
316
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Connection, Transport } from '../../transport';
|
|
2
|
+
import { Server } from '../../router';
|
|
3
|
+
export declare function ensureTransportIsClean(t: Transport<Connection>): Promise<void>;
|
|
4
|
+
export declare function waitUntil<T>(valueGetter: () => T, expected: T, message?: string): Promise<boolean>;
|
|
5
|
+
export declare function ensureTransportQueuesAreEventuallyEmpty(t: Transport<Connection>): Promise<void>;
|
|
6
|
+
export declare function ensureServerIsClean(s: Server<unknown>): Promise<boolean>;
|
|
7
|
+
export declare function testFinishesCleanly({ clientTransports, serverTransport, server, }: Partial<{
|
|
8
|
+
clientTransports: Array<Transport<Connection>>;
|
|
9
|
+
serverTransport: Transport<Connection>;
|
|
10
|
+
server: Server<unknown>;
|
|
11
|
+
}>): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAOtC,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,iBAapE;AAED,wBAAsB,SAAS,CAAC,CAAC,EAC/B,WAAW,EAAE,MAAM,CAAC,EACpB,QAAQ,EAAE,CAAC,EACX,OAAO,CAAC,EAAE,MAAM,oBAOjB;AAED,wBAAsB,uCAAuC,CAC3D,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,iBAazB;AAED,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,oBAM3D;AAED,wBAAsB,mBAAmB,CAAC,EACxC,gBAAgB,EAChB,eAAe,EACf,MAAM,GACP,EAAE,OAAO,CAAC;IACT,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;CACzB,CAAC,iBAgBD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, vi } from 'vitest';
|
|
2
|
+
const waitUntilOptions = {
|
|
3
|
+
timeout: 250,
|
|
4
|
+
interval: 5, // check every 5ms
|
|
5
|
+
};
|
|
6
|
+
export async function ensureTransportIsClean(t) {
|
|
7
|
+
expect(t.state, `transport ${t.clientId} should be closed after the test`).to.not.equal('open');
|
|
8
|
+
expect(t.connections, `transport ${t.clientId} should not have open connections after the test`).toStrictEqual(new Map());
|
|
9
|
+
expect(t.eventDispatcher.numberOfListeners('message'), `transport ${t.clientId} should not have open message handlers after the test`).equal(0);
|
|
10
|
+
}
|
|
11
|
+
export async function waitUntil(valueGetter, expected, message) {
|
|
12
|
+
return vi
|
|
13
|
+
.waitUntil(() => valueGetter() === expected, waitUntilOptions)
|
|
14
|
+
.finally(() => {
|
|
15
|
+
expect(valueGetter(), message).toEqual(expected);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function ensureTransportQueuesAreEventuallyEmpty(t) {
|
|
19
|
+
await waitUntil(() => t.sendQueue.size, 0, `transport ${t.clientId} should not have any messages waiting to send after the test`);
|
|
20
|
+
await waitUntil(() => t.sendBuffer.size, 0, `transport ${t.clientId} should not have any un-acked messages after the test`);
|
|
21
|
+
}
|
|
22
|
+
export async function ensureServerIsClean(s) {
|
|
23
|
+
return waitUntil(() => s.streams.size, 0, `server should not have any open streams after the test`);
|
|
24
|
+
}
|
|
25
|
+
export async function testFinishesCleanly({ clientTransports, serverTransport, server, }) {
|
|
26
|
+
if (clientTransports) {
|
|
27
|
+
await Promise.all(clientTransports.map((t) => t.close()));
|
|
28
|
+
await Promise.all(clientTransports.map(ensureTransportIsClean));
|
|
29
|
+
}
|
|
30
|
+
// server sits on top of server transport so we clean it up first
|
|
31
|
+
if (server) {
|
|
32
|
+
await ensureServerIsClean(server);
|
|
33
|
+
await server.close();
|
|
34
|
+
}
|
|
35
|
+
if (serverTransport) {
|
|
36
|
+
await serverTransport.close();
|
|
37
|
+
await ensureTransportIsClean(serverTransport);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Observable } from './observable';
|
|
|
2
2
|
export declare const EchoRequest: import("@sinclair/typebox").TObject<{
|
|
3
3
|
msg: import("@sinclair/typebox").TString;
|
|
4
4
|
ignore: import("@sinclair/typebox").TBoolean;
|
|
5
|
+
end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
5
6
|
}>;
|
|
6
7
|
export declare const EchoResponse: import("@sinclair/typebox").TObject<{
|
|
7
8
|
response: import("@sinclair/typebox").TString;
|
|
@@ -34,6 +35,7 @@ export declare const TestServiceConstructor: () => {
|
|
|
34
35
|
input: import("@sinclair/typebox").TObject<{
|
|
35
36
|
msg: import("@sinclair/typebox").TString;
|
|
36
37
|
ignore: import("@sinclair/typebox").TBoolean;
|
|
38
|
+
end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
37
39
|
}>;
|
|
38
40
|
output: import("@sinclair/typebox").TObject<{
|
|
39
41
|
response: import("@sinclair/typebox").TString;
|
|
@@ -42,6 +44,34 @@ export declare const TestServiceConstructor: () => {
|
|
|
42
44
|
handler: (context: import("../../router").ServiceContextWithState<{
|
|
43
45
|
count: number;
|
|
44
46
|
}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
47
|
+
end?: boolean | undefined;
|
|
48
|
+
msg: string;
|
|
49
|
+
ignore: boolean;
|
|
50
|
+
}>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
51
|
+
response: string;
|
|
52
|
+
}, never>>, void, unknown>) => Promise<void>;
|
|
53
|
+
type: "stream";
|
|
54
|
+
};
|
|
55
|
+
} & {
|
|
56
|
+
echoWithPrefix: {
|
|
57
|
+
init: import("@sinclair/typebox").TObject<{
|
|
58
|
+
prefix: import("@sinclair/typebox").TString;
|
|
59
|
+
}>;
|
|
60
|
+
input: import("@sinclair/typebox").TObject<{
|
|
61
|
+
msg: import("@sinclair/typebox").TString;
|
|
62
|
+
ignore: import("@sinclair/typebox").TBoolean;
|
|
63
|
+
end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
64
|
+
}>;
|
|
65
|
+
output: import("@sinclair/typebox").TObject<{
|
|
66
|
+
response: import("@sinclair/typebox").TString;
|
|
67
|
+
}>;
|
|
68
|
+
errors: import("@sinclair/typebox").TNever;
|
|
69
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
70
|
+
count: number;
|
|
71
|
+
}>, init: import("../../transport/message").TransportMessage<{
|
|
72
|
+
prefix: string;
|
|
73
|
+
}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
74
|
+
end?: boolean | undefined;
|
|
45
75
|
msg: string;
|
|
46
76
|
ignore: boolean;
|
|
47
77
|
}>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
@@ -213,4 +243,46 @@ export declare const SubscribableServiceConstructor: () => {
|
|
|
213
243
|
};
|
|
214
244
|
};
|
|
215
245
|
};
|
|
246
|
+
export declare const UploadableServiceConstructor: () => {
|
|
247
|
+
name: "uploadable";
|
|
248
|
+
state: {};
|
|
249
|
+
procedures: {
|
|
250
|
+
addMultiple: {
|
|
251
|
+
input: import("@sinclair/typebox").TObject<{
|
|
252
|
+
n: import("@sinclair/typebox").TNumber;
|
|
253
|
+
}>;
|
|
254
|
+
output: import("@sinclair/typebox").TObject<{
|
|
255
|
+
result: import("@sinclair/typebox").TNumber;
|
|
256
|
+
}>;
|
|
257
|
+
errors: import("@sinclair/typebox").TNever;
|
|
258
|
+
handler: (context: import("../../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
259
|
+
n: number;
|
|
260
|
+
}>>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
261
|
+
result: number;
|
|
262
|
+
}, never>>>;
|
|
263
|
+
type: "upload";
|
|
264
|
+
};
|
|
265
|
+
} & {
|
|
266
|
+
addMultipleWithPrefix: {
|
|
267
|
+
init: import("@sinclair/typebox").TObject<{
|
|
268
|
+
prefix: import("@sinclair/typebox").TString;
|
|
269
|
+
}>;
|
|
270
|
+
input: import("@sinclair/typebox").TObject<{
|
|
271
|
+
n: import("@sinclair/typebox").TNumber;
|
|
272
|
+
}>;
|
|
273
|
+
output: import("@sinclair/typebox").TObject<{
|
|
274
|
+
result: import("@sinclair/typebox").TString;
|
|
275
|
+
}>;
|
|
276
|
+
errors: import("@sinclair/typebox").TNever;
|
|
277
|
+
handler: (context: import("../../router").ServiceContextWithState<{}>, init: import("../../transport/message").TransportMessage<{
|
|
278
|
+
prefix: string;
|
|
279
|
+
}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
280
|
+
n: number;
|
|
281
|
+
}>>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
282
|
+
result: string;
|
|
283
|
+
}, never>>>;
|
|
284
|
+
type: "upload";
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
};
|
|
216
288
|
//# sourceMappingURL=services.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/services.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/services.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,WAAW;;;;EAItB,CAAC;AACH,eAAO,MAAM,YAAY;;EAA2C,CAAC;AAErE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDpB,CAAC;AAEhB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyBxB,CAAC;AAEhB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;CAc1B,CAAC;AAEhB,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiExB,CAAC;AAEhB,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B5B,CAAC;AAEhB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuC1B,CAAC"}
|
|
@@ -6,6 +6,7 @@ import { Observable } from './observable';
|
|
|
6
6
|
export const EchoRequest = Type.Object({
|
|
7
7
|
msg: Type.String(),
|
|
8
8
|
ignore: Type.Boolean(),
|
|
9
|
+
end: Type.Optional(Type.Boolean()),
|
|
9
10
|
});
|
|
10
11
|
export const EchoResponse = Type.Object({ response: Type.String() });
|
|
11
12
|
export const TestServiceConstructor = () => ServiceBuilder.create('test')
|
|
@@ -34,6 +35,24 @@ export const TestServiceConstructor = () => ServiceBuilder.create('test')
|
|
|
34
35
|
if (!req.ignore) {
|
|
35
36
|
returnStream.push(reply(msg, Ok({ response: req.msg })));
|
|
36
37
|
}
|
|
38
|
+
if (req.end) {
|
|
39
|
+
returnStream.end();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
.defineProcedure('echoWithPrefix', {
|
|
45
|
+
type: 'stream',
|
|
46
|
+
init: Type.Object({ prefix: Type.String() }),
|
|
47
|
+
input: EchoRequest,
|
|
48
|
+
output: EchoResponse,
|
|
49
|
+
errors: Type.Never(),
|
|
50
|
+
async handler(_ctx, init, msgStream, returnStream) {
|
|
51
|
+
for await (const msg of msgStream) {
|
|
52
|
+
const req = msg.payload;
|
|
53
|
+
if (!req.ignore) {
|
|
54
|
+
returnStream.push(reply(msg, Ok({ response: `${init.payload.prefix} ${req.msg}` })));
|
|
55
|
+
}
|
|
37
56
|
}
|
|
38
57
|
},
|
|
39
58
|
})
|
|
@@ -167,3 +186,39 @@ export const SubscribableServiceConstructor = () => ServiceBuilder.create('subsc
|
|
|
167
186
|
},
|
|
168
187
|
})
|
|
169
188
|
.finalize();
|
|
189
|
+
export const UploadableServiceConstructor = () => ServiceBuilder.create('uploadable')
|
|
190
|
+
.initialState({})
|
|
191
|
+
.defineProcedure('addMultiple', {
|
|
192
|
+
type: 'upload',
|
|
193
|
+
input: Type.Object({ n: Type.Number() }),
|
|
194
|
+
output: Type.Object({ result: Type.Number() }),
|
|
195
|
+
errors: Type.Never(),
|
|
196
|
+
async handler(_ctx, msgStream) {
|
|
197
|
+
let result = 0;
|
|
198
|
+
let lastMsg;
|
|
199
|
+
for await (const msg of msgStream) {
|
|
200
|
+
const { n } = msg.payload;
|
|
201
|
+
result += n;
|
|
202
|
+
lastMsg = msg;
|
|
203
|
+
}
|
|
204
|
+
return reply(lastMsg, Ok({ result: result }));
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
.defineProcedure('addMultipleWithPrefix', {
|
|
208
|
+
type: 'upload',
|
|
209
|
+
init: Type.Object({ prefix: Type.String() }),
|
|
210
|
+
input: Type.Object({ n: Type.Number() }),
|
|
211
|
+
output: Type.Object({ result: Type.String() }),
|
|
212
|
+
errors: Type.Never(),
|
|
213
|
+
async handler(_ctx, init, msgStream) {
|
|
214
|
+
let result = 0;
|
|
215
|
+
let lastMsg;
|
|
216
|
+
for await (const msg of msgStream) {
|
|
217
|
+
const { n } = msg.payload;
|
|
218
|
+
result += n;
|
|
219
|
+
lastMsg = msg;
|
|
220
|
+
}
|
|
221
|
+
return reply(lastMsg, Ok({ result: init.payload.prefix + ' ' + result }));
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
.finalize();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { asClientRpc, asClientStream, asClientSubscription, iterNext, } from '../util/testHelpers';
|
|
1
|
+
import { asClientRpc, asClientStream, asClientStreamWithInitialization, asClientSubscription, asClientUpload, asClientUploadWithInitialization, iterNext, } from '../util/testHelpers';
|
|
2
2
|
import { assert, describe, expect, test } from 'vitest';
|
|
3
|
-
import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
3
|
+
import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, UploadableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
4
4
|
import { UNCAUGHT_ERROR } from '../router/result';
|
|
5
5
|
import { Observable } from './fixtures/observable';
|
|
6
6
|
describe('server-side test', () => {
|
|
@@ -48,6 +48,20 @@ describe('server-side test', () => {
|
|
|
48
48
|
expect(result2.payload).toStrictEqual({ response: 'ghi' });
|
|
49
49
|
expect(output.readableLength).toBe(0);
|
|
50
50
|
});
|
|
51
|
+
test('stream with initialization', async () => {
|
|
52
|
+
const [input, output] = asClientStreamWithInitialization(initialState, service.procedures.echoWithPrefix, { prefix: 'test' });
|
|
53
|
+
input.push({ msg: 'abc', ignore: false });
|
|
54
|
+
input.push({ msg: 'def', ignore: true });
|
|
55
|
+
input.push({ msg: 'ghi', ignore: false });
|
|
56
|
+
input.end();
|
|
57
|
+
const result1 = await iterNext(output);
|
|
58
|
+
assert(result1 && result1.ok);
|
|
59
|
+
expect(result1.payload).toStrictEqual({ response: 'test abc' });
|
|
60
|
+
const result2 = await iterNext(output);
|
|
61
|
+
assert(result2 && result2.ok);
|
|
62
|
+
expect(result2.payload).toStrictEqual({ response: 'test ghi' });
|
|
63
|
+
expect(output.readableLength).toBe(0);
|
|
64
|
+
});
|
|
51
65
|
test('fallible stream', async () => {
|
|
52
66
|
const service = FallibleServiceConstructor();
|
|
53
67
|
const [input, output] = asClientStream({}, service.procedures.echo);
|
|
@@ -85,4 +99,23 @@ describe('server-side test', () => {
|
|
|
85
99
|
assert(streamResult2 && streamResult1.ok);
|
|
86
100
|
expect(streamResult2.payload).toStrictEqual({ result: 3 });
|
|
87
101
|
});
|
|
102
|
+
test('uploads', async () => {
|
|
103
|
+
const service = UploadableServiceConstructor();
|
|
104
|
+
const [input, result] = asClientUpload({}, service.procedures.addMultiple);
|
|
105
|
+
input.push({ n: 1 });
|
|
106
|
+
input.push({ n: 2 });
|
|
107
|
+
input.end();
|
|
108
|
+
expect(await result).toStrictEqual({ ok: true, payload: { result: 3 } });
|
|
109
|
+
});
|
|
110
|
+
test('uploads with initialization', async () => {
|
|
111
|
+
const service = UploadableServiceConstructor();
|
|
112
|
+
const [input, result] = asClientUploadWithInitialization({}, service.procedures.addMultipleWithPrefix, { prefix: 'test' });
|
|
113
|
+
input.push({ n: 1 });
|
|
114
|
+
input.push({ n: 2 });
|
|
115
|
+
input.end();
|
|
116
|
+
expect(await result).toStrictEqual({
|
|
117
|
+
ok: true,
|
|
118
|
+
payload: { result: 'test 3' },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
88
121
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariants.test.d.ts","sourceRoot":"","sources":["../../__tests__/invariants.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterAll, assert, describe, expect, test } from 'vitest';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { createWebSocketServer, createWsTransports, iterNext, onServerReady, } from '../util/testHelpers';
|
|
4
|
+
import { SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
5
|
+
import { createClient, createServer } from '../router';
|
|
6
|
+
import { ensureServerIsClean, ensureTransportQueuesAreEventuallyEmpty, waitUntil, } from './fixtures/cleanup';
|
|
7
|
+
describe('procedures should leave no trace after finishing', async () => {
|
|
8
|
+
const httpServer = http.createServer();
|
|
9
|
+
const port = await onServerReady(httpServer);
|
|
10
|
+
const webSocketServer = await createWebSocketServer(httpServer);
|
|
11
|
+
const getTransports = () => createWsTransports(port, webSocketServer);
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
webSocketServer.close();
|
|
14
|
+
httpServer.close();
|
|
15
|
+
});
|
|
16
|
+
test('closing a transport from the client cleans up connection on the server', async () => {
|
|
17
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
18
|
+
const serviceDefs = { test: TestServiceConstructor() };
|
|
19
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
20
|
+
const client = createClient(clientTransport);
|
|
21
|
+
expect(clientTransport.connections.size).toEqual(0);
|
|
22
|
+
expect(serverTransport.connections.size).toEqual(0);
|
|
23
|
+
// start procedure
|
|
24
|
+
await client.test.add.rpc({ n: 3 });
|
|
25
|
+
// end procedure
|
|
26
|
+
expect(clientTransport.connections.size).toEqual(1);
|
|
27
|
+
expect(serverTransport.connections.size).toEqual(1);
|
|
28
|
+
// should be back to 0 connections after client closes
|
|
29
|
+
clientTransport.close();
|
|
30
|
+
expect(clientTransport.connections.size).toEqual(0);
|
|
31
|
+
await waitUntil(() => serverTransport.connections.size, 0, 'server should cleanup connection after client closes');
|
|
32
|
+
});
|
|
33
|
+
test('closing a transport from the server cleans up connection on the client', async () => {
|
|
34
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
35
|
+
const serviceDefs = { test: TestServiceConstructor() };
|
|
36
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
37
|
+
const client = createClient(clientTransport);
|
|
38
|
+
expect(clientTransport.connections.size).toEqual(0);
|
|
39
|
+
expect(serverTransport.connections.size).toEqual(0);
|
|
40
|
+
// start procedure
|
|
41
|
+
await client.test.add.rpc({ n: 3 });
|
|
42
|
+
// end procedure
|
|
43
|
+
expect(clientTransport.connections.size).toEqual(1);
|
|
44
|
+
expect(serverTransport.connections.size).toEqual(1);
|
|
45
|
+
// should be back to 0 connections after client closes
|
|
46
|
+
serverTransport.close();
|
|
47
|
+
expect(serverTransport.connections.size).toEqual(0);
|
|
48
|
+
await waitUntil(() => clientTransport.connections.size, 0, 'client should cleanup connection after server closes');
|
|
49
|
+
});
|
|
50
|
+
test('rpc', async () => {
|
|
51
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
52
|
+
const serviceDefs = { test: TestServiceConstructor() };
|
|
53
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
54
|
+
const client = createClient(clientTransport);
|
|
55
|
+
let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
|
|
56
|
+
let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
|
|
57
|
+
// start procedure
|
|
58
|
+
await client.test.add.rpc({ n: 3 });
|
|
59
|
+
// end procedure
|
|
60
|
+
// number of message handlers shouldn't increase after rpc
|
|
61
|
+
expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
|
|
62
|
+
expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
|
|
63
|
+
// check number of connections
|
|
64
|
+
expect(serverTransport.connections.size).toEqual(1);
|
|
65
|
+
expect(clientTransport.connections.size).toEqual(1);
|
|
66
|
+
await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
|
|
67
|
+
await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
|
|
68
|
+
// ensure we have no streams left on the server
|
|
69
|
+
await ensureServerIsClean(server);
|
|
70
|
+
});
|
|
71
|
+
test('stream', async () => {
|
|
72
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
73
|
+
const serviceDefs = { test: TestServiceConstructor() };
|
|
74
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
75
|
+
const client = createClient(clientTransport);
|
|
76
|
+
let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
|
|
77
|
+
let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
|
|
78
|
+
// start procedure
|
|
79
|
+
const [input, output, close] = await client.test.echo.stream();
|
|
80
|
+
input.push({ msg: '1', ignore: false });
|
|
81
|
+
input.push({ msg: '2', ignore: false, end: true });
|
|
82
|
+
input.end();
|
|
83
|
+
const result1 = await iterNext(output);
|
|
84
|
+
assert(result1.ok);
|
|
85
|
+
expect(result1.payload).toStrictEqual({ response: '1' });
|
|
86
|
+
const result2 = await iterNext(output);
|
|
87
|
+
assert(result2.ok);
|
|
88
|
+
expect(result2.payload).toStrictEqual({ response: '2' });
|
|
89
|
+
// ensure we exactly have one stream even after we send multiple messages
|
|
90
|
+
expect(server.streams.size).toEqual(1);
|
|
91
|
+
const result3 = await output.next();
|
|
92
|
+
assert(result3.done);
|
|
93
|
+
close();
|
|
94
|
+
// end procedure
|
|
95
|
+
// number of message handlers shouldn't increase after stream ends
|
|
96
|
+
expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
|
|
97
|
+
expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
|
|
98
|
+
// check number of connections
|
|
99
|
+
expect(serverTransport.connections.size).toEqual(1);
|
|
100
|
+
expect(clientTransport.connections.size).toEqual(1);
|
|
101
|
+
await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
|
|
102
|
+
await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
|
|
103
|
+
// ensure we have no streams left on the server
|
|
104
|
+
await ensureServerIsClean(server);
|
|
105
|
+
});
|
|
106
|
+
test('subscription', async () => {
|
|
107
|
+
const [clientTransport, serverTransport] = getTransports();
|
|
108
|
+
const serviceDefs = { test: SubscribableServiceConstructor() };
|
|
109
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
110
|
+
const client = createClient(clientTransport);
|
|
111
|
+
let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
|
|
112
|
+
let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
|
|
113
|
+
// start procedure
|
|
114
|
+
const [subscription, close] = await client.test.value.subscribe({});
|
|
115
|
+
let result = await iterNext(subscription);
|
|
116
|
+
assert(result.ok);
|
|
117
|
+
expect(result.payload).toStrictEqual({ result: 0 });
|
|
118
|
+
const add1 = await client.test.add.rpc({ n: 1 });
|
|
119
|
+
assert(add1.ok);
|
|
120
|
+
result = await iterNext(subscription);
|
|
121
|
+
assert(result.ok);
|
|
122
|
+
expect(result.payload).toStrictEqual({ result: 1 });
|
|
123
|
+
close();
|
|
124
|
+
// end procedure
|
|
125
|
+
// number of message handlers shouldn't increase after stream ends
|
|
126
|
+
expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
|
|
127
|
+
expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
|
|
128
|
+
// check number of connections
|
|
129
|
+
expect(serverTransport.connections.size).toEqual(1);
|
|
130
|
+
expect(clientTransport.connections.size).toEqual(1);
|
|
131
|
+
await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
|
|
132
|
+
await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
|
|
133
|
+
// ensure we have no streams left on the server
|
|
134
|
+
await ensureServerIsClean(server);
|
|
135
|
+
});
|
|
136
|
+
});
|