@replit/river 0.7.0 → 0.7.2
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 +6 -4
- package/dist/codec/index.d.ts +1 -0
- package/dist/codec/index.d.ts.map +1 -1
- package/dist/codec/index.js +1 -0
- package/package.json +5 -3
- package/dist/__tests__/integration.test.d.ts +0 -50
- package/dist/__tests__/integration.test.js +0 -193
- package/dist/transport/impls/stdio.d.ts +0 -40
- package/dist/transport/impls/stdio.d.ts.map +0 -1
- package/dist/transport/impls/stdio.js +0 -56
- package/dist/transport/impls/stdio.test.d.ts +0 -2
- package/dist/transport/impls/stdio.test.d.ts.map +0 -1
- package/dist/transport/impls/stdio.test.js +0 -20
- package/dist/transport/impls/ws.d.ts +0 -71
- package/dist/transport/impls/ws.d.ts.map +0 -1
- package/dist/transport/impls/ws.js +0 -156
- package/dist/transport/impls/ws.test.d.ts +0 -2
- package/dist/transport/impls/ws.test.d.ts.map +0 -1
- package/dist/transport/impls/ws.test.js +0 -64
- package/dist/transport/types.d.ts +0 -50
- package/dist/transport/types.d.ts.map +0 -1
- package/dist/transport/types.js +0 -95
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@ It's like tRPC but...
|
|
|
10
10
|
|
|
11
11
|
## Developing
|
|
12
12
|
|
|
13
|
+
[](https://replit.com/new/github/replit/river)
|
|
14
|
+
|
|
13
15
|
- `npm i` -- install dependencies
|
|
14
|
-
- `npm check` -- lint
|
|
15
|
-
- `npm format` -- format
|
|
16
|
-
- `npm test` -- run tests
|
|
17
|
-
- `npm publish` -- cut a new release (should bump version in package.json first)
|
|
16
|
+
- `npm run check` -- lint
|
|
17
|
+
- `npm run format` -- format
|
|
18
|
+
- `npm run test` -- run tests
|
|
19
|
+
- `npm run publish` -- cut a new release (should bump version in package.json first)
|
package/dist/codec/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../codec/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../codec/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,YAAY,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/codec/index.js
CHANGED
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.7.
|
|
5
|
+
"version": "0.7.2",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/router/index.js",
|
|
@@ -27,17 +27,19 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/ws": "^8.5.5",
|
|
30
|
+
"@vitest/ui": "^1.0.1",
|
|
30
31
|
"prettier": "^3.0.0",
|
|
31
32
|
"tsup": "^7.2.0",
|
|
32
33
|
"typescript": "^5.2.2",
|
|
33
|
-
"vitest": "^0.
|
|
34
|
+
"vitest": "^1.0.1"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
37
|
"check": "tsc --noEmit && npx prettier . --check",
|
|
37
38
|
"format": "npx prettier . --write",
|
|
38
|
-
"build": "tsc",
|
|
39
|
+
"build": "rm -rf ./dist && tsc",
|
|
39
40
|
"prepack": "npm run build",
|
|
40
41
|
"release": "npm publish --access public",
|
|
42
|
+
"test:ui": "echo \"remember to go to /__vitest__ in the webview\" && vitest --ui --api.host 0.0.0.0 --api.port 3000",
|
|
41
43
|
"test": "vitest",
|
|
42
44
|
"bench": "vitest bench"
|
|
43
45
|
},
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export declare const EchoRequest: import("@sinclair/typebox").TObject<{
|
|
2
|
-
msg: import("@sinclair/typebox").TString;
|
|
3
|
-
ignore: import("@sinclair/typebox").TBoolean;
|
|
4
|
-
}>;
|
|
5
|
-
export declare const EchoResponse: import("@sinclair/typebox").TObject<{
|
|
6
|
-
response: import("@sinclair/typebox").TString;
|
|
7
|
-
}>;
|
|
8
|
-
export declare const TestServiceConstructor: () => {
|
|
9
|
-
name: "test";
|
|
10
|
-
state: {
|
|
11
|
-
count: number;
|
|
12
|
-
};
|
|
13
|
-
procedures: {
|
|
14
|
-
add: {
|
|
15
|
-
input: import("@sinclair/typebox").TObject<{
|
|
16
|
-
n: import("@sinclair/typebox").TNumber;
|
|
17
|
-
}>;
|
|
18
|
-
output: import("@sinclair/typebox").TObject<{
|
|
19
|
-
result: import("@sinclair/typebox").TNumber;
|
|
20
|
-
}>;
|
|
21
|
-
handler: (context: import("../router").ServiceContextWithState<{
|
|
22
|
-
count: number;
|
|
23
|
-
}>, input: import("../transport/message").TransportMessage<{
|
|
24
|
-
n: number;
|
|
25
|
-
}>) => Promise<import("../transport/message").TransportMessage<{
|
|
26
|
-
result: number;
|
|
27
|
-
}>>;
|
|
28
|
-
type: "rpc";
|
|
29
|
-
};
|
|
30
|
-
} & {
|
|
31
|
-
echo: {
|
|
32
|
-
input: import("@sinclair/typebox").TObject<{
|
|
33
|
-
msg: import("@sinclair/typebox").TString;
|
|
34
|
-
ignore: import("@sinclair/typebox").TBoolean;
|
|
35
|
-
}>;
|
|
36
|
-
output: import("@sinclair/typebox").TObject<{
|
|
37
|
-
response: import("@sinclair/typebox").TString;
|
|
38
|
-
}>;
|
|
39
|
-
handler: (context: import("../router").ServiceContextWithState<{
|
|
40
|
-
count: number;
|
|
41
|
-
}>, input: AsyncIterable<import("../transport/message").TransportMessage<{
|
|
42
|
-
msg: string;
|
|
43
|
-
ignore: boolean;
|
|
44
|
-
}>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<{
|
|
45
|
-
response: string;
|
|
46
|
-
}>, void, unknown>) => Promise<void>;
|
|
47
|
-
type: "stream";
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
};
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import { Type } from '@sinclair/typebox';
|
|
3
|
-
import { ServiceBuilder, serializeService } from '../router/builder';
|
|
4
|
-
import { reply } from '../transport/message';
|
|
5
|
-
import { afterAll, describe, expect, test } from 'vitest';
|
|
6
|
-
import { createWebSocketServer, createWsTransports, onServerReady, asClientRpc, asClientStream, } from '../testUtils';
|
|
7
|
-
import { createServer } from '../router/server';
|
|
8
|
-
import { createClient } from '../router/client';
|
|
9
|
-
export const EchoRequest = Type.Object({
|
|
10
|
-
msg: Type.String(),
|
|
11
|
-
ignore: Type.Boolean(),
|
|
12
|
-
});
|
|
13
|
-
export const EchoResponse = Type.Object({ response: Type.String() });
|
|
14
|
-
export const TestServiceConstructor = () => ServiceBuilder.create('test')
|
|
15
|
-
.initialState({
|
|
16
|
-
count: 0,
|
|
17
|
-
})
|
|
18
|
-
.defineProcedure('add', {
|
|
19
|
-
type: 'rpc',
|
|
20
|
-
input: Type.Object({ n: Type.Number() }),
|
|
21
|
-
output: Type.Object({ result: Type.Number() }),
|
|
22
|
-
async handler(ctx, msg) {
|
|
23
|
-
const { n } = msg.payload;
|
|
24
|
-
ctx.state.count += n;
|
|
25
|
-
return reply(msg, { result: ctx.state.count });
|
|
26
|
-
},
|
|
27
|
-
})
|
|
28
|
-
.defineProcedure('echo', {
|
|
29
|
-
type: 'stream',
|
|
30
|
-
input: EchoRequest,
|
|
31
|
-
output: EchoResponse,
|
|
32
|
-
async handler(_ctx, msgStream, returnStream) {
|
|
33
|
-
for await (const msg of msgStream) {
|
|
34
|
-
const req = msg.payload;
|
|
35
|
-
if (!req.ignore) {
|
|
36
|
-
returnStream.push(reply(msg, { response: req.msg }));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
.finalize();
|
|
42
|
-
const OrderingServiceConstructor = () => ServiceBuilder.create('test')
|
|
43
|
-
.initialState({
|
|
44
|
-
msgs: [],
|
|
45
|
-
})
|
|
46
|
-
.defineProcedure('add', {
|
|
47
|
-
type: 'rpc',
|
|
48
|
-
input: Type.Object({ n: Type.Number() }),
|
|
49
|
-
output: Type.Object({ ok: Type.Boolean() }),
|
|
50
|
-
async handler(ctx, msg) {
|
|
51
|
-
const { n } = msg.payload;
|
|
52
|
-
ctx.state.msgs.push(n);
|
|
53
|
-
return reply(msg, { ok: true });
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
.defineProcedure('getAll', {
|
|
57
|
-
type: 'rpc',
|
|
58
|
-
input: Type.Object({}),
|
|
59
|
-
output: Type.Object({ msgs: Type.Array(Type.Number()) }),
|
|
60
|
-
async handler(ctx, msg) {
|
|
61
|
-
return reply(msg, { msgs: ctx.state.msgs });
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
.finalize();
|
|
65
|
-
test('serialize service to jsonschema', () => {
|
|
66
|
-
const service = TestServiceConstructor();
|
|
67
|
-
expect(serializeService(service)).toStrictEqual({
|
|
68
|
-
name: 'test',
|
|
69
|
-
state: { count: 0 },
|
|
70
|
-
procedures: {
|
|
71
|
-
add: {
|
|
72
|
-
input: {
|
|
73
|
-
properties: {
|
|
74
|
-
n: { type: 'number' },
|
|
75
|
-
},
|
|
76
|
-
required: ['n'],
|
|
77
|
-
type: 'object',
|
|
78
|
-
},
|
|
79
|
-
output: {
|
|
80
|
-
properties: {
|
|
81
|
-
result: { type: 'number' },
|
|
82
|
-
},
|
|
83
|
-
required: ['result'],
|
|
84
|
-
type: 'object',
|
|
85
|
-
},
|
|
86
|
-
type: 'rpc',
|
|
87
|
-
},
|
|
88
|
-
echo: {
|
|
89
|
-
input: {
|
|
90
|
-
properties: {
|
|
91
|
-
msg: { type: 'string' },
|
|
92
|
-
ignore: { type: 'boolean' },
|
|
93
|
-
},
|
|
94
|
-
required: ['msg', 'ignore'],
|
|
95
|
-
type: 'object',
|
|
96
|
-
},
|
|
97
|
-
output: {
|
|
98
|
-
properties: {
|
|
99
|
-
response: { type: 'string' },
|
|
100
|
-
},
|
|
101
|
-
required: ['response'],
|
|
102
|
-
type: 'object',
|
|
103
|
-
},
|
|
104
|
-
type: 'stream',
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe('server-side test', () => {
|
|
110
|
-
const service = TestServiceConstructor();
|
|
111
|
-
const initialState = { count: 0 };
|
|
112
|
-
test('rpc basic', async () => {
|
|
113
|
-
const add = asClientRpc(initialState, service.procedures.add);
|
|
114
|
-
await expect(add({ n: 3 })).resolves.toStrictEqual({ result: 3 });
|
|
115
|
-
});
|
|
116
|
-
test('rpc initial state', async () => {
|
|
117
|
-
const add = asClientRpc({ count: 5 }, service.procedures.add);
|
|
118
|
-
await expect(add({ n: 6 })).resolves.toStrictEqual({ result: 11 });
|
|
119
|
-
});
|
|
120
|
-
test('stream basic', async () => {
|
|
121
|
-
const [input, output] = asClientStream(initialState, service.procedures.echo);
|
|
122
|
-
input.push({ msg: 'abc', ignore: false });
|
|
123
|
-
input.push({ msg: 'def', ignore: true });
|
|
124
|
-
input.push({ msg: 'ghi', ignore: false });
|
|
125
|
-
input.end();
|
|
126
|
-
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({
|
|
127
|
-
response: 'abc',
|
|
128
|
-
});
|
|
129
|
-
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({
|
|
130
|
-
response: 'ghi',
|
|
131
|
-
});
|
|
132
|
-
expect(output.readableLength).toBe(0);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('client <-> server integration test', async () => {
|
|
136
|
-
const server = http.createServer();
|
|
137
|
-
const port = await onServerReady(server);
|
|
138
|
-
const webSocketServer = await createWebSocketServer(server);
|
|
139
|
-
afterAll(() => {
|
|
140
|
-
webSocketServer.clients.forEach((socket) => {
|
|
141
|
-
socket.close();
|
|
142
|
-
});
|
|
143
|
-
server.close();
|
|
144
|
-
});
|
|
145
|
-
test('rpc', async () => {
|
|
146
|
-
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer);
|
|
147
|
-
const serviceDefs = { test: TestServiceConstructor() };
|
|
148
|
-
const server = await createServer(serverTransport, serviceDefs);
|
|
149
|
-
const client = createClient(clientTransport);
|
|
150
|
-
await expect(client.test.add({ n: 3 })).resolves.toStrictEqual({
|
|
151
|
-
result: 3,
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
test('stream', async () => {
|
|
155
|
-
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer);
|
|
156
|
-
const serviceDefs = { test: TestServiceConstructor() };
|
|
157
|
-
const server = await createServer(serverTransport, serviceDefs);
|
|
158
|
-
const client = createClient(clientTransport);
|
|
159
|
-
const [input, output, close] = await client.test.echo();
|
|
160
|
-
input.push({ msg: 'abc', ignore: false });
|
|
161
|
-
input.push({ msg: 'def', ignore: true });
|
|
162
|
-
input.push({ msg: 'ghi', ignore: false });
|
|
163
|
-
input.end();
|
|
164
|
-
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({
|
|
165
|
-
response: 'abc',
|
|
166
|
-
});
|
|
167
|
-
await expect(output.next().then((res) => res.value)).resolves.toStrictEqual({
|
|
168
|
-
response: 'ghi',
|
|
169
|
-
});
|
|
170
|
-
close();
|
|
171
|
-
});
|
|
172
|
-
test('message order is preserved in the face of disconnects', async () => {
|
|
173
|
-
const [clientTransport, serverTransport] = createWsTransports(port, webSocketServer);
|
|
174
|
-
const serviceDefs = { test: OrderingServiceConstructor() };
|
|
175
|
-
const server = await createServer(serverTransport, serviceDefs);
|
|
176
|
-
const client = createClient(clientTransport);
|
|
177
|
-
const expected = [];
|
|
178
|
-
for (let i = 0; i < 50; i++) {
|
|
179
|
-
expected.push(i);
|
|
180
|
-
if (i == 10) {
|
|
181
|
-
clientTransport.ws?.close();
|
|
182
|
-
}
|
|
183
|
-
if (i == 42) {
|
|
184
|
-
clientTransport.ws?.terminate();
|
|
185
|
-
}
|
|
186
|
-
await client.test.add({
|
|
187
|
-
n: i,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
const res = await client.test.getAll({});
|
|
191
|
-
return expect(res.msgs).toStrictEqual(expected);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { Codec } from '../../codec';
|
|
3
|
-
import { OpaqueTransportMessage, TransportClientId } from '../message';
|
|
4
|
-
import { Transport } from '../types';
|
|
5
|
-
interface Options {
|
|
6
|
-
codec: Codec;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* A transport implementation that uses standard input and output streams.
|
|
10
|
-
* @extends Transport
|
|
11
|
-
*/
|
|
12
|
-
export declare class StdioTransport extends Transport {
|
|
13
|
-
/**
|
|
14
|
-
* The readable stream to use as input.
|
|
15
|
-
*/
|
|
16
|
-
input: NodeJS.ReadableStream;
|
|
17
|
-
/**
|
|
18
|
-
* The writable stream to use as output.
|
|
19
|
-
*/
|
|
20
|
-
output: NodeJS.WritableStream;
|
|
21
|
-
/**
|
|
22
|
-
* Constructs a new StdioTransport instance.
|
|
23
|
-
* @param clientId - The ID of the client associated with this transport.
|
|
24
|
-
* @param input - The readable stream to use as input. Defaults to process.stdin.
|
|
25
|
-
* @param output - The writable stream to use as output. Defaults to process.stdout.
|
|
26
|
-
*/
|
|
27
|
-
constructor(clientId: TransportClientId, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream, providedOptions?: Partial<Options>);
|
|
28
|
-
/**
|
|
29
|
-
* Sends a message over the transport.
|
|
30
|
-
* @param msg - The message to send.
|
|
31
|
-
* @returns The ID of the sent message.
|
|
32
|
-
*/
|
|
33
|
-
send(msg: OpaqueTransportMessage): string;
|
|
34
|
-
/**
|
|
35
|
-
* Closes the transport.
|
|
36
|
-
*/
|
|
37
|
-
close(): Promise<void>;
|
|
38
|
-
}
|
|
39
|
-
export {};
|
|
40
|
-
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../transport/impls/stdio.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;CACd;AAQD;;;GAGG;AACH,qBAAa,cAAe,SAAQ,SAAS;IAC3C;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC;IAC7B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAE9B;;;;;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;IAcpC;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,MAAM;IAWzC;;OAEG;IACG,KAAK;CACZ"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { NaiveJsonCodec } from '../../codec/json';
|
|
2
|
-
import { Transport } from '../types';
|
|
3
|
-
import readline from 'readline';
|
|
4
|
-
const defaultOptions = {
|
|
5
|
-
codec: NaiveJsonCodec,
|
|
6
|
-
};
|
|
7
|
-
const newlineBuff = new TextEncoder().encode('\n');
|
|
8
|
-
/**
|
|
9
|
-
* A transport implementation that uses standard input and output streams.
|
|
10
|
-
* @extends Transport
|
|
11
|
-
*/
|
|
12
|
-
export class StdioTransport extends Transport {
|
|
13
|
-
/**
|
|
14
|
-
* The readable stream to use as input.
|
|
15
|
-
*/
|
|
16
|
-
input;
|
|
17
|
-
/**
|
|
18
|
-
* The writable stream to use as output.
|
|
19
|
-
*/
|
|
20
|
-
output;
|
|
21
|
-
/**
|
|
22
|
-
* Constructs a new StdioTransport instance.
|
|
23
|
-
* @param clientId - The ID of the client associated with this transport.
|
|
24
|
-
* @param input - The readable stream to use as input. Defaults to process.stdin.
|
|
25
|
-
* @param output - The writable stream to use as output. Defaults to process.stdout.
|
|
26
|
-
*/
|
|
27
|
-
constructor(clientId, input = process.stdin, output = process.stdout, providedOptions) {
|
|
28
|
-
const options = { ...defaultOptions, ...providedOptions };
|
|
29
|
-
super(options.codec, clientId);
|
|
30
|
-
this.input = input;
|
|
31
|
-
this.output = output;
|
|
32
|
-
const rl = readline.createInterface({
|
|
33
|
-
input: this.input,
|
|
34
|
-
});
|
|
35
|
-
const encoder = new TextEncoder();
|
|
36
|
-
rl.on('line', (msg) => this.onMessage(encoder.encode(msg)));
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Sends a message over the transport.
|
|
40
|
-
* @param msg - The message to send.
|
|
41
|
-
* @returns The ID of the sent message.
|
|
42
|
-
*/
|
|
43
|
-
send(msg) {
|
|
44
|
-
const id = msg.id;
|
|
45
|
-
const payload = this.codec.toBuffer(msg);
|
|
46
|
-
const out = new Uint8Array(payload.length + newlineBuff.length);
|
|
47
|
-
out.set(payload, 0);
|
|
48
|
-
out.set(newlineBuff, payload.length);
|
|
49
|
-
this.output.write(out);
|
|
50
|
-
return id;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Closes the transport.
|
|
54
|
-
*/
|
|
55
|
-
async close() { }
|
|
56
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.test.d.ts","sourceRoot":"","sources":["../../../transport/impls/stdio.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'vitest';
|
|
2
|
-
import stream from 'node:stream';
|
|
3
|
-
import { StdioTransport } from './stdio';
|
|
4
|
-
import { waitForMessage } from '..';
|
|
5
|
-
import { payloadToTransportMessage } from '../../testUtils';
|
|
6
|
-
describe('sending and receiving across node streams works', () => {
|
|
7
|
-
test('basic send/receive', async () => {
|
|
8
|
-
const clientToServer = new stream.PassThrough();
|
|
9
|
-
const serverToClient = new stream.PassThrough();
|
|
10
|
-
const serverTransport = new StdioTransport('SERVER', clientToServer, serverToClient);
|
|
11
|
-
const clientTransport = new StdioTransport('client', serverToClient, clientToServer);
|
|
12
|
-
const msg = {
|
|
13
|
-
msg: 'cool',
|
|
14
|
-
test: 123,
|
|
15
|
-
};
|
|
16
|
-
const p = waitForMessage(serverTransport);
|
|
17
|
-
clientTransport.send(payloadToTransportMessage(msg));
|
|
18
|
-
await expect(p).resolves.toStrictEqual(msg);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/// <reference types="ws" />
|
|
2
|
-
import WebSocket from 'isomorphic-ws';
|
|
3
|
-
import { Transport } from '../types';
|
|
4
|
-
import { MessageId, OpaqueTransportMessage, TransportClientId } from '../message';
|
|
5
|
-
import { type Codec } from '../../codec';
|
|
6
|
-
interface Options {
|
|
7
|
-
retryIntervalMs: number;
|
|
8
|
-
codec: Codec;
|
|
9
|
-
binaryType: 'arraybuffer' | 'blob';
|
|
10
|
-
}
|
|
11
|
-
type WebSocketResult = {
|
|
12
|
-
ws: WebSocket;
|
|
13
|
-
} | {
|
|
14
|
-
err: string;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* A transport implementation that uses a WebSocket connection with automatic reconnection.
|
|
18
|
-
* @class
|
|
19
|
-
* @extends Transport
|
|
20
|
-
*/
|
|
21
|
-
export declare class WebSocketTransport extends Transport {
|
|
22
|
-
/**
|
|
23
|
-
* A function that returns a Promise that resolves to a WebSocket instance.
|
|
24
|
-
*/
|
|
25
|
-
wsGetter: () => Promise<WebSocket>;
|
|
26
|
-
ws?: WebSocket;
|
|
27
|
-
options: Options;
|
|
28
|
-
/**
|
|
29
|
-
* A flag indicating whether the transport has been destroyed.
|
|
30
|
-
* A destroyed transport will not attempt to reconnect and cannot be used again.
|
|
31
|
-
*/
|
|
32
|
-
state: 'open' | 'closed' | 'destroyed';
|
|
33
|
-
/**
|
|
34
|
-
* An ongoing reconnect attempt if it exists. When the attempt finishes, it contains a
|
|
35
|
-
* {@link WebSocketResult} object when a connection is established or an error occurs.
|
|
36
|
-
*/
|
|
37
|
-
reconnectPromise?: Promise<WebSocketResult>;
|
|
38
|
-
/**
|
|
39
|
-
* An array of message IDs that are waiting to be sent over the WebSocket connection.
|
|
40
|
-
* This builds up if the WebSocket is down for a period of time.
|
|
41
|
-
*/
|
|
42
|
-
sendQueue: Array<MessageId>;
|
|
43
|
-
/**
|
|
44
|
-
* Creates a new WebSocketTransport instance.
|
|
45
|
-
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
46
|
-
* @param clientId The ID of the client using the transport.
|
|
47
|
-
* @param providedOptions An optional object containing configuration options for the transport.
|
|
48
|
-
*/
|
|
49
|
-
constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, providedOptions?: Partial<Options>);
|
|
50
|
-
/**
|
|
51
|
-
* Begins a new attempt to establish a WebSocket connection.
|
|
52
|
-
*/
|
|
53
|
-
private tryConnect;
|
|
54
|
-
/**
|
|
55
|
-
* Sends a message over the WebSocket connection. If the WebSocket connection is
|
|
56
|
-
* not healthy, it will queue until the connection is successful.
|
|
57
|
-
* @param msg The message to send.
|
|
58
|
-
* @returns The ID of the sent message.
|
|
59
|
-
*/
|
|
60
|
-
send(msg: OpaqueTransportMessage): MessageId;
|
|
61
|
-
/**
|
|
62
|
-
* Closes the WebSocket transport. Any messages sent while the transport is closed will be silently discarded.
|
|
63
|
-
*/
|
|
64
|
-
close(): Promise<void | undefined>;
|
|
65
|
-
/**
|
|
66
|
-
* Destroys the WebSocket transport. Any messages sent while the transport is closed will throw an error.
|
|
67
|
-
*/
|
|
68
|
-
destroy(): Promise<void | undefined>;
|
|
69
|
-
}
|
|
70
|
-
export {};
|
|
71
|
-
//# sourceMappingURL=ws.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../../../transport/impls/ws.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,aAAa,GAAG,MAAM,CAAC;CACpC;AAQD,KAAK,eAAe,GAAG;IAAE,EAAE,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C;;OAEG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;IACnC,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;IAEvC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAE5C;;;OAGG;IACH,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAE5B;;;;;OAKG;gBAED,QAAQ,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,EAClC,QAAQ,EAAE,iBAAiB,EAC3B,eAAe,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAWpC;;OAEG;YACW,UAAU;IAuExB;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,SAAS;IA4B5C;;OAEG;IACG,KAAK;IAMX;;OAEG;IACG,OAAO;CAKd"}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { Transport } from '../types';
|
|
2
|
-
import { NaiveJsonCodec } from '../../codec/json';
|
|
3
|
-
import { log } from '../../logging';
|
|
4
|
-
const defaultOptions = {
|
|
5
|
-
retryIntervalMs: 250,
|
|
6
|
-
codec: NaiveJsonCodec,
|
|
7
|
-
binaryType: 'arraybuffer',
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* A transport implementation that uses a WebSocket connection with automatic reconnection.
|
|
11
|
-
* @class
|
|
12
|
-
* @extends Transport
|
|
13
|
-
*/
|
|
14
|
-
export class WebSocketTransport extends Transport {
|
|
15
|
-
/**
|
|
16
|
-
* A function that returns a Promise that resolves to a WebSocket instance.
|
|
17
|
-
*/
|
|
18
|
-
wsGetter;
|
|
19
|
-
ws;
|
|
20
|
-
options;
|
|
21
|
-
/**
|
|
22
|
-
* A flag indicating whether the transport has been destroyed.
|
|
23
|
-
* A destroyed transport will not attempt to reconnect and cannot be used again.
|
|
24
|
-
*/
|
|
25
|
-
state;
|
|
26
|
-
/**
|
|
27
|
-
* An ongoing reconnect attempt if it exists. When the attempt finishes, it contains a
|
|
28
|
-
* {@link WebSocketResult} object when a connection is established or an error occurs.
|
|
29
|
-
*/
|
|
30
|
-
reconnectPromise;
|
|
31
|
-
/**
|
|
32
|
-
* An array of message IDs that are waiting to be sent over the WebSocket connection.
|
|
33
|
-
* This builds up if the WebSocket is down for a period of time.
|
|
34
|
-
*/
|
|
35
|
-
sendQueue;
|
|
36
|
-
/**
|
|
37
|
-
* Creates a new WebSocketTransport instance.
|
|
38
|
-
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
39
|
-
* @param clientId The ID of the client using the transport.
|
|
40
|
-
* @param providedOptions An optional object containing configuration options for the transport.
|
|
41
|
-
*/
|
|
42
|
-
constructor(wsGetter, clientId, providedOptions) {
|
|
43
|
-
const options = { ...defaultOptions, ...providedOptions };
|
|
44
|
-
super(options.codec, clientId);
|
|
45
|
-
this.state = 'open';
|
|
46
|
-
this.wsGetter = wsGetter;
|
|
47
|
-
this.options = options;
|
|
48
|
-
this.sendQueue = [];
|
|
49
|
-
this.tryConnect();
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Begins a new attempt to establish a WebSocket connection.
|
|
53
|
-
*/
|
|
54
|
-
async tryConnect() {
|
|
55
|
-
if (this.state !== 'open') {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// wait until it's ready or we get an error
|
|
59
|
-
this.reconnectPromise ??= new Promise(async (resolve) => {
|
|
60
|
-
log?.info(`${this.clientId} -- establishing a new websocket`);
|
|
61
|
-
const ws = await this.wsGetter();
|
|
62
|
-
if (ws.readyState === ws.OPEN) {
|
|
63
|
-
return resolve({ ws });
|
|
64
|
-
}
|
|
65
|
-
if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
|
|
66
|
-
return resolve({ err: 'ws is closing or closed' });
|
|
67
|
-
}
|
|
68
|
-
ws.addEventListener('open', function onOpen() {
|
|
69
|
-
ws.removeEventListener('open', onOpen);
|
|
70
|
-
resolve({ ws });
|
|
71
|
-
});
|
|
72
|
-
ws.addEventListener('error', function onError(err) {
|
|
73
|
-
ws.removeEventListener('error', onError);
|
|
74
|
-
resolve({ err: err.message });
|
|
75
|
-
});
|
|
76
|
-
ws.addEventListener('close', function onClose(evt) {
|
|
77
|
-
ws.removeEventListener('close', onClose);
|
|
78
|
-
resolve({ err: evt.reason });
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
const res = await this.reconnectPromise;
|
|
82
|
-
// only send if we resolved a valid websocket
|
|
83
|
-
if ('ws' in res && res.ws.readyState === res.ws.OPEN) {
|
|
84
|
-
log?.info(`${this.clientId} -- websocket ok`);
|
|
85
|
-
this.ws = res.ws;
|
|
86
|
-
this.ws.binaryType = 'arraybuffer';
|
|
87
|
-
this.ws.onmessage = (msg) => this.onMessage(msg.data);
|
|
88
|
-
this.ws.onclose = () => {
|
|
89
|
-
this.reconnectPromise = undefined;
|
|
90
|
-
this.tryConnect().catch();
|
|
91
|
-
};
|
|
92
|
-
// send outstanding
|
|
93
|
-
for (const id of this.sendQueue) {
|
|
94
|
-
const msg = this.sendBuffer.get(id);
|
|
95
|
-
if (!msg) {
|
|
96
|
-
const err = 'tried to resend a message we received an ack for';
|
|
97
|
-
log?.error(err);
|
|
98
|
-
throw new Error(err);
|
|
99
|
-
}
|
|
100
|
-
log?.info(`${this.clientId} -- sending ${JSON.stringify(msg)}`);
|
|
101
|
-
this.ws.send(this.codec.toBuffer(msg));
|
|
102
|
-
}
|
|
103
|
-
this.sendQueue = [];
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// otherwise try and reconnect again
|
|
107
|
-
log?.warn(`${this.clientId} -- websocket failed, trying again in ${this.options.retryIntervalMs}ms`);
|
|
108
|
-
this.reconnectPromise = undefined;
|
|
109
|
-
setTimeout(() => this.tryConnect(), this.options.retryIntervalMs);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Sends a message over the WebSocket connection. If the WebSocket connection is
|
|
113
|
-
* not healthy, it will queue until the connection is successful.
|
|
114
|
-
* @param msg The message to send.
|
|
115
|
-
* @returns The ID of the sent message.
|
|
116
|
-
*/
|
|
117
|
-
send(msg) {
|
|
118
|
-
const id = msg.id;
|
|
119
|
-
if (this.state === 'destroyed') {
|
|
120
|
-
const err = 'ws is destroyed, cant send';
|
|
121
|
-
log?.error(err + `: ${JSON.stringify(msg)}`);
|
|
122
|
-
throw new Error(err);
|
|
123
|
-
}
|
|
124
|
-
else if (this.state === 'closed') {
|
|
125
|
-
log?.info(`ws is closed, discarding msg: ${JSON.stringify(msg)}`);
|
|
126
|
-
return msg.id;
|
|
127
|
-
}
|
|
128
|
-
this.sendBuffer.set(id, msg);
|
|
129
|
-
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
130
|
-
log?.info(`${this.clientId} -- sending ${JSON.stringify(msg)}`);
|
|
131
|
-
this.ws.send(this.codec.toBuffer(msg));
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
log?.info(`${this.clientId} -- transport not ready, queuing ${JSON.stringify(msg)}`);
|
|
135
|
-
this.sendQueue.push(id);
|
|
136
|
-
this.tryConnect().catch();
|
|
137
|
-
}
|
|
138
|
-
return id;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Closes the WebSocket transport. Any messages sent while the transport is closed will be silently discarded.
|
|
142
|
-
*/
|
|
143
|
-
async close() {
|
|
144
|
-
log?.info('closed ws transport');
|
|
145
|
-
this.state = 'closed';
|
|
146
|
-
return this.ws?.close();
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Destroys the WebSocket transport. Any messages sent while the transport is closed will throw an error.
|
|
150
|
-
*/
|
|
151
|
-
async destroy() {
|
|
152
|
-
log?.info('destroyed ws transport');
|
|
153
|
-
this.state = 'destroyed';
|
|
154
|
-
return this.ws?.close();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ws.test.d.ts","sourceRoot":"","sources":["../../../transport/impls/ws.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import { describe, test, expect, afterAll } from 'vitest';
|
|
3
|
-
import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, } from '../../testUtils';
|
|
4
|
-
import { waitForMessage } from '..';
|
|
5
|
-
describe('sending and receiving across websockets works', async () => {
|
|
6
|
-
const server = http.createServer();
|
|
7
|
-
const port = await onServerReady(server);
|
|
8
|
-
const wss = await createWebSocketServer(server);
|
|
9
|
-
afterAll(() => {
|
|
10
|
-
wss.clients.forEach((socket) => {
|
|
11
|
-
socket.close();
|
|
12
|
-
});
|
|
13
|
-
server.close();
|
|
14
|
-
});
|
|
15
|
-
test('basic send/receive', async () => {
|
|
16
|
-
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
17
|
-
const msg = createDummyTransportMessage();
|
|
18
|
-
clientTransport.send(msg);
|
|
19
|
-
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg.id)).resolves.toStrictEqual(msg.payload);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('retry logic', async () => {
|
|
23
|
-
const server = http.createServer();
|
|
24
|
-
const port = await onServerReady(server);
|
|
25
|
-
const wss = await createWebSocketServer(server);
|
|
26
|
-
afterAll(() => {
|
|
27
|
-
wss.clients.forEach((socket) => {
|
|
28
|
-
socket.close();
|
|
29
|
-
});
|
|
30
|
-
server.close();
|
|
31
|
-
});
|
|
32
|
-
// TODO: right now, we only test client-side disconnects, we probably
|
|
33
|
-
// need to also write tests for server-side crashes (but this involves clearing/restoring state)
|
|
34
|
-
// not going to worry about this rn but for future
|
|
35
|
-
test('ws transport is recreated after clean disconnect', async () => {
|
|
36
|
-
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
37
|
-
const msg1 = createDummyTransportMessage();
|
|
38
|
-
const msg2 = createDummyTransportMessage();
|
|
39
|
-
clientTransport.send(msg1);
|
|
40
|
-
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
41
|
-
clientTransport.ws?.close();
|
|
42
|
-
clientTransport.send(msg2);
|
|
43
|
-
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload);
|
|
44
|
-
});
|
|
45
|
-
test('ws transport is recreated after unclean disconnect', async () => {
|
|
46
|
-
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
47
|
-
const msg1 = createDummyTransportMessage();
|
|
48
|
-
const msg2 = createDummyTransportMessage();
|
|
49
|
-
clientTransport.send(msg1);
|
|
50
|
-
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
51
|
-
clientTransport.ws?.terminate();
|
|
52
|
-
clientTransport.send(msg2);
|
|
53
|
-
return expect(waitForMessage(serverTransport, (recv) => recv.id === msg2.id)).resolves.toStrictEqual(msg2.payload);
|
|
54
|
-
});
|
|
55
|
-
test('ws transport is not recreated after destroy', async () => {
|
|
56
|
-
const [clientTransport, serverTransport] = createWsTransports(port, wss);
|
|
57
|
-
const msg1 = createDummyTransportMessage();
|
|
58
|
-
const msg2 = createDummyTransportMessage();
|
|
59
|
-
clientTransport.send(msg1);
|
|
60
|
-
await expect(waitForMessage(serverTransport, (recv) => recv.id === msg1.id)).resolves.toStrictEqual(msg1.payload);
|
|
61
|
-
clientTransport.destroy();
|
|
62
|
-
return expect(() => clientTransport.send(msg2)).toThrow(new Error('ws is destroyed, cant send'));
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Codec } from '../codec/types';
|
|
2
|
-
import { MessageId, OpaqueTransportMessage, TransportClientId } from './message';
|
|
3
|
-
/**
|
|
4
|
-
* Abstract base for a transport layer for communication between nodes in a River network.
|
|
5
|
-
* Any River transport methods need to implement this interface.
|
|
6
|
-
* @abstract
|
|
7
|
-
*/
|
|
8
|
-
export declare abstract class Transport {
|
|
9
|
-
/**
|
|
10
|
-
* The {@link Codec} used to encode and decode messages.
|
|
11
|
-
*/
|
|
12
|
-
codec: Codec;
|
|
13
|
-
/**
|
|
14
|
-
* The client ID of this transport.
|
|
15
|
-
*/
|
|
16
|
-
clientId: TransportClientId;
|
|
17
|
-
/**
|
|
18
|
-
* The set of message handlers registered with this transport.
|
|
19
|
-
*/
|
|
20
|
-
handlers: Set<(msg: OpaqueTransportMessage) => void>;
|
|
21
|
-
/**
|
|
22
|
-
* The buffer of messages that have been sent but not yet acknowledged.
|
|
23
|
-
*/
|
|
24
|
-
sendBuffer: Map<MessageId, OpaqueTransportMessage>;
|
|
25
|
-
/**
|
|
26
|
-
* Creates a new Transport instance.
|
|
27
|
-
* @param codec The codec used to encode and decode messages.
|
|
28
|
-
* @param clientId The client ID of this transport.
|
|
29
|
-
*/
|
|
30
|
-
constructor(codec: Codec, clientId: TransportClientId);
|
|
31
|
-
/**
|
|
32
|
-
* Called when a message is received by this transport.
|
|
33
|
-
* You generally shouldn't need to override this in downstream transport implementations.
|
|
34
|
-
* @param msg The received message.
|
|
35
|
-
*/
|
|
36
|
-
onMessage(msg: Uint8Array): void;
|
|
37
|
-
/**
|
|
38
|
-
* Adds a message listener to this transport.
|
|
39
|
-
* @param handler The message handler to add.
|
|
40
|
-
*/
|
|
41
|
-
addMessageListener(handler: (msg: OpaqueTransportMessage) => void): void;
|
|
42
|
-
/**
|
|
43
|
-
* Removes a message listener from this transport.
|
|
44
|
-
* @param handler The message handler to remove.
|
|
45
|
-
*/
|
|
46
|
-
removeMessageListener(handler: (msg: OpaqueTransportMessage) => void): void;
|
|
47
|
-
abstract send(msg: OpaqueTransportMessage): MessageId;
|
|
48
|
-
abstract close(): Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../transport/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAEL,SAAS,EACT,sBAAsB,EAGtB,iBAAiB,EAGlB,MAAM,WAAW,CAAC;AAGnB;;;;GAIG;AACH,8BAAsB,SAAS;IAC7B;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;IAEb;;OAEG;IACH,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;OAEG;IACH,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,sBAAsB,KAAK,IAAI,CAAC,CAAC;IAGrD;;OAEG;IACH,UAAU,EAAE,GAAG,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;IAEnD;;;;OAIG;gBACS,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB;IAOrD;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,UAAU;IAoDzB;;;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,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,SAAS;IACrD,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAChC"}
|
package/dist/transport/types.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { Value } from '@sinclair/typebox/value';
|
|
2
|
-
import { OpaqueTransportMessageSchema, TransportAckSchema, isAck, reply, } from './message';
|
|
3
|
-
import { log } from '../logging';
|
|
4
|
-
/**
|
|
5
|
-
* Abstract base for a transport layer for communication between nodes in a River network.
|
|
6
|
-
* Any River transport methods need to implement this interface.
|
|
7
|
-
* @abstract
|
|
8
|
-
*/
|
|
9
|
-
export class Transport {
|
|
10
|
-
/**
|
|
11
|
-
* The {@link Codec} used to encode and decode messages.
|
|
12
|
-
*/
|
|
13
|
-
codec;
|
|
14
|
-
/**
|
|
15
|
-
* The client ID of this transport.
|
|
16
|
-
*/
|
|
17
|
-
clientId;
|
|
18
|
-
/**
|
|
19
|
-
* The set of message handlers registered with this transport.
|
|
20
|
-
*/
|
|
21
|
-
handlers;
|
|
22
|
-
// TODO; we can do much better here on retry (maybe resending the sendBuffer on fixed interval)
|
|
23
|
-
/**
|
|
24
|
-
* The buffer of messages that have been sent but not yet acknowledged.
|
|
25
|
-
*/
|
|
26
|
-
sendBuffer;
|
|
27
|
-
/**
|
|
28
|
-
* Creates a new Transport instance.
|
|
29
|
-
* @param codec The codec used to encode and decode messages.
|
|
30
|
-
* @param clientId The client ID of this transport.
|
|
31
|
-
*/
|
|
32
|
-
constructor(codec, clientId) {
|
|
33
|
-
this.handlers = new Set();
|
|
34
|
-
this.sendBuffer = new Map();
|
|
35
|
-
this.codec = codec;
|
|
36
|
-
this.clientId = clientId;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Called when a message is received by this transport.
|
|
40
|
-
* You generally shouldn't need to override this in downstream transport implementations.
|
|
41
|
-
* @param msg The received message.
|
|
42
|
-
*/
|
|
43
|
-
onMessage(msg) {
|
|
44
|
-
const parsedMsg = this.codec.fromBuffer(msg);
|
|
45
|
-
if (parsedMsg === null) {
|
|
46
|
-
log?.warn(`${this.clientId} -- received malformed msg: ${new TextDecoder().decode(msg)}`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
let stringifiedMessage;
|
|
50
|
-
if (log) {
|
|
51
|
-
stringifiedMessage = JSON.stringify(parsedMsg);
|
|
52
|
-
}
|
|
53
|
-
if (Value.Check(TransportAckSchema, parsedMsg) &&
|
|
54
|
-
isAck(parsedMsg.controlFlags)) {
|
|
55
|
-
// process ack
|
|
56
|
-
log?.info(`${this.clientId} -- received ack: ${stringifiedMessage}`);
|
|
57
|
-
if (this.sendBuffer.has(parsedMsg.payload.ack)) {
|
|
58
|
-
this.sendBuffer.delete(parsedMsg.payload.ack);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else if (Value.Check(OpaqueTransportMessageSchema, parsedMsg)) {
|
|
62
|
-
// regular river message
|
|
63
|
-
log?.info(`${this.clientId} -- received msg: ${stringifiedMessage}`);
|
|
64
|
-
// ignore if not for us
|
|
65
|
-
if (parsedMsg.to !== this.clientId && parsedMsg.to !== 'broadcast') {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
// handle actual message
|
|
69
|
-
for (const handler of this.handlers) {
|
|
70
|
-
handler(parsedMsg);
|
|
71
|
-
}
|
|
72
|
-
const ackMsg = reply(parsedMsg, { ack: parsedMsg.id });
|
|
73
|
-
ackMsg.controlFlags = 1 /* ControlFlags.AckBit */;
|
|
74
|
-
ackMsg.from = this.clientId;
|
|
75
|
-
this.send(ackMsg);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
log?.warn(`${this.clientId} -- received invalid transport msg: ${stringifiedMessage}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Adds a message listener to this transport.
|
|
83
|
-
* @param handler The message handler to add.
|
|
84
|
-
*/
|
|
85
|
-
addMessageListener(handler) {
|
|
86
|
-
this.handlers.add(handler);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Removes a message listener from this transport.
|
|
90
|
-
* @param handler The message handler to remove.
|
|
91
|
-
*/
|
|
92
|
-
removeMessageListener(handler) {
|
|
93
|
-
this.handlers.delete(handler);
|
|
94
|
-
}
|
|
95
|
-
}
|