@replit/river 0.8.0 → 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 +1 -1
- package/dist/__tests__/e2e.test.js +55 -4
- 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 +3 -0
- package/dist/__tests__/fixtures/services.d.ts.map +1 -1
- package/dist/__tests__/fixtures/services.js +4 -0
- 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 +1 -0
- package/dist/router/client.d.ts.map +1 -1
- package/dist/router/client.js +15 -6
- package/dist/router/server.d.ts +14 -0
- package/dist/router/server.d.ts.map +1 -1
- package/dist/router/server.js +61 -41
- 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 +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/package.json +2 -2
- /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';
|
|
@@ -8,15 +8,14 @@ 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,18 @@ 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
|
+
});
|
|
76
102
|
});
|
|
77
103
|
test('fallible stream', async () => {
|
|
78
104
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -96,6 +122,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
96
122
|
message: 'some message',
|
|
97
123
|
});
|
|
98
124
|
close();
|
|
125
|
+
await testFinishesCleanly({
|
|
126
|
+
clientTransports: [clientTransport],
|
|
127
|
+
serverTransport,
|
|
128
|
+
server,
|
|
129
|
+
});
|
|
99
130
|
});
|
|
100
131
|
test('subscription', async () => {
|
|
101
132
|
const options = { codec };
|
|
@@ -132,6 +163,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
132
163
|
expect(result.payload).toStrictEqual({ result: 4 });
|
|
133
164
|
close1();
|
|
134
165
|
close2();
|
|
166
|
+
await testFinishesCleanly({
|
|
167
|
+
clientTransports: [client1Transport, client2Transport],
|
|
168
|
+
serverTransport,
|
|
169
|
+
server,
|
|
170
|
+
});
|
|
135
171
|
});
|
|
136
172
|
test('message order is preserved in the face of disconnects', async () => {
|
|
137
173
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -153,7 +189,12 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
153
189
|
}
|
|
154
190
|
const res = await client.test.getAll.rpc({});
|
|
155
191
|
assert(res.ok);
|
|
156
|
-
|
|
192
|
+
expect(res.payload.msgs).toStrictEqual(expected);
|
|
193
|
+
await testFinishesCleanly({
|
|
194
|
+
clientTransports: [clientTransport],
|
|
195
|
+
serverTransport,
|
|
196
|
+
server,
|
|
197
|
+
});
|
|
157
198
|
});
|
|
158
199
|
const CONCURRENCY = 10;
|
|
159
200
|
test('concurrent rpcs', async () => {
|
|
@@ -170,6 +211,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
170
211
|
assert(result.ok);
|
|
171
212
|
expect(result.payload).toStrictEqual({ n: i });
|
|
172
213
|
}
|
|
214
|
+
await testFinishesCleanly({
|
|
215
|
+
clientTransports: [clientTransport],
|
|
216
|
+
serverTransport,
|
|
217
|
+
server,
|
|
218
|
+
});
|
|
173
219
|
});
|
|
174
220
|
test('concurrent streams', async () => {
|
|
175
221
|
const [clientTransport, serverTransport] = getTransports();
|
|
@@ -198,5 +244,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
|
|
|
198
244
|
const [_input, _output, close] = openStreams[i];
|
|
199
245
|
close();
|
|
200
246
|
}
|
|
247
|
+
await testFinishesCleanly({
|
|
248
|
+
clientTransports: [clientTransport],
|
|
249
|
+
serverTransport,
|
|
250
|
+
server,
|
|
251
|
+
});
|
|
201
252
|
});
|
|
202
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
|
+
}
|
|
@@ -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,7 @@ 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;
|
|
45
48
|
msg: string;
|
|
46
49
|
ignore: boolean;
|
|
47
50
|
}>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
@@ -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,9 @@ 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
|
+
}
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
})
|
|
@@ -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.messageHandlers.size;
|
|
56
|
+
let clientListeners = clientTransport.messageHandlers.size;
|
|
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.messageHandlers.size).toEqual(serverListeners);
|
|
62
|
+
expect(clientTransport.messageHandlers.size).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.messageHandlers.size;
|
|
77
|
+
let clientListeners = clientTransport.messageHandlers.size;
|
|
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.messageHandlers.size).toEqual(serverListeners);
|
|
97
|
+
expect(clientTransport.messageHandlers.size).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.messageHandlers.size;
|
|
112
|
+
let clientListeners = clientTransport.messageHandlers.size;
|
|
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.messageHandlers.size).toEqual(serverListeners);
|
|
127
|
+
expect(clientTransport.messageHandlers.size).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
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../router/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACT,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAIL,iBAAiB,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../router/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACT,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAIL,iBAAiB,EAGlB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,KAAK,SAAS,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;AAElD;;;;GAIG;AACH,KAAK,aAAa,CAAC,MAAM,SAAS,UAAU,IAAI;KAC7C,QAAQ,IAAI,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,QAAQ,CAChD,MAAM,EACN,QAAQ,CACT,SAAS,KAAK,GACX;QACE,GAAG,EAAE,CACH,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KACvC,OAAO,CACV,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF,CAAC;KACH,GACD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,QAAQ,GAC3C;QACE,MAAM,EAAE,MAAM,OAAO,CACnB;YACE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7C,SAAS,CACP,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF;YACD,MAAM,IAAI;SACX,CACF,CAAC;KACH,GACD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,cAAc,GACjD;QACE,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,OAAO,CAChE;YACE,SAAS,CACP,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF;YACD,MAAM,IAAI;SACX,CACF,CAAC;KACH,GACD,KAAK;CACV,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI;KACxE,OAAO,IAAI,MAAM,GAAG,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5E,CAAC;AAgCF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,8DACZ,UAAU,UAAU,CAAC,aACtB,iBAAiB,sBAoIA,CAAC"}
|
package/dist/router/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
|
-
import { msg, } from '../transport/message';
|
|
2
|
+
import { msg, isStreamClose, closeStream, } from '../transport/message';
|
|
3
3
|
import { waitForMessage } from '../transport';
|
|
4
4
|
import { nanoid } from 'nanoid';
|
|
5
5
|
const noop = () => { };
|
|
@@ -49,18 +49,25 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
|
|
|
49
49
|
if (procType === 'stream') {
|
|
50
50
|
const inputStream = pushable({ objectMode: true });
|
|
51
51
|
const outputStream = pushable({ objectMode: true });
|
|
52
|
+
let firstMessage = true;
|
|
52
53
|
// input -> transport
|
|
53
54
|
// this gets cleaned up on i.end() which is called by closeHandler
|
|
54
55
|
(async () => {
|
|
55
56
|
for await (const rawIn of inputStream) {
|
|
56
57
|
const m = msg(transport.clientId, serverId, serviceName, procName, streamId, rawIn);
|
|
57
|
-
|
|
58
|
+
if (firstMessage) {
|
|
59
|
+
m.controlFlags |= 2 /* ControlFlags.StreamOpenBit */;
|
|
60
|
+
firstMessage = false;
|
|
61
|
+
}
|
|
58
62
|
transport.send(m);
|
|
59
63
|
}
|
|
60
64
|
})();
|
|
61
65
|
// transport -> output
|
|
62
66
|
const listener = (msg) => {
|
|
63
|
-
if (
|
|
67
|
+
if (isStreamClose(msg.controlFlags)) {
|
|
68
|
+
outputStream.end();
|
|
69
|
+
}
|
|
70
|
+
else if (belongsToSameStream(msg)) {
|
|
64
71
|
outputStream.push(msg.payload);
|
|
65
72
|
}
|
|
66
73
|
};
|
|
@@ -68,9 +75,7 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
|
|
|
68
75
|
const closeHandler = () => {
|
|
69
76
|
inputStream.end();
|
|
70
77
|
outputStream.end();
|
|
71
|
-
|
|
72
|
-
closeMessage.controlFlags |= 4 /* ControlFlags.StreamClosedBit */;
|
|
73
|
-
transport.send(closeMessage);
|
|
78
|
+
transport.send(closeStream(transport.clientId, serverId, serviceName, procName, streamId));
|
|
74
79
|
transport.removeMessageListener(listener);
|
|
75
80
|
};
|
|
76
81
|
return [inputStream, outputStream, closeHandler];
|
|
@@ -93,10 +98,14 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
|
|
|
93
98
|
if (belongsToSameStream(msg)) {
|
|
94
99
|
outputStream.push(msg.payload);
|
|
95
100
|
}
|
|
101
|
+
if (isStreamClose(msg.controlFlags)) {
|
|
102
|
+
outputStream.end();
|
|
103
|
+
}
|
|
96
104
|
};
|
|
97
105
|
transport.addMessageListener(listener);
|
|
98
106
|
const closeHandler = () => {
|
|
99
107
|
outputStream.end();
|
|
108
|
+
transport.send(closeStream(transport.clientId, serverId, serviceName, procName, streamId));
|
|
100
109
|
transport.removeMessageListener(listener);
|
|
101
110
|
};
|
|
102
111
|
return [outputStream, closeHandler];
|
package/dist/router/server.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
import { Static, TObject } from '@sinclair/typebox';
|
|
1
2
|
import { Connection, Transport } from '../transport/transport';
|
|
2
3
|
import { AnyService } from './builder';
|
|
4
|
+
import type { Pushable } from 'it-pushable';
|
|
5
|
+
import { TransportMessage } from '../transport/message';
|
|
3
6
|
import { ServiceContext } from './context';
|
|
7
|
+
import { Result, RiverError } from './result';
|
|
4
8
|
/**
|
|
5
9
|
* Represents a server with a set of services. Use {@link createServer} to create it.
|
|
6
10
|
* @template Services - The type of services provided by the server.
|
|
7
11
|
*/
|
|
8
12
|
export interface Server<Services> {
|
|
9
13
|
services: Services;
|
|
14
|
+
streams: Map<string, ProcStream>;
|
|
10
15
|
close(): Promise<void>;
|
|
11
16
|
}
|
|
17
|
+
interface ProcStream {
|
|
18
|
+
incoming: Pushable<TransportMessage>;
|
|
19
|
+
outgoing: Pushable<TransportMessage<Result<Static<TObject>, Static<RiverError>>>>;
|
|
20
|
+
promises: {
|
|
21
|
+
outputHandler: Promise<unknown>;
|
|
22
|
+
inputHandler: Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* Creates a server instance that listens for incoming messages from a transport and routes them to the appropriate service and procedure.
|
|
14
27
|
* The server tracks the state of each service along with open streams and the extended context object.
|
|
@@ -18,4 +31,5 @@ export interface Server<Services> {
|
|
|
18
31
|
* @returns A promise that resolves to a server instance with the registered services.
|
|
19
32
|
*/
|
|
20
33
|
export declare function createServer<Services extends Record<string, AnyService>>(transport: Transport<Connection>, services: Services, extendedContext?: Omit<ServiceContext, 'state'>): Promise<Server<Services>>;
|
|
34
|
+
export {};
|
|
21
35
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAgB,UAAU,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAGL,gBAAgB,EAKjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAA2B,MAAM,WAAW,CAAC;AAGpE,OAAO,EAEL,MAAM,EACN,UAAU,EAGX,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,MAAM,WAAW,MAAM,CAAC,QAAQ;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,UAAU,UAAU;IAClB,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACrC,QAAQ,EAAE,QAAQ,CAChB,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAC9D,CAAC;IACF,QAAQ,EAAE;QACR,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;KAChC,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EAC5E,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAuM3B"}
|
package/dist/router/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
|
-
import { ControlMessagePayloadSchema, isStreamClose, isStreamOpen, reply, } from '../transport/message';
|
|
2
|
+
import { ControlMessagePayloadSchema, isStreamClose, isStreamOpen, reply, closeStream, } from '../transport/message';
|
|
3
3
|
import { log } from '../logging';
|
|
4
4
|
import { Value } from '@sinclair/typebox/value';
|
|
5
5
|
import { Err, UNCAUGHT_ERROR, } from './result';
|
|
@@ -15,6 +15,16 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
15
15
|
const contextMap = new Map();
|
|
16
16
|
// map of streamId to ProcStream
|
|
17
17
|
const streamMap = new Map();
|
|
18
|
+
async function cleanupStream(id) {
|
|
19
|
+
const stream = streamMap.get(id);
|
|
20
|
+
if (stream) {
|
|
21
|
+
stream.incoming.end();
|
|
22
|
+
await stream.promises.inputHandler;
|
|
23
|
+
stream.outgoing.end();
|
|
24
|
+
await stream.promises.outputHandler;
|
|
25
|
+
streamMap.delete(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
function getContext(service) {
|
|
19
29
|
const context = contextMap.get(service);
|
|
20
30
|
if (!context) {
|
|
@@ -28,50 +38,57 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
28
38
|
for (const service of Object.values(services)) {
|
|
29
39
|
contextMap.set(service, { ...extendedContext, state: service.state });
|
|
30
40
|
}
|
|
31
|
-
const handler = async (
|
|
32
|
-
if (
|
|
41
|
+
const handler = async (message) => {
|
|
42
|
+
if (message.to !== transport.clientId) {
|
|
33
43
|
log?.info(`${transport.clientId} -- got msg with destination that isn't the server, ignoring`);
|
|
34
44
|
return;
|
|
35
45
|
}
|
|
36
|
-
if (!(
|
|
37
|
-
log?.warn(`${transport.clientId} -- couldn't find service ${
|
|
46
|
+
if (!(message.serviceName in services)) {
|
|
47
|
+
log?.warn(`${transport.clientId} -- couldn't find service ${message.serviceName}`);
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
|
-
const service = services[
|
|
50
|
+
const service = services[message.serviceName];
|
|
41
51
|
const serviceContext = getContext(service);
|
|
42
|
-
if (!(
|
|
43
|
-
log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${
|
|
52
|
+
if (!(message.procedureName in service.procedures)) {
|
|
53
|
+
log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${message.serviceName}.${message.procedureName}`);
|
|
44
54
|
return;
|
|
45
55
|
}
|
|
46
|
-
const procedure = service.procedures[
|
|
47
|
-
const streamIdx = `${
|
|
48
|
-
if (isStreamOpen(
|
|
56
|
+
const procedure = service.procedures[message.procedureName];
|
|
57
|
+
const streamIdx = `${message.serviceName}.${message.procedureName}:${message.streamId}`;
|
|
58
|
+
if (isStreamOpen(message.controlFlags) && !streamMap.has(streamIdx)) {
|
|
49
59
|
const incoming = pushable({ objectMode: true });
|
|
50
60
|
const outgoing = pushable({ objectMode: true });
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const outputHandler =
|
|
62
|
+
// sending outgoing messages back to client
|
|
63
|
+
(async () => {
|
|
64
|
+
for await (const response of outgoing) {
|
|
65
|
+
transport.send(response);
|
|
66
|
+
}
|
|
67
|
+
// we ended, send a close bit back to the client
|
|
68
|
+
// only subscriptions and streams have streams the
|
|
69
|
+
// handler can close
|
|
70
|
+
if (procedure.type === 'subscription' ||
|
|
71
|
+
procedure.type === 'stream') {
|
|
72
|
+
transport.send(closeStream(transport.clientId, message.from, message.serviceName, message.procedureName, message.streamId));
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
59
75
|
function errorHandler(err) {
|
|
60
76
|
const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
|
|
61
|
-
log?.error(`${transport.clientId} -- procedure ${
|
|
62
|
-
outgoing.push(reply(
|
|
77
|
+
log?.error(`${transport.clientId} -- procedure ${message.serviceName}.${message.procedureName}:${message.streamId} threw an error: ${errorMsg}`);
|
|
78
|
+
outgoing.push(reply(message, Err({
|
|
63
79
|
code: UNCAUGHT_ERROR,
|
|
64
80
|
message: errorMsg,
|
|
65
81
|
})));
|
|
66
82
|
}
|
|
67
83
|
// pump incoming message stream -> handler -> outgoing message stream
|
|
84
|
+
let inputHandler;
|
|
68
85
|
if (procedure.type === 'stream') {
|
|
69
|
-
|
|
86
|
+
inputHandler = procedure
|
|
70
87
|
.handler(serviceContext, incoming, outgoing)
|
|
71
|
-
.catch(errorHandler)
|
|
88
|
+
.catch(errorHandler);
|
|
72
89
|
}
|
|
73
90
|
else if (procedure.type === 'rpc') {
|
|
74
|
-
|
|
91
|
+
inputHandler = (async () => {
|
|
75
92
|
const inputMessage = await incoming.next();
|
|
76
93
|
if (inputMessage.done) {
|
|
77
94
|
return;
|
|
@@ -83,10 +100,10 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
83
100
|
catch (err) {
|
|
84
101
|
errorHandler(err);
|
|
85
102
|
}
|
|
86
|
-
})()
|
|
103
|
+
})();
|
|
87
104
|
}
|
|
88
105
|
else if (procedure.type === 'subscription') {
|
|
89
|
-
|
|
106
|
+
inputHandler = (async () => {
|
|
90
107
|
const inputMessage = await incoming.next();
|
|
91
108
|
if (inputMessage.done) {
|
|
92
109
|
return;
|
|
@@ -97,40 +114,43 @@ export async function createServer(transport, services, extendedContext) {
|
|
|
97
114
|
catch (err) {
|
|
98
115
|
errorHandler(err);
|
|
99
116
|
}
|
|
100
|
-
})()
|
|
117
|
+
})();
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// procedure is inferred to be never here as this is not a valid procedure type
|
|
121
|
+
// we cast just to log
|
|
122
|
+
log?.warn(`${transport.clientId} -- got request for invalid procedure type ${procedure.type} at ${message.serviceName}.${message.procedureName}`);
|
|
123
|
+
return;
|
|
101
124
|
}
|
|
102
125
|
streamMap.set(streamIdx, {
|
|
103
126
|
incoming,
|
|
104
127
|
outgoing,
|
|
105
|
-
|
|
128
|
+
promises: { inputHandler, outputHandler },
|
|
106
129
|
});
|
|
107
130
|
}
|
|
108
131
|
const procStream = streamMap.get(streamIdx);
|
|
109
132
|
if (!procStream) {
|
|
110
|
-
log?.warn(`${transport.clientId} -- couldn't find a matching procedure stream for ${
|
|
133
|
+
log?.warn(`${transport.clientId} -- couldn't find a matching procedure stream for ${message.serviceName}.${message.procedureName}:${message.streamId}`);
|
|
111
134
|
return;
|
|
112
135
|
}
|
|
113
|
-
if (Value.Check(procedure.input,
|
|
114
|
-
procStream.incoming.push(
|
|
136
|
+
if (Value.Check(procedure.input, message.payload)) {
|
|
137
|
+
procStream.incoming.push(message);
|
|
115
138
|
}
|
|
116
|
-
else if (!Value.Check(ControlMessagePayloadSchema,
|
|
117
|
-
log?.error(`${transport.clientId} -- procedure ${
|
|
139
|
+
else if (!Value.Check(ControlMessagePayloadSchema, message.payload)) {
|
|
140
|
+
log?.error(`${transport.clientId} -- procedure ${message.serviceName}.${message.procedureName} received invalid payload: ${JSON.stringify(message.payload)}`);
|
|
118
141
|
}
|
|
119
|
-
if (isStreamClose(
|
|
120
|
-
|
|
121
|
-
await Promise.all(procStream.openPromises);
|
|
122
|
-
procStream.outgoing.end();
|
|
142
|
+
if (isStreamClose(message.controlFlags)) {
|
|
143
|
+
await cleanupStream(streamIdx);
|
|
123
144
|
}
|
|
124
145
|
};
|
|
125
146
|
transport.addMessageListener(handler);
|
|
126
147
|
return {
|
|
127
148
|
services,
|
|
149
|
+
streams: streamMap,
|
|
128
150
|
async close() {
|
|
129
151
|
transport.removeMessageListener(handler);
|
|
130
|
-
for (const
|
|
131
|
-
|
|
132
|
-
await Promise.all(stream.openPromises);
|
|
133
|
-
stream.outgoing.end();
|
|
152
|
+
for (const streamIdx of streamMap.keys()) {
|
|
153
|
+
await cleanupStream(streamIdx);
|
|
134
154
|
}
|
|
135
155
|
},
|
|
136
156
|
};
|
|
@@ -3,9 +3,6 @@ import { Codec } from '../../../codec';
|
|
|
3
3
|
import { TransportClientId } from '../../message';
|
|
4
4
|
import { Connection, Transport } from '../../transport';
|
|
5
5
|
export declare class StdioConnection extends Connection {
|
|
6
|
-
/**
|
|
7
|
-
* The writable stream to use as output.
|
|
8
|
-
*/
|
|
9
6
|
output: NodeJS.WritableStream;
|
|
10
7
|
constructor(transport: Transport<StdioConnection>, connectedTo: TransportClientId, output: NodeJS.WritableStream);
|
|
11
8
|
send(payload: Uint8Array): boolean;
|
|
@@ -16,7 +13,6 @@ interface Options {
|
|
|
16
13
|
}
|
|
17
14
|
/**
|
|
18
15
|
* A transport implementation that uses standard input and output streams.
|
|
19
|
-
* Can only be used 1:1, not N:1
|
|
20
16
|
* @extends Transport
|
|
21
17
|
*/
|
|
22
18
|
export declare class StdioTransport extends Transport<StdioConnection> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../../transport/impls/stdio/stdio.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKxD,qBAAa,eAAgB,SAAQ,UAAU;IAC7C
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../../transport/impls/stdio/stdio.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKxD,qBAAa,eAAgB,SAAQ,UAAU;IAC7C,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;gBAG5B,SAAS,EAAE,SAAS,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,iBAAiB,EAC9B,MAAM,EAAE,MAAM,CAAC,cAAc;IAM/B,IAAI,CAAC,OAAO,EAAE,UAAU;IAOlB,KAAK;CAGZ;AAED,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD;;;GAGG;AACH,qBAAa,cAAe,SAAQ,SAAS,CAAC,eAAe,CAAC;IAC5D,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC,cAAc,CAAiB;IAC7C,MAAM,EAAE,MAAM,CAAC,cAAc,CAAkB;IAE/C;;;;;OAKG;gBAED,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,GAAE,MAAM,CAAC,cAA8B,EAC5C,MAAM,GAAE,MAAM,CAAC,cAA+B,EAC9C,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAUpC,8BAA8B,IAAI,IAAI;IAwBhC,mBAAmB,CAAC,EAAE,EAAE,iBAAiB;CAShD"}
|
|
@@ -4,9 +4,6 @@ import { Connection, Transport } from '../../transport';
|
|
|
4
4
|
import readline from 'readline';
|
|
5
5
|
const newlineBuff = new TextEncoder().encode('\n');
|
|
6
6
|
export class StdioConnection extends Connection {
|
|
7
|
-
/**
|
|
8
|
-
* The writable stream to use as output.
|
|
9
|
-
*/
|
|
10
7
|
output;
|
|
11
8
|
constructor(transport, connectedTo, output) {
|
|
12
9
|
super(transport, connectedTo);
|
|
@@ -19,7 +16,6 @@ export class StdioConnection extends Connection {
|
|
|
19
16
|
return this.output.write(out);
|
|
20
17
|
}
|
|
21
18
|
async close() {
|
|
22
|
-
this.transport.onDisconnect(this);
|
|
23
19
|
this.output.end();
|
|
24
20
|
}
|
|
25
21
|
}
|
|
@@ -28,7 +24,6 @@ const defaultOptions = {
|
|
|
28
24
|
};
|
|
29
25
|
/**
|
|
30
26
|
* A transport implementation that uses standard input and output streams.
|
|
31
|
-
* Can only be used 1:1, not N:1
|
|
32
27
|
* @extends Transport
|
|
33
28
|
*/
|
|
34
29
|
export class StdioTransport extends Transport {
|
|
@@ -3,6 +3,7 @@ import stream from 'node:stream';
|
|
|
3
3
|
import { StdioTransport } from './stdio';
|
|
4
4
|
import { waitForMessage } from '../..';
|
|
5
5
|
import { payloadToTransportMessage } from '../../../util/testHelpers';
|
|
6
|
+
import { ensureTransportIsClean } from '../../../__tests__/fixtures/cleanup';
|
|
6
7
|
describe('sending and receiving across node streams works', () => {
|
|
7
8
|
test('basic send/receive', async () => {
|
|
8
9
|
const clientToServer = new stream.PassThrough();
|
|
@@ -16,5 +17,9 @@ describe('sending and receiving across node streams works', () => {
|
|
|
16
17
|
const p = waitForMessage(serverTransport);
|
|
17
18
|
clientTransport.send(payloadToTransportMessage(msg, 'stream', clientTransport.clientId, serverTransport.clientId));
|
|
18
19
|
await expect(p).resolves.toStrictEqual(msg);
|
|
20
|
+
await clientTransport.close();
|
|
21
|
+
await serverTransport.close();
|
|
22
|
+
await ensureTransportIsClean(clientTransport);
|
|
23
|
+
await ensureTransportIsClean(serverTransport);
|
|
19
24
|
});
|
|
20
25
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/client.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC;CACd;AAQD,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,iBAAiB,CAAC;IAE5B,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;IAEpE;;;;;OAKG;gBAED,QAAQ,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAClC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAWpC,8BAA8B,IAAI,IAAI;IAIhC,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,SAAI;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/client.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC;CACd;AAQD,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,iBAAiB,CAAC;IAE5B,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;IAEpE;;;;;OAKG;gBAED,QAAQ,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAClC,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAWpC,8BAA8B,IAAI,IAAI;IAIhC,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,SAAI;IAuEjD;;OAEG;IACG,IAAI;CAGX"}
|
|
@@ -83,13 +83,13 @@ export class WebSocketClientTransport extends Transport {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
// otherwise try and reconnect again
|
|
86
|
-
log?.warn(`${this.clientId} -- websocket failed, trying again in ${this.options.retryIntervalMs}ms`);
|
|
87
86
|
this.reconnectPromises.delete(to);
|
|
88
87
|
if (attempt >= this.options.retryAttemptsMax) {
|
|
89
|
-
|
|
88
|
+
throw new Error(`${this.clientId} -- websocket to ${to} failed after ${attempt} attempts, giving up`);
|
|
90
89
|
}
|
|
91
90
|
else {
|
|
92
91
|
// linear backoff
|
|
92
|
+
log?.warn(`${this.clientId} -- websocket to ${to} failed, trying again in ${this.options.retryIntervalMs * attempt}ms`);
|
|
93
93
|
setTimeout(() => this.createNewConnection(to, attempt + 1), this.options.retryIntervalMs * attempt);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/connection.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,SAAS,MAAM,eAAe,CAAC;AAEtC,qBAAa,mBAAoB,SAAQ,UAAU;IACjD,EAAE,EAAE,SAAS,CAAC;gBAGZ,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC,EACzC,WAAW,EAAE,iBAAiB,EAC9B,EAAE,EAAE,SAAS;
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/connection.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,SAAS,MAAM,eAAe,CAAC;AAEtC,qBAAa,mBAAoB,SAAQ,UAAU;IACjD,EAAE,EAAE,SAAS,CAAC;gBAGZ,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC,EACzC,WAAW,EAAE,iBAAiB,EAC9B,EAAE,EAAE,SAAS;IAUf,IAAI,CAAC,OAAO,EAAE,UAAU;IASlB,KAAK;CAGZ"}
|
|
@@ -5,7 +5,8 @@ export class WebSocketConnection extends Connection {
|
|
|
5
5
|
super(transport, connectedTo);
|
|
6
6
|
this.ws = ws;
|
|
7
7
|
ws.binaryType = 'arraybuffer';
|
|
8
|
-
|
|
8
|
+
// take over the onmessage for this websocket
|
|
9
|
+
this.ws.onmessage = (msg) => transport.onMessage(msg.data);
|
|
9
10
|
}
|
|
10
11
|
send(payload) {
|
|
11
12
|
if (this.ws.readyState === this.ws.OPEN) {
|
|
@@ -12,8 +12,6 @@ export declare class WebSocketServerTransport extends Transport<WebSocketConnect
|
|
|
12
12
|
constructor(wss: Server, clientId: TransportClientId, providedOptions?: Partial<Options>);
|
|
13
13
|
setupConnectionStatusListeners(): void;
|
|
14
14
|
createNewConnection(to: string): Promise<void>;
|
|
15
|
-
destroy(): Promise<void>;
|
|
16
|
-
close(): Promise<void>;
|
|
17
15
|
}
|
|
18
16
|
export {};
|
|
19
17
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,iBAAiB,CAAC;gBAG1B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IASpC,8BAA8B,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../transport/impls/ws/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,MAAM,gBAAgB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAMD,qBAAa,wBAAyB,SAAQ,SAAS,CAAC,mBAAmB,CAAC;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,iBAAiB,CAAC;gBAG1B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IASpC,8BAA8B,IAAI,IAAI;IAiChC,mBAAmB,CAAC,EAAE,EAAE,MAAM;CAKrC"}
|
|
@@ -19,8 +19,11 @@ export class WebSocketServerTransport extends Transport {
|
|
|
19
19
|
this.wss.on('connection', (ws) => {
|
|
20
20
|
let conn = undefined;
|
|
21
21
|
ws.onmessage = (msg) => {
|
|
22
|
+
// when we establish WebSocketConnection, ws.onmessage
|
|
23
|
+
// gets overriden so this only runs on the first valid message
|
|
24
|
+
// the websocket receives
|
|
22
25
|
const parsedMsg = this.parseMsg(msg.data);
|
|
23
|
-
if (parsedMsg) {
|
|
26
|
+
if (parsedMsg && !conn) {
|
|
24
27
|
conn = new WebSocketConnection(this, parsedMsg.from, ws);
|
|
25
28
|
this.onConnect(conn);
|
|
26
29
|
this.handleMsg(parsedMsg);
|
|
@@ -42,12 +45,4 @@ export class WebSocketServerTransport extends Transport {
|
|
|
42
45
|
log?.warn(err);
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
|
-
async destroy() {
|
|
46
|
-
super.destroy();
|
|
47
|
-
this.wss.close();
|
|
48
|
-
}
|
|
49
|
-
async close() {
|
|
50
|
-
super.close();
|
|
51
|
-
this.wss.close();
|
|
52
|
-
}
|
|
53
48
|
}
|
|
@@ -4,21 +4,24 @@ import { createWebSocketServer, createWsTransports, createDummyTransportMessage,
|
|
|
4
4
|
import { msg, waitForMessage } from '../..';
|
|
5
5
|
import { WebSocketServerTransport } from './server';
|
|
6
6
|
import { WebSocketClientTransport } from './client';
|
|
7
|
+
import { testFinishesCleanly } from '../../../__tests__/fixtures/cleanup';
|
|
7
8
|
describe('sending and receiving across websockets works', async () => {
|
|
8
9
|
const server = http.createServer();
|
|
9
10
|
const port = await onServerReady(server);
|
|
10
11
|
const wss = await createWebSocketServer(server);
|
|
11
12
|
afterAll(() => {
|
|
12
|
-
wss.
|
|
13
|
-
socket.close();
|
|
14
|
-
});
|
|
13
|
+
wss.close();
|
|
15
14
|
server.close();
|
|
16
15
|
});
|
|
17
16
|
test('basic send/receive', async () => {
|
|
18
17
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
19
18
|
const msg = createDummyTransportMessage();
|
|
20
19
|
clientTransport.send(msg);
|
|
21
|
-
|
|
20
|
+
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg.id)).resolves.toStrictEqual(msg.payload);
|
|
21
|
+
await testFinishesCleanly({
|
|
22
|
+
clientTransports: [clientTransport],
|
|
23
|
+
serverTransport,
|
|
24
|
+
});
|
|
22
25
|
});
|
|
23
26
|
test('sending respects to/from fields', async () => {
|
|
24
27
|
const makeDummyMessage = (from, to, message) => {
|
|
@@ -50,6 +53,10 @@ describe('sending and receiving across websockets works', async () => {
|
|
|
50
53
|
serverTransport.send(msg1);
|
|
51
54
|
serverTransport.send(msg2);
|
|
52
55
|
await expect(promises).resolves.toStrictEqual([msg1.payload, msg2.payload]);
|
|
56
|
+
await testFinishesCleanly({
|
|
57
|
+
clientTransports: [client1, client2],
|
|
58
|
+
serverTransport,
|
|
59
|
+
});
|
|
53
60
|
});
|
|
54
61
|
});
|
|
55
62
|
describe('retry logic', async () => {
|
|
@@ -57,9 +64,7 @@ describe('retry logic', async () => {
|
|
|
57
64
|
const port = await onServerReady(server);
|
|
58
65
|
const wss = await createWebSocketServer(server);
|
|
59
66
|
afterAll(() => {
|
|
60
|
-
wss.
|
|
61
|
-
socket.close();
|
|
62
|
-
});
|
|
67
|
+
wss.close();
|
|
63
68
|
server.close();
|
|
64
69
|
});
|
|
65
70
|
// TODO: right now, we only test client-side disconnects, we probably
|
|
@@ -73,7 +78,12 @@ describe('retry logic', async () => {
|
|
|
73
78
|
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
74
79
|
clientTransport.connections.forEach((conn) => conn.ws.close());
|
|
75
80
|
clientTransport.send(msg2);
|
|
76
|
-
|
|
81
|
+
// by this point the client should have reconnected
|
|
82
|
+
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload);
|
|
83
|
+
await testFinishesCleanly({
|
|
84
|
+
clientTransports: [clientTransport],
|
|
85
|
+
serverTransport,
|
|
86
|
+
});
|
|
77
87
|
});
|
|
78
88
|
test('ws transport is recreated after unclean disconnect', async () => {
|
|
79
89
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
@@ -83,7 +93,13 @@ describe('retry logic', async () => {
|
|
|
83
93
|
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
84
94
|
clientTransport.connections.forEach((conn) => conn.ws.terminate());
|
|
85
95
|
clientTransport.send(msg2);
|
|
86
|
-
|
|
96
|
+
// by this point the client should have reconnected
|
|
97
|
+
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload);
|
|
98
|
+
// this is not expected to be clean because we destroyed the transport
|
|
99
|
+
await testFinishesCleanly({
|
|
100
|
+
clientTransports: [clientTransport],
|
|
101
|
+
serverTransport,
|
|
102
|
+
});
|
|
87
103
|
});
|
|
88
104
|
test('ws transport is not recreated after destroy', async () => {
|
|
89
105
|
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
@@ -92,6 +108,10 @@ describe('retry logic', async () => {
|
|
|
92
108
|
clientTransport.send(msg1);
|
|
93
109
|
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
94
110
|
clientTransport.destroy();
|
|
95
|
-
|
|
111
|
+
expect(() => clientTransport.send(msg2)).toThrow(new Error('transport is destroyed, cant send'));
|
|
112
|
+
// this is not expected to be clean because we destroyed the transport
|
|
113
|
+
expect(clientTransport.state).toEqual('destroyed');
|
|
114
|
+
await clientTransport.close();
|
|
115
|
+
await serverTransport.close();
|
|
96
116
|
});
|
|
97
117
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { OpaqueTransportMessage } from './message';
|
|
2
|
-
import {
|
|
3
|
-
export { Transport } from './transport';
|
|
2
|
+
import { Transport, Connection } from './transport';
|
|
3
|
+
export { Transport, Connection } from './transport';
|
|
4
4
|
export { TransportMessageSchema, OpaqueTransportMessageSchema, msg, reply, } from './message';
|
|
5
|
-
export type { TransportMessage, MessageId, OpaqueTransportMessage, TransportClientId, } from './message';
|
|
5
|
+
export type { TransportMessage, MessageId, OpaqueTransportMessage, TransportClientId, isStreamOpen, isStreamClose, } from './message';
|
|
6
6
|
/**
|
|
7
7
|
* Waits for a message from the transport.
|
|
8
8
|
* @param {Transport} t - The transport to listen to.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transport/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transport/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,GAAG,EACH,KAAK,GACN,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,EACxB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,OAAO,EACjD,cAAc,CAAC,EAAE,OAAO,oBAczB"}
|
package/dist/transport/index.js
CHANGED
|
@@ -103,6 +103,16 @@ export declare function msg<Payload extends object>(from: string, to: string, se
|
|
|
103
103
|
* @returns A new transport message with appropriate to, from, and payload fields
|
|
104
104
|
*/
|
|
105
105
|
export declare function reply<Payload extends object>(msg: OpaqueTransportMessage, response: Payload): TransportMessage<Payload>;
|
|
106
|
+
/**
|
|
107
|
+
* Create a request to close a stream
|
|
108
|
+
* @param from The ID of the client initiating the close.
|
|
109
|
+
* @param to The ID of the client being closed.
|
|
110
|
+
* @param respondTo The transport message to respond to.
|
|
111
|
+
* @returns The close message
|
|
112
|
+
*/
|
|
113
|
+
export declare function closeStream(from: TransportClientId, to: TransportClientId, service: string, proc: string, stream: string): TransportMessage<{
|
|
114
|
+
type: "CLOSE";
|
|
115
|
+
}>;
|
|
106
116
|
/**
|
|
107
117
|
* Checks if the given control flag (usually found in msg.controlFlag) is an ack message.
|
|
108
118
|
* @param controlFlag - The control flag to check.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../transport/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,OAAO,
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../transport/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,OAAO,EAAU,MAAM,mBAAmB,CAAC;AAG1D;;;;;;GAMG;AACH,0BAAkB,YAAY;IAC5B,MAAM,IAAS;IACf,aAAa,IAAS;IACtB,eAAe,IAAS;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;EAU/B,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;EAI9B,CAAC;AAEF,eAAO,MAAM,2BAA2B;;EAEtC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;EAExC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,CAC1B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzE;IACF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,wBAAgB,GAAG,CAAC,OAAO,SAAS,MAAM,EACxC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,GACf,gBAAgB,CAAC,OAAO,CAAC,CAW3B;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,EAC1C,GAAG,EAAE,sBAAsB,EAC3B,QAAQ,EAAE,OAAO,GAChB,gBAAgB,CAAC,OAAO,CAAC,CAS3B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;;GAOf;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAK1D"}
|
|
@@ -65,12 +65,27 @@ export function msg(from, to, service, proc, stream, payload) {
|
|
|
65
65
|
export function reply(msg, response) {
|
|
66
66
|
return {
|
|
67
67
|
...msg,
|
|
68
|
+
controlFlags: 0,
|
|
68
69
|
id: nanoid(),
|
|
69
70
|
to: msg.from,
|
|
70
71
|
from: msg.to,
|
|
71
72
|
payload: response,
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Create a request to close a stream
|
|
77
|
+
* @param from The ID of the client initiating the close.
|
|
78
|
+
* @param to The ID of the client being closed.
|
|
79
|
+
* @param respondTo The transport message to respond to.
|
|
80
|
+
* @returns The close message
|
|
81
|
+
*/
|
|
82
|
+
export function closeStream(from, to, service, proc, stream) {
|
|
83
|
+
const closeMessage = msg(from, to, service, proc, stream, {
|
|
84
|
+
type: 'CLOSE',
|
|
85
|
+
});
|
|
86
|
+
closeMessage.controlFlags |= 4 /* ControlFlags.StreamClosedBit */;
|
|
87
|
+
return closeMessage;
|
|
88
|
+
}
|
|
74
89
|
/**
|
|
75
90
|
* Checks if the given control flag (usually found in msg.controlFlag) is an ack message.
|
|
76
91
|
* @param controlFlag - The control flag to check.
|
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
import { Codec } from '../codec/types';
|
|
2
2
|
import { MessageId, OpaqueTransportMessage, TransportClientId } from './message';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
4
|
+
* A 1:1 connection between two transports. Once this is created,
|
|
5
|
+
* the {@link Connection} is expected to take over responsibility for
|
|
6
|
+
* reading and writing messages from the underlying connection.
|
|
7
|
+
*
|
|
8
|
+
* 1) Messages received on the {@link Connection} are dispatched back to the {@link Transport}
|
|
9
|
+
* via {@link Transport.onMessage}. The {@link Transport} then notifies any registered message listeners.
|
|
10
|
+
* 2) When {@link Transport.send}(msg) is called, the transport looks up the appropriate
|
|
11
|
+
* connection in the {@link connections} map via `msg.to` and calls {@link send}(bytes)
|
|
12
|
+
* so the connection can send it.
|
|
9
13
|
*/
|
|
10
14
|
export declare abstract class Connection {
|
|
11
15
|
connectedTo: TransportClientId;
|
|
12
16
|
transport: Transport<Connection>;
|
|
13
17
|
constructor(transport: Transport<Connection>, connectedTo: TransportClientId);
|
|
14
|
-
onMessage(msg: Uint8Array): void;
|
|
15
18
|
abstract send(msg: Uint8Array): boolean;
|
|
16
|
-
abstract close():
|
|
19
|
+
abstract close(): void;
|
|
17
20
|
}
|
|
18
21
|
export type TransportStatus = 'open' | 'closed' | 'destroyed';
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* Transports manage the lifecycle (creation/deletion) of connections. Its responsibilities include:
|
|
24
|
+
*
|
|
25
|
+
* 1) Constructing a new {@link Connection} on {@link TransportMessage}s from new clients.
|
|
26
|
+
* After constructing the {@link Connection}, {@link onConnect} is called which adds it to the connection map.
|
|
27
|
+
* 2) Delegating message listening of the connection to the newly created {@link Connection}.
|
|
28
|
+
* From this point on, the {@link Connection} is responsible for *reading* and *writing*
|
|
29
|
+
* messages from the connection.
|
|
30
|
+
* 3) When a connection is closed, the {@link Transport} calls {@link onDisconnect} which closes the
|
|
31
|
+
* connection via {@link Connection.close} and removes it from the {@link connections} map.
|
|
32
|
+
|
|
33
|
+
*
|
|
34
|
+
* ```plaintext
|
|
35
|
+
* ▲
|
|
36
|
+
* incoming │
|
|
37
|
+
* messages │
|
|
38
|
+
* ▼
|
|
39
|
+
* ┌─────────────┐ 1:N ┌────────────┐
|
|
40
|
+
* │ Transport │ ◄─────► │ Connection │
|
|
41
|
+
* └─────────────┘ └────────────┘
|
|
42
|
+
* ▲
|
|
43
|
+
* │
|
|
44
|
+
* ▼
|
|
45
|
+
* ┌───────────┐
|
|
46
|
+
* │ Message │
|
|
47
|
+
* │ Listeners │
|
|
48
|
+
* └───────────┘
|
|
49
|
+
* ```
|
|
24
50
|
* @abstract
|
|
25
51
|
*/
|
|
26
52
|
export declare abstract class Transport<ConnType extends Connection> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../transport/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAEL,SAAS,EACT,sBAAsB,EAGtB,iBAAiB,EAGlB,MAAM,WAAW,CAAC;AAGnB
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../transport/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAEL,SAAS,EACT,sBAAsB,EAGtB,iBAAiB,EAGlB,MAAM,WAAW,CAAC;AAGnB;;;;;;;;;;GAUG;AACH,8BAAsB,UAAU;IAC9B,WAAW,EAAE,iBAAiB,CAAC;IAC/B,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;gBAG/B,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,WAAW,EAAE,iBAAiB;IAMhC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO;IACvC,QAAQ,CAAC,KAAK,IAAI,IAAI;CACvB;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,8BAAsB,SAAS,CAAC,QAAQ,SAAS,UAAU;IACzD;;;OAGG;IACH,KAAK,EAAE,eAAe,CAAC;IAEvB;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;IAEb;;OAEG;IACH,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;OAEG;IACH,eAAe,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,CAAC,CAAC;IAE5D;;;OAGG;IACH,SAAS,EAAE,GAAG,CAAC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAEpD;;OAEG;IACH,UAAU,EAAE,GAAG,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;IAEnD;;OAEG;IACH,WAAW,EAAE,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IAE9C;;;;OAIG;gBACS,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB;IAUrD;;;OAGG;IACH,QAAQ,CAAC,8BAA8B,IAAI,IAAI;IAE/C;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAElE;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,QAAQ;IAyBxB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,QAAQ;IAM3B;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,UAAU;IAIzB;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,sBAAsB,GAAG,IAAI;IAmBlE;;;;OAIG;IACH,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,sBAAsB,GAAG,IAAI;IAgCtD;;;OAGG;IACH,kBAAkB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,GAAG,IAAI;IAIxE;;;OAGG;IACH,qBAAqB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,GAAG,IAAI;IAI3E;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,SAAS;IA4C5C;;;;OAIG;IACG,KAAK;IAUX;;;;OAIG;IACG,OAAO;CASd"}
|
|
@@ -2,11 +2,15 @@ import { Value } from '@sinclair/typebox/value';
|
|
|
2
2
|
import { OpaqueTransportMessageSchema, TransportAckSchema, isAck, reply, } from './message';
|
|
3
3
|
import { log } from '../logging';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @
|
|
5
|
+
* A 1:1 connection between two transports. Once this is created,
|
|
6
|
+
* the {@link Connection} is expected to take over responsibility for
|
|
7
|
+
* reading and writing messages from the underlying connection.
|
|
8
|
+
*
|
|
9
|
+
* 1) Messages received on the {@link Connection} are dispatched back to the {@link Transport}
|
|
10
|
+
* via {@link Transport.onMessage}. The {@link Transport} then notifies any registered message listeners.
|
|
11
|
+
* 2) When {@link Transport.send}(msg) is called, the transport looks up the appropriate
|
|
12
|
+
* connection in the {@link connections} map via `msg.to` and calls {@link send}(bytes)
|
|
13
|
+
* so the connection can send it.
|
|
10
14
|
*/
|
|
11
15
|
export class Connection {
|
|
12
16
|
connectedTo;
|
|
@@ -15,15 +19,35 @@ export class Connection {
|
|
|
15
19
|
this.connectedTo = connectedTo;
|
|
16
20
|
this.transport = transport;
|
|
17
21
|
}
|
|
18
|
-
onMessage(msg) {
|
|
19
|
-
return this.transport.onMessage(msg);
|
|
20
|
-
}
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Transports manage the lifecycle (creation/deletion) of connections. Its responsibilities include:
|
|
25
|
+
*
|
|
26
|
+
* 1) Constructing a new {@link Connection} on {@link TransportMessage}s from new clients.
|
|
27
|
+
* After constructing the {@link Connection}, {@link onConnect} is called which adds it to the connection map.
|
|
28
|
+
* 2) Delegating message listening of the connection to the newly created {@link Connection}.
|
|
29
|
+
* From this point on, the {@link Connection} is responsible for *reading* and *writing*
|
|
30
|
+
* messages from the connection.
|
|
31
|
+
* 3) When a connection is closed, the {@link Transport} calls {@link onDisconnect} which closes the
|
|
32
|
+
* connection via {@link Connection.close} and removes it from the {@link connections} map.
|
|
33
|
+
|
|
34
|
+
*
|
|
35
|
+
* ```plaintext
|
|
36
|
+
* ▲
|
|
37
|
+
* incoming │
|
|
38
|
+
* messages │
|
|
39
|
+
* ▼
|
|
40
|
+
* ┌─────────────┐ 1:N ┌────────────┐
|
|
41
|
+
* │ Transport │ ◄─────► │ Connection │
|
|
42
|
+
* └─────────────┘ └────────────┘
|
|
43
|
+
* ▲
|
|
44
|
+
* │
|
|
45
|
+
* ▼
|
|
46
|
+
* ┌───────────┐
|
|
47
|
+
* │ Message │
|
|
48
|
+
* │ Listeners │
|
|
49
|
+
* └───────────┘
|
|
50
|
+
* ```
|
|
27
51
|
* @abstract
|
|
28
52
|
*/
|
|
29
53
|
export class Transport {
|
|
@@ -91,7 +115,7 @@ export class Transport {
|
|
|
91
115
|
}
|
|
92
116
|
this.send(msg);
|
|
93
117
|
}
|
|
94
|
-
this.sendQueue.
|
|
118
|
+
this.sendQueue.delete(conn.connectedTo);
|
|
95
119
|
}
|
|
96
120
|
/**
|
|
97
121
|
* The downstream implementation needs to call this when a connection is closed.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@replit/river",
|
|
3
3
|
"sideEffects": false,
|
|
4
4
|
"description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/router/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"prepack": "npm run build",
|
|
41
41
|
"release": "npm publish --access public",
|
|
42
42
|
"test:ui": "echo \"remember to go to /__vitest__ in the webview\" && vitest --ui --api.host 0.0.0.0 --api.port 3000",
|
|
43
|
-
"test": "vitest",
|
|
43
|
+
"test": "vitest --test-timeout=500",
|
|
44
44
|
"bench": "vitest bench"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
File without changes
|