@replit/river 0.7.2 → 0.8.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/README.md +2 -0
- package/dist/__tests__/bandwidth.bench.js +6 -6
- package/dist/__tests__/e2e.test.js +112 -23
- 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/observable.d.ts +26 -0
- package/dist/__tests__/fixtures/observable.d.ts.map +1 -0
- package/dist/__tests__/fixtures/observable.js +38 -0
- package/dist/__tests__/fixtures/observable.test.d.ts +2 -0
- package/dist/__tests__/fixtures/observable.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/observable.test.js +39 -0
- package/dist/__tests__/{fixtures.d.ts → fixtures/services.d.ts} +61 -18
- package/dist/__tests__/fixtures/services.d.ts.map +1 -0
- package/dist/__tests__/{fixtures.js → fixtures/services.js} +35 -3
- package/dist/__tests__/handler.test.js +24 -7
- 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 +2 -1
- package/dist/router/builder.d.ts +10 -4
- package/dist/router/builder.d.ts.map +1 -1
- package/dist/router/client.d.ts +14 -5
- package/dist/router/client.d.ts.map +1 -1
- package/dist/router/client.js +42 -11
- package/dist/router/server.d.ts +14 -0
- package/dist/router/server.d.ts.map +1 -1
- package/dist/router/server.js +85 -48
- 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 +6 -1
- 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 +31 -11
- package/dist/transport/index.d.ts +3 -3
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/index.js +1 -1
- 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 +37 -11
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/transport/transport.js +37 -13
- package/dist/{testUtils.d.ts → util/testHelpers.d.ts} +27 -8
- package/dist/util/testHelpers.d.ts.map +1 -0
- package/dist/{testUtils.js → util/testHelpers.js} +51 -5
- package/package.json +3 -3
- package/dist/__tests__/fixtures.d.ts.map +0 -1
- package/dist/testUtils.d.ts.map +0 -1
- /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,8 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { assert, bench, describe } from 'vitest';
|
|
3
|
-
import { createWebSocketServer, createWsTransports, onServerReady, } from '../
|
|
4
|
-
import largePayload from './largePayload.json';
|
|
5
|
-
import { TestServiceConstructor } from './fixtures';
|
|
3
|
+
import { createWebSocketServer, createWsTransports, onServerReady, } from '../util/testHelpers';
|
|
4
|
+
import largePayload from './fixtures/largePayload.json';
|
|
5
|
+
import { TestServiceConstructor } from './fixtures/services';
|
|
6
6
|
import { createServer } from '../router/server';
|
|
7
7
|
import { createClient } from '../router/client';
|
|
8
8
|
import { StupidlyLargeService } from './typescript-stress.test';
|
|
@@ -57,10 +57,10 @@ describe('simple router level bandwidth', async () => {
|
|
|
57
57
|
const server = await createServer(serverTransport, serviceDefs);
|
|
58
58
|
const client = createClient(clientTransport);
|
|
59
59
|
bench('rpc (wait for response)', async () => {
|
|
60
|
-
const result = await client.test.add({ n: 1 });
|
|
60
|
+
const result = await client.test.add.rpc({ n: 1 });
|
|
61
61
|
assert(result.ok);
|
|
62
62
|
}, { time: BENCH_DURATION });
|
|
63
|
-
const [input, output] = await client.test.echo();
|
|
63
|
+
const [input, output] = await client.test.echo.stream();
|
|
64
64
|
bench('stream (wait for response)', async () => {
|
|
65
65
|
input.push({ msg: 'abc', ignore: false });
|
|
66
66
|
const result = await output.next();
|
|
@@ -84,7 +84,7 @@ describe('complex (50 procedures) router level bandwidth', async () => {
|
|
|
84
84
|
const server = await createServer(serverTransport, serviceDefs);
|
|
85
85
|
const client = createClient(clientTransport);
|
|
86
86
|
bench('rpc (wait for response)', async () => {
|
|
87
|
-
const result = await client.b.f35({ a: 1 });
|
|
87
|
+
const result = await client.b.f35.rpc({ a: 1 });
|
|
88
88
|
assert(result.ok);
|
|
89
89
|
}, { time: BENCH_DURATION });
|
|
90
90
|
});
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { afterAll, assert, describe, expect, test } from 'vitest';
|
|
2
|
-
import { createWebSocketServer, createWsTransports, onServerReady, } from '../
|
|
2
|
+
import { createLocalWebSocketClient, createWebSocketServer, createWsTransports, iterNext, onServerReady, } from '../util/testHelpers';
|
|
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, TestServiceConstructor, } from './fixtures';
|
|
6
|
+
import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
|
|
7
7
|
import { UNCAUGHT_ERROR } from '../router/result';
|
|
8
8
|
import { codecs } from '../codec/codec.test';
|
|
9
|
+
import { WebSocketClientTransport } from '../transport/impls/ws/client';
|
|
10
|
+
import { WebSocketServerTransport } from '../transport/impls/ws/server';
|
|
11
|
+
import { testFinishesCleanly } from './fixtures/cleanup';
|
|
9
12
|
describe.each(codecs)('client <-> server integration test ($name codec)', async ({ codec }) => {
|
|
10
13
|
const httpServer = http.createServer();
|
|
11
14
|
const port = await onServerReady(httpServer);
|
|
12
15
|
const webSocketServer = await createWebSocketServer(httpServer);
|
|
13
16
|
const getTransports = () => createWsTransports(port, webSocketServer, codec);
|
|
14
17
|
afterAll(() => {
|
|
15
|
-
webSocketServer.
|
|
16
|
-
socket.close();
|
|
17
|
-
});
|
|
18
|
+
webSocketServer.close();
|
|
18
19
|
httpServer.close();
|
|
19
20
|
});
|
|
20
21
|
test('rpc', async () => {
|
|
@@ -22,19 +23,24 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
22
23
|
const serviceDefs = { test: TestServiceConstructor() };
|
|
23
24
|
const server = await createServer(serverTransport, serviceDefs);
|
|
24
25
|
const client = createClient(clientTransport);
|
|
25
|
-
const result = await client.test.add({ n: 3 });
|
|
26
|
+
const result = await client.test.add.rpc({ n: 3 });
|
|
26
27
|
assert(result.ok);
|
|
27
28
|
expect(result.payload).toStrictEqual({ result: 3 });
|
|
29
|
+
await testFinishesCleanly({
|
|
30
|
+
clientTransports: [clientTransport],
|
|
31
|
+
serverTransport,
|
|
32
|
+
server,
|
|
33
|
+
});
|
|
28
34
|
});
|
|
29
35
|
test('fallible rpc', async () => {
|
|
30
36
|
const [clientTransport, serverTransport] = getTransports();
|
|
31
37
|
const serviceDefs = { test: FallibleServiceConstructor() };
|
|
32
38
|
const server = await createServer(serverTransport, serviceDefs);
|
|
33
39
|
const client = createClient(clientTransport);
|
|
34
|
-
const result = await client.test.divide({ a: 10, b: 2 });
|
|
40
|
+
const result = await client.test.divide.rpc({ a: 10, b: 2 });
|
|
35
41
|
assert(result.ok);
|
|
36
42
|
expect(result.payload).toStrictEqual({ result: 5 });
|
|
37
|
-
const result2 = await client.test.divide({ a: 10, b: 0 });
|
|
43
|
+
const result2 = await client.test.divide.rpc({ a: 10, b: 0 });
|
|
38
44
|
assert(!result2.ok);
|
|
39
45
|
expect(result2.payload).toStrictEqual({
|
|
40
46
|
code: DIV_BY_ZERO,
|
|
@@ -43,57 +49,125 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
43
49
|
test: 'abc',
|
|
44
50
|
},
|
|
45
51
|
});
|
|
52
|
+
await testFinishesCleanly({
|
|
53
|
+
clientTransports: [clientTransport],
|
|
54
|
+
serverTransport,
|
|
55
|
+
server,
|
|
56
|
+
});
|
|
46
57
|
});
|
|
47
58
|
test('rpc with binary (uint8array)', async () => {
|
|
48
59
|
const [clientTransport, serverTransport] = getTransports();
|
|
49
60
|
const serviceDefs = { test: BinaryFileServiceConstructor() };
|
|
50
61
|
const server = await createServer(serverTransport, serviceDefs);
|
|
51
62
|
const client = createClient(clientTransport);
|
|
52
|
-
const result = await client.test.getFile({ file: 'test.py' });
|
|
63
|
+
const result = await client.test.getFile.rpc({ file: 'test.py' });
|
|
53
64
|
assert(result.ok);
|
|
54
65
|
assert(result.payload.contents instanceof Uint8Array);
|
|
55
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
|
+
});
|
|
56
72
|
});
|
|
57
73
|
test('stream', async () => {
|
|
58
74
|
const [clientTransport, serverTransport] = getTransports();
|
|
59
75
|
const serviceDefs = { test: TestServiceConstructor() };
|
|
60
76
|
const server = await createServer(serverTransport, serviceDefs);
|
|
61
77
|
const client = createClient(clientTransport);
|
|
62
|
-
const [input, output, close] = await client.test.echo();
|
|
78
|
+
const [input, output, close] = await client.test.echo.stream();
|
|
63
79
|
input.push({ msg: 'abc', ignore: false });
|
|
64
80
|
input.push({ msg: 'def', ignore: true });
|
|
65
81
|
input.push({ msg: 'ghi', ignore: false });
|
|
82
|
+
input.push({ msg: 'end', ignore: false, end: true });
|
|
66
83
|
input.end();
|
|
67
|
-
const result1 = await output
|
|
84
|
+
const result1 = await iterNext(output);
|
|
68
85
|
assert(result1.ok);
|
|
69
86
|
expect(result1.payload).toStrictEqual({ response: 'abc' });
|
|
70
|
-
const result2 = await output
|
|
87
|
+
const result2 = await iterNext(output);
|
|
71
88
|
assert(result2.ok);
|
|
72
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);
|
|
73
96
|
close();
|
|
97
|
+
await testFinishesCleanly({
|
|
98
|
+
clientTransports: [clientTransport],
|
|
99
|
+
serverTransport,
|
|
100
|
+
server,
|
|
101
|
+
});
|
|
74
102
|
});
|
|
75
103
|
test('fallible stream', async () => {
|
|
76
104
|
const [clientTransport, serverTransport] = getTransports();
|
|
77
105
|
const serviceDefs = { test: FallibleServiceConstructor() };
|
|
78
106
|
const server = await createServer(serverTransport, serviceDefs);
|
|
79
107
|
const client = createClient(clientTransport);
|
|
80
|
-
const [input, output, close] = await client.test.echo();
|
|
108
|
+
const [input, output, close] = await client.test.echo.stream();
|
|
81
109
|
input.push({ msg: 'abc', throwResult: false, throwError: false });
|
|
82
|
-
const result1 = await output
|
|
110
|
+
const result1 = await iterNext(output);
|
|
83
111
|
assert(result1 && result1.ok);
|
|
84
112
|
expect(result1.payload).toStrictEqual({ response: 'abc' });
|
|
85
113
|
input.push({ msg: 'def', throwResult: true, throwError: false });
|
|
86
|
-
const result2 = await output
|
|
114
|
+
const result2 = await iterNext(output);
|
|
87
115
|
assert(result2 && !result2.ok);
|
|
88
116
|
expect(result2.payload.code).toStrictEqual(STREAM_ERROR);
|
|
89
117
|
input.push({ msg: 'ghi', throwResult: false, throwError: true });
|
|
90
|
-
const result3 = await output
|
|
118
|
+
const result3 = await iterNext(output);
|
|
91
119
|
assert(result3 && !result3.ok);
|
|
92
120
|
expect(result3.payload).toStrictEqual({
|
|
93
121
|
code: UNCAUGHT_ERROR,
|
|
94
122
|
message: 'some message',
|
|
95
123
|
});
|
|
96
124
|
close();
|
|
125
|
+
await testFinishesCleanly({
|
|
126
|
+
clientTransports: [clientTransport],
|
|
127
|
+
serverTransport,
|
|
128
|
+
server,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
test('subscription', async () => {
|
|
132
|
+
const options = { codec };
|
|
133
|
+
const serverTransport = new WebSocketServerTransport(webSocketServer, 'SERVER', options);
|
|
134
|
+
const client1Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client1', 'SERVER', options);
|
|
135
|
+
const client2Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client2', 'SERVER', options);
|
|
136
|
+
const serviceDefs = { test: SubscribableServiceConstructor() };
|
|
137
|
+
const server = await createServer(serverTransport, serviceDefs);
|
|
138
|
+
const client1 = createClient(client1Transport);
|
|
139
|
+
const client2 = createClient(client2Transport);
|
|
140
|
+
const [subscription1, close1] = await client1.test.value.subscribe({});
|
|
141
|
+
let result = await iterNext(subscription1);
|
|
142
|
+
assert(result.ok);
|
|
143
|
+
expect(result.payload).toStrictEqual({ result: 0 });
|
|
144
|
+
const [subscription2, close2] = await client2.test.value.subscribe({});
|
|
145
|
+
result = await iterNext(subscription2);
|
|
146
|
+
assert(result.ok);
|
|
147
|
+
expect(result.payload).toStrictEqual({ result: 0 });
|
|
148
|
+
const add1 = await client1.test.add.rpc({ n: 1 });
|
|
149
|
+
assert(add1.ok);
|
|
150
|
+
result = await iterNext(subscription1);
|
|
151
|
+
assert(result.ok);
|
|
152
|
+
expect(result.payload).toStrictEqual({ result: 1 });
|
|
153
|
+
result = await iterNext(subscription2);
|
|
154
|
+
assert(result.ok);
|
|
155
|
+
expect(result.payload).toStrictEqual({ result: 1 });
|
|
156
|
+
const add2 = await client2.test.add.rpc({ n: 3 });
|
|
157
|
+
assert(add2.ok);
|
|
158
|
+
result = await iterNext(subscription1);
|
|
159
|
+
assert(result.ok);
|
|
160
|
+
expect(result.payload).toStrictEqual({ result: 4 });
|
|
161
|
+
result = await iterNext(subscription2);
|
|
162
|
+
assert(result.ok);
|
|
163
|
+
expect(result.payload).toStrictEqual({ result: 4 });
|
|
164
|
+
close1();
|
|
165
|
+
close2();
|
|
166
|
+
await testFinishesCleanly({
|
|
167
|
+
clientTransports: [client1Transport, client2Transport],
|
|
168
|
+
serverTransport,
|
|
169
|
+
server,
|
|
170
|
+
});
|
|
97
171
|
});
|
|
98
172
|
test('message order is preserved in the face of disconnects', async () => {
|
|
99
173
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -109,13 +183,18 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
109
183
|
if (i == 42) {
|
|
110
184
|
clientTransport.connections.forEach((conn) => conn.ws.terminate());
|
|
111
185
|
}
|
|
112
|
-
await client.test.add({
|
|
186
|
+
await client.test.add.rpc({
|
|
113
187
|
n: i,
|
|
114
188
|
});
|
|
115
189
|
}
|
|
116
|
-
const res = await client.test.getAll({});
|
|
190
|
+
const res = await client.test.getAll.rpc({});
|
|
117
191
|
assert(res.ok);
|
|
118
|
-
|
|
192
|
+
expect(res.payload.msgs).toStrictEqual(expected);
|
|
193
|
+
await testFinishesCleanly({
|
|
194
|
+
clientTransports: [clientTransport],
|
|
195
|
+
serverTransport,
|
|
196
|
+
server,
|
|
197
|
+
});
|
|
119
198
|
});
|
|
120
199
|
const CONCURRENCY = 10;
|
|
121
200
|
test('concurrent rpcs', async () => {
|
|
@@ -125,13 +204,18 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
125
204
|
const client = createClient(clientTransport);
|
|
126
205
|
const promises = [];
|
|
127
206
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
128
|
-
promises.push(client.test.add({ n: i }));
|
|
207
|
+
promises.push(client.test.add.rpc({ n: i }));
|
|
129
208
|
}
|
|
130
209
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
131
210
|
const result = await promises[i];
|
|
132
211
|
assert(result.ok);
|
|
133
212
|
expect(result.payload).toStrictEqual({ n: i });
|
|
134
213
|
}
|
|
214
|
+
await testFinishesCleanly({
|
|
215
|
+
clientTransports: [clientTransport],
|
|
216
|
+
serverTransport,
|
|
217
|
+
server,
|
|
218
|
+
});
|
|
135
219
|
});
|
|
136
220
|
test('concurrent streams', async () => {
|
|
137
221
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -140,7 +224,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
140
224
|
const client = createClient(clientTransport);
|
|
141
225
|
const openStreams = [];
|
|
142
226
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
143
|
-
const streamHandle = await client.test.echo();
|
|
227
|
+
const streamHandle = await client.test.echo.stream();
|
|
144
228
|
const input = streamHandle[0];
|
|
145
229
|
input.push({ msg: `${i}-1`, ignore: false });
|
|
146
230
|
input.push({ msg: `${i}-2`, ignore: false });
|
|
@@ -148,10 +232,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
148
232
|
}
|
|
149
233
|
for (let i = 0; i < CONCURRENCY; i++) {
|
|
150
234
|
const output = openStreams[i][1];
|
|
151
|
-
const result1 = await output
|
|
235
|
+
const result1 = await iterNext(output);
|
|
152
236
|
assert(result1.ok);
|
|
153
237
|
expect(result1.payload).toStrictEqual({ response: `${i}-1` });
|
|
154
|
-
const result2 = await output
|
|
238
|
+
const result2 = await iterNext(output);
|
|
155
239
|
assert(result2.ok);
|
|
156
240
|
expect(result2.payload).toStrictEqual({ response: `${i}-2` });
|
|
157
241
|
}
|
|
@@ -160,5 +244,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
160
244
|
const [_input, _output, close] = openStreams[i];
|
|
161
245
|
close();
|
|
162
246
|
}
|
|
247
|
+
await testFinishesCleanly({
|
|
248
|
+
clientTransports: [clientTransport],
|
|
249
|
+
serverTransport,
|
|
250
|
+
server,
|
|
251
|
+
});
|
|
163
252
|
});
|
|
164
253
|
});
|
|
@@ -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.messageHandlers, `transport ${t.clientId} should not have open message handlers after the test`).toStrictEqual(new Set());
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an observable value that can be subscribed to for changes.
|
|
3
|
+
* This should only be used in tests
|
|
4
|
+
* @template T - The type of the value being observed.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Observable<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
private listeners;
|
|
9
|
+
constructor(initialValue: T);
|
|
10
|
+
/**
|
|
11
|
+
* Gets the current value of the observable.
|
|
12
|
+
*/
|
|
13
|
+
get(): T;
|
|
14
|
+
/**
|
|
15
|
+
* Sets the current value of the observable. All listeners will get an update with this value.
|
|
16
|
+
* @param newValue - The new value to set.
|
|
17
|
+
*/
|
|
18
|
+
set(tx: (preValue: T) => T): void;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribes to changes in the observable value.
|
|
21
|
+
* @param listener - A callback function that will be called when the value changes.
|
|
22
|
+
* @returns A function that can be called to unsubscribe from further notifications.
|
|
23
|
+
*/
|
|
24
|
+
observe(listener: (val: T) => void): () => boolean;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=observable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observable.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/observable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,UAAU,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,SAAS,CAAwB;gBAE7B,YAAY,EAAE,CAAC;IAK3B;;OAEG;IACH,GAAG;IAIH;;;OAGG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC;IAM1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI;CAKnC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an observable value that can be subscribed to for changes.
|
|
3
|
+
* This should only be used in tests
|
|
4
|
+
* @template T - The type of the value being observed.
|
|
5
|
+
*/
|
|
6
|
+
export class Observable {
|
|
7
|
+
value;
|
|
8
|
+
listeners;
|
|
9
|
+
constructor(initialValue) {
|
|
10
|
+
this.value = initialValue;
|
|
11
|
+
this.listeners = new Set();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Gets the current value of the observable.
|
|
15
|
+
*/
|
|
16
|
+
get() {
|
|
17
|
+
return this.value;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sets the current value of the observable. All listeners will get an update with this value.
|
|
21
|
+
* @param newValue - The new value to set.
|
|
22
|
+
*/
|
|
23
|
+
set(tx) {
|
|
24
|
+
const newValue = tx(this.value);
|
|
25
|
+
this.value = newValue;
|
|
26
|
+
this.listeners.forEach((listener) => listener(newValue));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Subscribes to changes in the observable value.
|
|
30
|
+
* @param listener - A callback function that will be called when the value changes.
|
|
31
|
+
* @returns A function that can be called to unsubscribe from further notifications.
|
|
32
|
+
*/
|
|
33
|
+
observe(listener) {
|
|
34
|
+
this.listeners.add(listener);
|
|
35
|
+
listener(this.get());
|
|
36
|
+
return () => this.listeners.delete(listener);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observable.test.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/observable.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Observable } from './observable';
|
|
2
|
+
import { describe, expect, test, vitest } from 'vitest';
|
|
3
|
+
describe('Observable', () => {
|
|
4
|
+
test('should set initial value correctly', () => {
|
|
5
|
+
const initialValue = 10;
|
|
6
|
+
const observable = new Observable(initialValue);
|
|
7
|
+
expect(observable.value).toBe(initialValue);
|
|
8
|
+
});
|
|
9
|
+
test('should update value correctly', () => {
|
|
10
|
+
const observable = new Observable(10);
|
|
11
|
+
const newValue = 20;
|
|
12
|
+
observable.set(() => newValue);
|
|
13
|
+
expect(observable.value).toBe(newValue);
|
|
14
|
+
});
|
|
15
|
+
test('should notify listeners when value changes', () => {
|
|
16
|
+
const observable = new Observable(10);
|
|
17
|
+
const listener = vitest.fn();
|
|
18
|
+
observable.observe(listener);
|
|
19
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
20
|
+
const newValue = 20;
|
|
21
|
+
observable.set(() => newValue);
|
|
22
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
23
|
+
expect(listener).toHaveBeenCalledWith(newValue);
|
|
24
|
+
});
|
|
25
|
+
test('should unsubscribe from notifications', () => {
|
|
26
|
+
const observable = new Observable(10);
|
|
27
|
+
const listener = vitest.fn();
|
|
28
|
+
const unsubscribe = observable.observe(listener);
|
|
29
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
30
|
+
const newValue = 20;
|
|
31
|
+
observable.set(() => newValue);
|
|
32
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
33
|
+
expect(listener).toHaveBeenCalledWith(newValue);
|
|
34
|
+
unsubscribe();
|
|
35
|
+
const anotherValue = 30;
|
|
36
|
+
observable.set(() => anotherValue);
|
|
37
|
+
expect(listener).toHaveBeenCalledTimes(2); // should not be called again after unsubscribing
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Observable } from './observable';
|
|
1
2
|
export declare const EchoRequest: import("@sinclair/typebox").TObject<{
|
|
2
3
|
msg: import("@sinclair/typebox").TString;
|
|
3
4
|
ignore: import("@sinclair/typebox").TBoolean;
|
|
5
|
+
end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
4
6
|
}>;
|
|
5
7
|
export declare const EchoResponse: import("@sinclair/typebox").TObject<{
|
|
6
8
|
response: import("@sinclair/typebox").TString;
|
|
@@ -19,11 +21,11 @@ export declare const TestServiceConstructor: () => {
|
|
|
19
21
|
result: import("@sinclair/typebox").TNumber;
|
|
20
22
|
}>;
|
|
21
23
|
errors: import("@sinclair/typebox").TNever;
|
|
22
|
-
handler: (context: import("
|
|
24
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
23
25
|
count: number;
|
|
24
|
-
}>, input: import("
|
|
26
|
+
}>, input: import("../../transport/message").TransportMessage<{
|
|
25
27
|
n: number;
|
|
26
|
-
}>) => Promise<import("
|
|
28
|
+
}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
27
29
|
result: number;
|
|
28
30
|
}, never>>>;
|
|
29
31
|
type: "rpc";
|
|
@@ -33,17 +35,19 @@ export declare const TestServiceConstructor: () => {
|
|
|
33
35
|
input: import("@sinclair/typebox").TObject<{
|
|
34
36
|
msg: import("@sinclair/typebox").TString;
|
|
35
37
|
ignore: import("@sinclair/typebox").TBoolean;
|
|
38
|
+
end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
36
39
|
}>;
|
|
37
40
|
output: import("@sinclair/typebox").TObject<{
|
|
38
41
|
response: import("@sinclair/typebox").TString;
|
|
39
42
|
}>;
|
|
40
43
|
errors: import("@sinclair/typebox").TNever;
|
|
41
|
-
handler: (context: import("
|
|
44
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
42
45
|
count: number;
|
|
43
|
-
}>, input: AsyncIterable<import("
|
|
46
|
+
}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
47
|
+
end?: boolean | undefined;
|
|
44
48
|
msg: string;
|
|
45
49
|
ignore: boolean;
|
|
46
|
-
}>>, output: import("it-pushable").Pushable<import("
|
|
50
|
+
}>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
47
51
|
response: string;
|
|
48
52
|
}, never>>, void, unknown>) => Promise<void>;
|
|
49
53
|
type: "stream";
|
|
@@ -64,11 +68,11 @@ export declare const OrderingServiceConstructor: () => {
|
|
|
64
68
|
n: import("@sinclair/typebox").TNumber;
|
|
65
69
|
}>;
|
|
66
70
|
errors: import("@sinclair/typebox").TNever;
|
|
67
|
-
handler: (context: import("
|
|
71
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
68
72
|
msgs: number[];
|
|
69
|
-
}>, input: import("
|
|
73
|
+
}>, input: import("../../transport/message").TransportMessage<{
|
|
70
74
|
n: number;
|
|
71
|
-
}>) => Promise<import("
|
|
75
|
+
}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
72
76
|
n: number;
|
|
73
77
|
}, never>>>;
|
|
74
78
|
type: "rpc";
|
|
@@ -80,9 +84,9 @@ export declare const OrderingServiceConstructor: () => {
|
|
|
80
84
|
msgs: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TNumber>;
|
|
81
85
|
}>;
|
|
82
86
|
errors: import("@sinclair/typebox").TNever;
|
|
83
|
-
handler: (context: import("
|
|
87
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
84
88
|
msgs: number[];
|
|
85
|
-
}>, input: import("
|
|
89
|
+
}>, input: import("../../transport/message").TransportMessage<{}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
86
90
|
msgs: number[];
|
|
87
91
|
}, never>>>;
|
|
88
92
|
type: "rpc";
|
|
@@ -101,9 +105,9 @@ export declare const BinaryFileServiceConstructor: () => {
|
|
|
101
105
|
contents: import("@sinclair/typebox").TUint8Array;
|
|
102
106
|
}>;
|
|
103
107
|
errors: import("@sinclair/typebox").TNever;
|
|
104
|
-
handler: (context: import("
|
|
108
|
+
handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
|
|
105
109
|
file: string;
|
|
106
|
-
}>) => Promise<import("
|
|
110
|
+
}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
107
111
|
contents: Uint8Array;
|
|
108
112
|
}, never>>>;
|
|
109
113
|
type: "rpc";
|
|
@@ -131,10 +135,10 @@ export declare const FallibleServiceConstructor: () => {
|
|
|
131
135
|
test: import("@sinclair/typebox").TString;
|
|
132
136
|
}>;
|
|
133
137
|
}>;
|
|
134
|
-
handler: (context: import("
|
|
138
|
+
handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
|
|
135
139
|
a: number;
|
|
136
140
|
b: number;
|
|
137
|
-
}>) => Promise<import("
|
|
141
|
+
}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
138
142
|
result: number;
|
|
139
143
|
}, {
|
|
140
144
|
message: string;
|
|
@@ -159,11 +163,11 @@ export declare const FallibleServiceConstructor: () => {
|
|
|
159
163
|
code: import("@sinclair/typebox").TLiteral<"STREAM_ERROR">;
|
|
160
164
|
message: import("@sinclair/typebox").TString;
|
|
161
165
|
}>;
|
|
162
|
-
handler: (context: import("
|
|
166
|
+
handler: (context: import("../../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
|
|
163
167
|
msg: string;
|
|
164
168
|
throwResult: boolean;
|
|
165
169
|
throwError: boolean;
|
|
166
|
-
}>>, output: import("it-pushable").Pushable<import("
|
|
170
|
+
}>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
167
171
|
response: string;
|
|
168
172
|
}, {
|
|
169
173
|
message: string;
|
|
@@ -173,4 +177,43 @@ export declare const FallibleServiceConstructor: () => {
|
|
|
173
177
|
};
|
|
174
178
|
};
|
|
175
179
|
};
|
|
176
|
-
|
|
180
|
+
export declare const SubscribableServiceConstructor: () => {
|
|
181
|
+
name: "subscribable";
|
|
182
|
+
state: {
|
|
183
|
+
count: Observable<number>;
|
|
184
|
+
};
|
|
185
|
+
procedures: {
|
|
186
|
+
add: {
|
|
187
|
+
input: import("@sinclair/typebox").TObject<{
|
|
188
|
+
n: import("@sinclair/typebox").TNumber;
|
|
189
|
+
}>;
|
|
190
|
+
output: import("@sinclair/typebox").TObject<{
|
|
191
|
+
result: import("@sinclair/typebox").TNumber;
|
|
192
|
+
}>;
|
|
193
|
+
errors: import("@sinclair/typebox").TNever;
|
|
194
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
195
|
+
count: Observable<number>;
|
|
196
|
+
}>, input: import("../../transport/message").TransportMessage<{
|
|
197
|
+
n: number;
|
|
198
|
+
}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
199
|
+
result: number;
|
|
200
|
+
}, never>>>;
|
|
201
|
+
type: "rpc";
|
|
202
|
+
};
|
|
203
|
+
} & {
|
|
204
|
+
value: {
|
|
205
|
+
input: import("@sinclair/typebox").TObject<{}>;
|
|
206
|
+
output: import("@sinclair/typebox").TObject<{
|
|
207
|
+
result: import("@sinclair/typebox").TNumber;
|
|
208
|
+
}>;
|
|
209
|
+
errors: import("@sinclair/typebox").TNever;
|
|
210
|
+
handler: (context: import("../../router").ServiceContextWithState<{
|
|
211
|
+
count: Observable<number>;
|
|
212
|
+
}>, input: import("../../transport/message").TransportMessage<{}>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
213
|
+
result: number;
|
|
214
|
+
}, never>>, void, unknown>) => Promise<void>;
|
|
215
|
+
type: "subscription";
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
//# sourceMappingURL=services.d.ts.map
|
|
@@ -0,0 +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;;;;EAItB,CAAC;AACH,eAAO,MAAM,YAAY;;EAA2C,CAAC;AAErE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCpB,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"}
|