@replit/river 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bandwidth.bench.d.ts +1 -0
- package/dist/__tests__/bandwidth.bench.d.ts.map +1 -0
- package/dist/__tests__/bandwidth.bench.js +12 -5
- package/dist/__tests__/e2e.test.d.ts +2 -0
- package/dist/__tests__/e2e.test.d.ts.map +1 -0
- package/dist/__tests__/e2e.test.js +153 -0
- package/dist/__tests__/fixtures.d.ts +155 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +129 -0
- package/dist/__tests__/handler.test.d.ts +2 -0
- package/dist/__tests__/handler.test.d.ts.map +1 -0
- package/dist/__tests__/handler.test.js +71 -0
- package/dist/__tests__/serialize.test.d.ts +2 -0
- package/dist/__tests__/serialize.test.d.ts.map +1 -0
- package/dist/__tests__/serialize.test.js +135 -0
- package/dist/__tests__/typescript-stress.test.d.ts +736 -98
- package/dist/__tests__/typescript-stress.test.d.ts.map +1 -0
- package/dist/__tests__/typescript-stress.test.js +13 -1
- package/dist/codec/codec.test.d.ts +1 -0
- package/dist/codec/codec.test.d.ts.map +1 -0
- package/dist/codec/index.d.ts +1 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/json.d.ts +5 -0
- package/dist/codec/json.d.ts.map +1 -0
- package/dist/codec/json.js +4 -0
- package/dist/codec/types.d.ts +15 -0
- package/dist/codec/types.d.ts.map +1 -0
- package/dist/logging/index.d.ts +13 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +12 -0
- package/dist/router/builder.d.ts +91 -7
- package/dist/router/builder.d.ts.map +1 -0
- package/dist/router/builder.js +32 -0
- package/dist/router/client.d.ts +28 -3
- package/dist/router/client.d.ts.map +1 -0
- package/dist/router/client.js +37 -6
- package/dist/router/context.d.ts +2 -0
- package/dist/router/context.d.ts.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/result.d.ts +25 -0
- package/dist/router/result.d.ts.map +1 -0
- package/dist/router/result.js +18 -0
- package/dist/router/server.d.ts +13 -0
- package/dist/router/server.d.ts.map +1 -0
- package/dist/router/server.js +85 -56
- package/dist/testUtils.d.ts +69 -2
- package/dist/testUtils.d.ts.map +1 -0
- package/dist/testUtils.js +91 -4
- package/dist/transport/impls/stdio.d.ts +25 -0
- package/dist/transport/impls/stdio.d.ts.map +1 -0
- package/dist/transport/impls/stdio.js +24 -0
- package/dist/transport/impls/stdio.test.d.ts +1 -0
- package/dist/transport/impls/stdio.test.d.ts.map +1 -0
- package/dist/transport/impls/stdio.test.js +2 -8
- package/dist/transport/impls/ws.d.ts +40 -1
- package/dist/transport/impls/ws.d.ts.map +1 -0
- package/dist/transport/impls/ws.js +39 -2
- package/dist/transport/impls/ws.test.d.ts +1 -0
- package/dist/transport/impls/ws.test.d.ts.map +1 -0
- package/dist/transport/impls/ws.test.js +8 -20
- package/dist/transport/index.d.ts +9 -2
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +7 -1
- package/dist/transport/message.d.ts +94 -36
- package/dist/transport/message.d.ts.map +1 -0
- package/dist/transport/message.js +66 -19
- package/dist/transport/message.test.d.ts +1 -0
- package/dist/transport/message.test.d.ts.map +1 -0
- package/dist/transport/message.test.js +39 -6
- package/dist/transport/types.d.ts +38 -2
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +44 -5
- package/package.json +1 -2
package/dist/router/server.js
CHANGED
|
@@ -1,90 +1,119 @@
|
|
|
1
|
-
import { Value } from '@sinclair/typebox/value';
|
|
2
1
|
import { pushable } from 'it-pushable';
|
|
2
|
+
import { isStreamClose, isStreamOpen, reply, } from '../transport/message';
|
|
3
3
|
import { log } from '../logging';
|
|
4
|
+
import { Value } from '@sinclair/typebox/value';
|
|
5
|
+
import { Err, UNCAUGHT_ERROR, } from './result';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a server instance that listens for incoming messages from a transport and routes them to the appropriate service and procedure.
|
|
8
|
+
* The server tracks the state of each service along with open streams and the extended context object.
|
|
9
|
+
* @param transport - The transport to listen to.
|
|
10
|
+
* @param services - An object containing all the services to be registered on the server.
|
|
11
|
+
* @param extendedContext - An optional object containing additional context to be passed to all services.
|
|
12
|
+
* @returns A promise that resolves to a server instance with the registered services.
|
|
13
|
+
*/
|
|
4
14
|
export async function createServer(transport, services, extendedContext) {
|
|
5
15
|
const contextMap = new Map();
|
|
16
|
+
// map of streamId to ProcStream
|
|
6
17
|
const streamMap = new Map();
|
|
7
18
|
function getContext(service) {
|
|
8
19
|
const context = contextMap.get(service);
|
|
9
20
|
if (!context) {
|
|
10
|
-
const err =
|
|
21
|
+
const err = `${transport.clientId} -- no context found for ${service.name}`;
|
|
11
22
|
log?.error(err);
|
|
12
23
|
throw new Error(err);
|
|
13
24
|
}
|
|
14
25
|
return context;
|
|
15
26
|
}
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
// populate the context map
|
|
28
|
+
for (const service of Object.values(services)) {
|
|
18
29
|
contextMap.set(service, { ...extendedContext, state: service.state });
|
|
19
|
-
// create streams for every stream procedure
|
|
20
|
-
for (const [procedureName, proc] of Object.entries(service.procedures)) {
|
|
21
|
-
const procedure = proc;
|
|
22
|
-
if (procedure.type === 'stream') {
|
|
23
|
-
const incoming = pushable({ objectMode: true });
|
|
24
|
-
const outgoing = pushable({ objectMode: true });
|
|
25
|
-
const procStream = {
|
|
26
|
-
incoming,
|
|
27
|
-
outgoing,
|
|
28
|
-
doneCtx: Promise.all([
|
|
29
|
-
// processing the actual procedure
|
|
30
|
-
procedure.handler(getContext(service), incoming, outgoing),
|
|
31
|
-
// sending outgoing messages back to client
|
|
32
|
-
(async () => {
|
|
33
|
-
for await (const response of outgoing) {
|
|
34
|
-
transport.send(response);
|
|
35
|
-
}
|
|
36
|
-
})(),
|
|
37
|
-
]),
|
|
38
|
-
};
|
|
39
|
-
streamMap.set(`${serviceName}:${procedureName}`, procStream);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
30
|
}
|
|
43
31
|
const handler = async (msg) => {
|
|
44
32
|
if (msg.to !== 'SERVER') {
|
|
33
|
+
log?.info(`${transport.clientId} -- got msg with destination that isn't the server, ignoring`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!(msg.serviceName in services)) {
|
|
37
|
+
log?.warn(`${transport.clientId} -- couldn't find service ${msg.serviceName}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const service = services[msg.serviceName];
|
|
41
|
+
const serviceContext = getContext(service);
|
|
42
|
+
if (!(msg.procedureName in service.procedures)) {
|
|
43
|
+
log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${msg.serviceName}.${msg.procedureName}`);
|
|
45
44
|
return;
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// back to the client
|
|
62
|
-
const streams = streamMap.get(`${msg.serviceName}:${msg.procedureName}`);
|
|
63
|
-
if (!streams) {
|
|
64
|
-
// this should never happen but log here if we get here
|
|
65
|
-
return;
|
|
46
|
+
const procedure = service.procedures[msg.procedureName];
|
|
47
|
+
if (!Value.Check(procedure.input, msg.payload)) {
|
|
48
|
+
log?.error(`${transport.clientId} -- procedure ${msg.serviceName}.${msg.procedureName} received invalid payload: ${msg.payload}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const inputMessage = msg;
|
|
52
|
+
if (isStreamOpen(inputMessage.controlFlags)) {
|
|
53
|
+
const incoming = pushable({ objectMode: true });
|
|
54
|
+
const outgoing = pushable({ objectMode: true });
|
|
55
|
+
const openPromises = [
|
|
56
|
+
// sending outgoing messages back to client
|
|
57
|
+
(async () => {
|
|
58
|
+
for await (const response of outgoing) {
|
|
59
|
+
transport.send(response);
|
|
66
60
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
})(),
|
|
62
|
+
];
|
|
63
|
+
function errorHandler(err) {
|
|
64
|
+
const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
|
|
65
|
+
log?.error(`${transport.clientId} -- procedure ${msg.serviceName}.${msg.procedureName}:${msg.streamId} threw an error: ${errorMsg}`);
|
|
66
|
+
outgoing.push(reply(msg, Err({
|
|
67
|
+
code: UNCAUGHT_ERROR,
|
|
68
|
+
message: errorMsg,
|
|
69
|
+
})));
|
|
73
70
|
}
|
|
71
|
+
// pump incoming message stream -> handler -> outgoing message stream
|
|
72
|
+
if (procedure.type === 'stream') {
|
|
73
|
+
openPromises.push(procedure
|
|
74
|
+
.handler(serviceContext, incoming, outgoing)
|
|
75
|
+
.catch(errorHandler));
|
|
76
|
+
}
|
|
77
|
+
else if (procedure.type === 'rpc') {
|
|
78
|
+
openPromises.push((async () => {
|
|
79
|
+
for await (const inputMessage of incoming) {
|
|
80
|
+
try {
|
|
81
|
+
const outputMessage = await procedure.handler(serviceContext, inputMessage);
|
|
82
|
+
outgoing.push(outputMessage);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
errorHandler(err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})());
|
|
89
|
+
}
|
|
90
|
+
streamMap.set(`${msg.serviceName}.${msg.procedureName}:${msg.streamId}`, {
|
|
91
|
+
incoming,
|
|
92
|
+
outgoing,
|
|
93
|
+
openPromises,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const procStream = streamMap.get(`${msg.serviceName}.${msg.procedureName}:${msg.streamId}`);
|
|
97
|
+
if (!procStream) {
|
|
98
|
+
log?.warn(`${transport.clientId} -- couldn't find a matching procedure stream for ${msg.serviceName}.${msg.procedureName}:${msg.streamId}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
procStream.incoming.push(inputMessage);
|
|
102
|
+
if (isStreamClose(inputMessage.controlFlags)) {
|
|
103
|
+
procStream.incoming.end();
|
|
104
|
+
await Promise.all(procStream.openPromises);
|
|
105
|
+
procStream.outgoing.end();
|
|
74
106
|
}
|
|
75
|
-
log?.warn(`${transport.clientId} -- couldn't find a matching procedure for ${msg.serviceName}.${msg.procedureName}`);
|
|
76
107
|
};
|
|
77
108
|
transport.addMessageListener(handler);
|
|
78
109
|
return {
|
|
79
110
|
services,
|
|
80
111
|
async close() {
|
|
81
|
-
// remove listener
|
|
82
112
|
transport.removeMessageListener(handler);
|
|
83
|
-
// end all existing streams
|
|
84
113
|
for (const [_, stream] of streamMap) {
|
|
85
114
|
stream.incoming.end();
|
|
115
|
+
await Promise.all(stream.openPromises);
|
|
86
116
|
stream.outgoing.end();
|
|
87
|
-
await stream.doneCtx;
|
|
88
117
|
}
|
|
89
118
|
},
|
|
90
119
|
};
|
package/dist/testUtils.d.ts
CHANGED
|
@@ -5,10 +5,77 @@ import http from 'http';
|
|
|
5
5
|
import { WebSocketTransport } from './transport/impls/ws';
|
|
6
6
|
import { Static, TObject } from '@sinclair/typebox';
|
|
7
7
|
import { Procedure, ServiceContext } from './router';
|
|
8
|
+
import { OpaqueTransportMessage, TransportMessage } from './transport';
|
|
8
9
|
import { Pushable } from 'it-pushable';
|
|
10
|
+
import { Result, RiverError, RiverUncaughtSchema } from './router/result';
|
|
11
|
+
/**
|
|
12
|
+
* Creates a WebSocket server instance using the provided HTTP server.
|
|
13
|
+
* Only used as helper for testing.
|
|
14
|
+
* @param server - The HTTP server instance to use for the WebSocket server.
|
|
15
|
+
* @returns A Promise that resolves to the created WebSocket server instance.
|
|
16
|
+
*/
|
|
9
17
|
export declare function createWebSocketServer(server: http.Server): Promise<WebSocket.Server<typeof WebSocket, typeof http.IncomingMessage>>;
|
|
18
|
+
/**
|
|
19
|
+
* Starts listening on the given server and returns the automatically allocated port number.
|
|
20
|
+
* This should only be used for testing.
|
|
21
|
+
* @param server - The http server to listen on.
|
|
22
|
+
* @returns A promise that resolves with the allocated port number.
|
|
23
|
+
* @throws An error if a port cannot be allocated.
|
|
24
|
+
*/
|
|
10
25
|
export declare function onServerReady(server: http.Server): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a WebSocket client that connects to a local server at the specified port.
|
|
28
|
+
* This should only be used for testing.
|
|
29
|
+
* @param port - The port number to connect to.
|
|
30
|
+
* @returns A Promise that resolves to a WebSocket instance.
|
|
31
|
+
*/
|
|
11
32
|
export declare function createLocalWebSocketClient(port: number): Promise<WebSocket>;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a pair of WebSocket transports for testing purposes.
|
|
35
|
+
* @param port - The port number to use for the client transport. This should be acquired after starting a server via {@link createWebSocketServer}.
|
|
36
|
+
* @param wss - The WebSocketServer instance to use for the server transport.
|
|
37
|
+
* @returns An array containing the client and server {@link WebSocketTransport} instances.
|
|
38
|
+
*/
|
|
12
39
|
export declare function createWsTransports(port: number, wss: WebSocketServer): [WebSocketTransport, WebSocketTransport];
|
|
13
|
-
|
|
14
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Transforms an RPC procedure definition into a normal function call.
|
|
42
|
+
* This should only be used for testing.
|
|
43
|
+
* @template State - The type of the state object.
|
|
44
|
+
* @template I - The type of the input message payload.
|
|
45
|
+
* @template O - The type of the output message payload.
|
|
46
|
+
* @param {State} state - The state object.
|
|
47
|
+
* @param {Procedure<State, 'rpc', I, O>} proc - The RPC procedure to invoke.
|
|
48
|
+
* @param {Omit<ServiceContext, 'state'>} [extendedContext] - Optional extended context.
|
|
49
|
+
* @returns A function that can be used to invoke the RPC procedure.
|
|
50
|
+
*/
|
|
51
|
+
export declare function asClientRpc<State extends object | unknown, I extends TObject, O extends TObject, E extends RiverError>(state: State, proc: Procedure<State, 'rpc', I, O, E>, extendedContext?: Omit<ServiceContext, 'state'>): (msg: Static<I>) => Promise<Result<Static<O>, Static<E> | Static<typeof RiverUncaughtSchema>>>;
|
|
52
|
+
/**
|
|
53
|
+
* Transforms a stream procedure definition into a pair of input and output streams.
|
|
54
|
+
* Input messages can be pushed into the input stream.
|
|
55
|
+
* This should only be used for testing.
|
|
56
|
+
* @template State - The type of the state object.
|
|
57
|
+
* @template I - The type of the input object.
|
|
58
|
+
* @template O - The type of the output object.
|
|
59
|
+
* @param {State} state - The state object.
|
|
60
|
+
* @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
|
|
61
|
+
* @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
|
|
62
|
+
* @returns {[Pushable<Static<I>>, Pushable<Static<O>>]} - Pair of input and output streams.
|
|
63
|
+
*/
|
|
64
|
+
export declare function asClientStream<State extends object | unknown, I extends TObject, O extends TObject, E extends RiverError>(state: State, proc: Procedure<State, 'stream', I, O, E>, extendedContext?: Omit<ServiceContext, 'state'>): [
|
|
65
|
+
Pushable<Static<I>>,
|
|
66
|
+
Pushable<Result<Static<O>, Static<E> | Static<typeof RiverUncaughtSchema>>>
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Converts a payload object to a transport message with reasonable defaults.
|
|
70
|
+
* This should only be used for testing.
|
|
71
|
+
* @param payload - The payload object to be converted.
|
|
72
|
+
* @param streamId - The optional stream ID.
|
|
73
|
+
* @returns The transport message.
|
|
74
|
+
*/
|
|
75
|
+
export declare function payloadToTransportMessage<Payload extends object>(payload: Payload, streamId?: string): TransportMessage<Payload>;
|
|
76
|
+
/**
|
|
77
|
+
* Creates a dummy opaque transport message for testing purposes.
|
|
78
|
+
* @returns The created opaque transport message.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createDummyTransportMessage(): OpaqueTransportMessage;
|
|
81
|
+
//# sourceMappingURL=testUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testUtils.d.ts","sourceRoot":"","sources":["../testUtils.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAGjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AACjD,OAAO,EAEL,MAAM,EACN,UAAU,EACV,mBAAmB,EAEpB,MAAM,iBAAiB,CAAC;AAEzB;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,4EAE9D;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWxE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,sBAE5D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,eAAe,GACnB,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAc1C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,KAAK,SAAS,MAAM,GAAG,OAAO,EAC9B,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,UAAU,EAEpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACtC,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,SAGxC,OAAO,CAAC,CAAC,KACb,QACD,OAAO,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,0BAA0B,CAAC,CAAC,CAClE,CAYF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,KAAK,SAAS,MAAM,GAAG,OAAO,EAC9B,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,UAAU,EAEpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C;IACD,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC;CAC5E,CAuDA;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,SAAS,MAAM,EAC9D,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,MAAM,6BAUlB;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,sBAAsB,CAKpE"}
|
package/dist/testUtils.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import WebSocket from 'isomorphic-ws';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
3
|
import { WebSocketTransport } from './transport/impls/ws';
|
|
4
|
-
import {
|
|
4
|
+
import { msg, reply, } from './transport';
|
|
5
5
|
import { pushable } from 'it-pushable';
|
|
6
|
+
import { Err, UNCAUGHT_ERROR, } from './router/result';
|
|
7
|
+
/**
|
|
8
|
+
* Creates a WebSocket server instance using the provided HTTP server.
|
|
9
|
+
* Only used as helper for testing.
|
|
10
|
+
* @param server - The HTTP server instance to use for the WebSocket server.
|
|
11
|
+
* @returns A Promise that resolves to the created WebSocket server instance.
|
|
12
|
+
*/
|
|
6
13
|
export async function createWebSocketServer(server) {
|
|
7
14
|
return new WebSocketServer({ server });
|
|
8
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Starts listening on the given server and returns the automatically allocated port number.
|
|
18
|
+
* This should only be used for testing.
|
|
19
|
+
* @param server - The http server to listen on.
|
|
20
|
+
* @returns A promise that resolves with the allocated port number.
|
|
21
|
+
* @throws An error if a port cannot be allocated.
|
|
22
|
+
*/
|
|
9
23
|
export async function onServerReady(server) {
|
|
10
24
|
return new Promise((resolve, reject) => {
|
|
11
25
|
server.listen(() => {
|
|
@@ -19,9 +33,21 @@ export async function onServerReady(server) {
|
|
|
19
33
|
});
|
|
20
34
|
});
|
|
21
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a WebSocket client that connects to a local server at the specified port.
|
|
38
|
+
* This should only be used for testing.
|
|
39
|
+
* @param port - The port number to connect to.
|
|
40
|
+
* @returns A Promise that resolves to a WebSocket instance.
|
|
41
|
+
*/
|
|
22
42
|
export async function createLocalWebSocketClient(port) {
|
|
23
43
|
return new WebSocket(`ws://localhost:${port}`);
|
|
24
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a pair of WebSocket transports for testing purposes.
|
|
47
|
+
* @param port - The port number to use for the client transport. This should be acquired after starting a server via {@link createWebSocketServer}.
|
|
48
|
+
* @param wss - The WebSocketServer instance to use for the server transport.
|
|
49
|
+
* @returns An array containing the client and server {@link WebSocketTransport} instances.
|
|
50
|
+
*/
|
|
25
51
|
export function createWsTransports(port, wss) {
|
|
26
52
|
return [
|
|
27
53
|
new WebSocketTransport(async () => {
|
|
@@ -37,14 +63,46 @@ export function createWsTransports(port, wss) {
|
|
|
37
63
|
}, 'SERVER'),
|
|
38
64
|
];
|
|
39
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Transforms an RPC procedure definition into a normal function call.
|
|
68
|
+
* This should only be used for testing.
|
|
69
|
+
* @template State - The type of the state object.
|
|
70
|
+
* @template I - The type of the input message payload.
|
|
71
|
+
* @template O - The type of the output message payload.
|
|
72
|
+
* @param {State} state - The state object.
|
|
73
|
+
* @param {Procedure<State, 'rpc', I, O>} proc - The RPC procedure to invoke.
|
|
74
|
+
* @param {Omit<ServiceContext, 'state'>} [extendedContext] - Optional extended context.
|
|
75
|
+
* @returns A function that can be used to invoke the RPC procedure.
|
|
76
|
+
*/
|
|
40
77
|
export function asClientRpc(state, proc, extendedContext) {
|
|
41
78
|
return (msg) => proc
|
|
42
79
|
.handler({ ...extendedContext, state }, payloadToTransportMessage(msg))
|
|
43
|
-
.then((res) => res.payload)
|
|
80
|
+
.then((res) => res.payload)
|
|
81
|
+
.catch((err) => {
|
|
82
|
+
const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
|
|
83
|
+
return Err({
|
|
84
|
+
code: UNCAUGHT_ERROR,
|
|
85
|
+
message: errorMsg,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
44
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Transforms a stream procedure definition into a pair of input and output streams.
|
|
91
|
+
* Input messages can be pushed into the input stream.
|
|
92
|
+
* This should only be used for testing.
|
|
93
|
+
* @template State - The type of the state object.
|
|
94
|
+
* @template I - The type of the input object.
|
|
95
|
+
* @template O - The type of the output object.
|
|
96
|
+
* @param {State} state - The state object.
|
|
97
|
+
* @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
|
|
98
|
+
* @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
|
|
99
|
+
* @returns {[Pushable<Static<I>>, Pushable<Static<O>>]} - Pair of input and output streams.
|
|
100
|
+
*/
|
|
45
101
|
export function asClientStream(state, proc, extendedContext) {
|
|
46
102
|
const rawInput = pushable({ objectMode: true });
|
|
47
|
-
const rawOutput = pushable({
|
|
103
|
+
const rawOutput = pushable({
|
|
104
|
+
objectMode: true,
|
|
105
|
+
});
|
|
48
106
|
const transportInput = pushable({
|
|
49
107
|
objectMode: true,
|
|
50
108
|
});
|
|
@@ -66,8 +124,37 @@ export function asClientStream(state, proc, extendedContext) {
|
|
|
66
124
|
})();
|
|
67
125
|
// handle
|
|
68
126
|
(async () => {
|
|
69
|
-
|
|
127
|
+
try {
|
|
128
|
+
await proc.handler({ ...extendedContext, state }, transportInput, transportOutput);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
|
|
132
|
+
transportOutput.push(reply(payloadToTransportMessage({}), Err({
|
|
133
|
+
code: UNCAUGHT_ERROR,
|
|
134
|
+
message: errorMsg,
|
|
135
|
+
})));
|
|
136
|
+
}
|
|
70
137
|
transportOutput.end();
|
|
71
138
|
})();
|
|
72
139
|
return [rawInput, rawOutput];
|
|
73
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Converts a payload object to a transport message with reasonable defaults.
|
|
143
|
+
* This should only be used for testing.
|
|
144
|
+
* @param payload - The payload object to be converted.
|
|
145
|
+
* @param streamId - The optional stream ID.
|
|
146
|
+
* @returns The transport message.
|
|
147
|
+
*/
|
|
148
|
+
export function payloadToTransportMessage(payload, streamId) {
|
|
149
|
+
return msg('client', 'SERVER', 'service', 'procedure', streamId ?? 'stream', payload);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Creates a dummy opaque transport message for testing purposes.
|
|
153
|
+
* @returns The created opaque transport message.
|
|
154
|
+
*/
|
|
155
|
+
export function createDummyTransportMessage() {
|
|
156
|
+
return payloadToTransportMessage({
|
|
157
|
+
msg: 'cool',
|
|
158
|
+
test: Math.random(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { OpaqueTransportMessage, TransportClientId } from '../message';
|
|
3
3
|
import { Transport } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* A transport implementation that uses standard input and output streams.
|
|
6
|
+
* @extends Transport
|
|
7
|
+
*/
|
|
4
8
|
export declare class StdioTransport extends Transport {
|
|
9
|
+
/**
|
|
10
|
+
* The readable stream to use as input.
|
|
11
|
+
*/
|
|
5
12
|
input: NodeJS.ReadableStream;
|
|
13
|
+
/**
|
|
14
|
+
* The writable stream to use as output.
|
|
15
|
+
*/
|
|
6
16
|
output: NodeJS.WritableStream;
|
|
17
|
+
/**
|
|
18
|
+
* Constructs a new StdioTransport instance.
|
|
19
|
+
* @param clientId - The ID of the client associated with this transport.
|
|
20
|
+
* @param input - The readable stream to use as input. Defaults to process.stdin.
|
|
21
|
+
* @param output - The writable stream to use as output. Defaults to process.stdout.
|
|
22
|
+
*/
|
|
7
23
|
constructor(clientId: TransportClientId, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream);
|
|
24
|
+
/**
|
|
25
|
+
* Sends a message over the transport.
|
|
26
|
+
* @param msg - The message to send.
|
|
27
|
+
* @returns The ID of the sent message.
|
|
28
|
+
*/
|
|
8
29
|
send(msg: OpaqueTransportMessage): string;
|
|
30
|
+
/**
|
|
31
|
+
* Closes the transport.
|
|
32
|
+
*/
|
|
9
33
|
close(): Promise<void>;
|
|
10
34
|
}
|
|
35
|
+
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../../transport/impls/stdio.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC;;;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;IAYhD;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,MAAM;IAMzC;;OAEG;IACG,KAAK;CACZ"}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { NaiveJsonCodec } from '../../codec/json';
|
|
2
2
|
import { Transport } from '../types';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
+
/**
|
|
5
|
+
* A transport implementation that uses standard input and output streams.
|
|
6
|
+
* @extends Transport
|
|
7
|
+
*/
|
|
4
8
|
export class StdioTransport extends Transport {
|
|
9
|
+
/**
|
|
10
|
+
* The readable stream to use as input.
|
|
11
|
+
*/
|
|
5
12
|
input;
|
|
13
|
+
/**
|
|
14
|
+
* The writable stream to use as output.
|
|
15
|
+
*/
|
|
6
16
|
output;
|
|
17
|
+
/**
|
|
18
|
+
* Constructs a new StdioTransport instance.
|
|
19
|
+
* @param clientId - The ID of the client associated with this transport.
|
|
20
|
+
* @param input - The readable stream to use as input. Defaults to process.stdin.
|
|
21
|
+
* @param output - The writable stream to use as output. Defaults to process.stdout.
|
|
22
|
+
*/
|
|
7
23
|
constructor(clientId, input = process.stdin, output = process.stdout) {
|
|
8
24
|
super(NaiveJsonCodec, clientId);
|
|
9
25
|
this.input = input;
|
|
@@ -13,10 +29,18 @@ export class StdioTransport extends Transport {
|
|
|
13
29
|
});
|
|
14
30
|
rl.on('line', (msg) => this.onMessage(msg));
|
|
15
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Sends a message over the transport.
|
|
34
|
+
* @param msg - The message to send.
|
|
35
|
+
* @returns The ID of the sent message.
|
|
36
|
+
*/
|
|
16
37
|
send(msg) {
|
|
17
38
|
const id = msg.id;
|
|
18
39
|
this.output.write(this.codec.toStringBuf(msg) + '\n');
|
|
19
40
|
return id;
|
|
20
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Closes the transport.
|
|
44
|
+
*/
|
|
21
45
|
async close() { }
|
|
22
46
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.test.d.ts","sourceRoot":"","sources":["../../../transport/impls/stdio.test.ts"],"names":[],"mappings":""}
|
|
@@ -2,6 +2,7 @@ import { describe, test, expect } from 'vitest';
|
|
|
2
2
|
import stream from 'node:stream';
|
|
3
3
|
import { StdioTransport } from './stdio';
|
|
4
4
|
import { waitForMessage } from '..';
|
|
5
|
+
import { payloadToTransportMessage } from '../../testUtils';
|
|
5
6
|
describe('sending and receiving across node streams works', () => {
|
|
6
7
|
test('basic send/receive', async () => {
|
|
7
8
|
const clientToServer = new stream.PassThrough();
|
|
@@ -13,14 +14,7 @@ describe('sending and receiving across node streams works', () => {
|
|
|
13
14
|
test: 123,
|
|
14
15
|
};
|
|
15
16
|
const p = waitForMessage(serverTransport);
|
|
16
|
-
clientTransport.send(
|
|
17
|
-
id: '1',
|
|
18
|
-
from: 'client',
|
|
19
|
-
to: 'SERVER',
|
|
20
|
-
serviceName: 'test',
|
|
21
|
-
procedureName: 'test',
|
|
22
|
-
payload: msg,
|
|
23
|
-
});
|
|
17
|
+
clientTransport.send(payloadToTransportMessage(msg));
|
|
24
18
|
await expect(p).resolves.toStrictEqual(msg);
|
|
25
19
|
});
|
|
26
20
|
});
|
|
@@ -10,16 +10,55 @@ type WebSocketResult = {
|
|
|
10
10
|
} | {
|
|
11
11
|
err: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* A transport implementation that uses a WebSocket connection with automatic reconnection.
|
|
15
|
+
* @class
|
|
16
|
+
* @extends Transport
|
|
17
|
+
*/
|
|
13
18
|
export declare class WebSocketTransport extends Transport {
|
|
19
|
+
/**
|
|
20
|
+
* A function that returns a Promise that resolves to a WebSocket instance.
|
|
21
|
+
*/
|
|
14
22
|
wsGetter: () => Promise<WebSocket>;
|
|
15
23
|
ws?: WebSocket;
|
|
24
|
+
options: Options;
|
|
25
|
+
/**
|
|
26
|
+
* A flag indicating whether the transport has been destroyed.
|
|
27
|
+
* A destroyed transport will not attempt to reconnect and cannot be used again.
|
|
28
|
+
*/
|
|
16
29
|
destroyed: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* An ongoing reconnect attempt if it exists. When the attempt finishes, it contains a
|
|
32
|
+
* {@link WebSocketResult} object when a connection is established or an error occurs.
|
|
33
|
+
*/
|
|
17
34
|
reconnectPromise?: Promise<WebSocketResult>;
|
|
18
|
-
|
|
35
|
+
/**
|
|
36
|
+
* An array of message IDs that are waiting to be sent over the WebSocket connection.
|
|
37
|
+
* This builds up if the WebSocket is down for a period of time.
|
|
38
|
+
*/
|
|
19
39
|
sendQueue: Array<MessageId>;
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new WebSocketTransport instance.
|
|
42
|
+
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
43
|
+
* @param clientId The ID of the client using the transport.
|
|
44
|
+
* @param options An optional object containing configuration options for the transport.
|
|
45
|
+
*/
|
|
20
46
|
constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, options?: Partial<Options>);
|
|
47
|
+
/**
|
|
48
|
+
* Begins a new attempt to establish a WebSocket connection.
|
|
49
|
+
*/
|
|
21
50
|
private tryConnect;
|
|
51
|
+
/**
|
|
52
|
+
* Sends a message over the WebSocket connection. If the WebSocket connection is
|
|
53
|
+
* not healthy, it will queue until the connection is successful.
|
|
54
|
+
* @param msg The message to send.
|
|
55
|
+
* @returns The ID of the sent message.
|
|
56
|
+
*/
|
|
22
57
|
send(msg: OpaqueTransportMessage): MessageId;
|
|
58
|
+
/**
|
|
59
|
+
* Destroys the WebSocket transport and marks it as unusable.
|
|
60
|
+
*/
|
|
23
61
|
close(): Promise<void | undefined>;
|
|
24
62
|
}
|
|
25
63
|
export {};
|
|
64
|
+
//# sourceMappingURL=ws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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;AAGpB,UAAU,OAAO;IACf,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD,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,SAAS,EAAE,OAAO,CAAC;IAEnB;;;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,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC;IAU5B;;OAEG;YACW,UAAU;IAkExB;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,sBAAsB,GAAG,SAAS;IAyB5C;;OAEG;IACG,KAAK;CAKZ"}
|
|
@@ -4,13 +4,39 @@ import { log } from '../../logging';
|
|
|
4
4
|
const defaultOptions = {
|
|
5
5
|
retryIntervalMs: 250,
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* A transport implementation that uses a WebSocket connection with automatic reconnection.
|
|
9
|
+
* @class
|
|
10
|
+
* @extends Transport
|
|
11
|
+
*/
|
|
7
12
|
export class WebSocketTransport extends Transport {
|
|
13
|
+
/**
|
|
14
|
+
* A function that returns a Promise that resolves to a WebSocket instance.
|
|
15
|
+
*/
|
|
8
16
|
wsGetter;
|
|
9
17
|
ws;
|
|
18
|
+
options;
|
|
19
|
+
/**
|
|
20
|
+
* A flag indicating whether the transport has been destroyed.
|
|
21
|
+
* A destroyed transport will not attempt to reconnect and cannot be used again.
|
|
22
|
+
*/
|
|
10
23
|
destroyed;
|
|
24
|
+
/**
|
|
25
|
+
* An ongoing reconnect attempt if it exists. When the attempt finishes, it contains a
|
|
26
|
+
* {@link WebSocketResult} object when a connection is established or an error occurs.
|
|
27
|
+
*/
|
|
11
28
|
reconnectPromise;
|
|
12
|
-
|
|
29
|
+
/**
|
|
30
|
+
* An array of message IDs that are waiting to be sent over the WebSocket connection.
|
|
31
|
+
* This builds up if the WebSocket is down for a period of time.
|
|
32
|
+
*/
|
|
13
33
|
sendQueue;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new WebSocketTransport instance.
|
|
36
|
+
* @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
|
|
37
|
+
* @param clientId The ID of the client using the transport.
|
|
38
|
+
* @param options An optional object containing configuration options for the transport.
|
|
39
|
+
*/
|
|
14
40
|
constructor(wsGetter, clientId, options) {
|
|
15
41
|
super(NaiveJsonCodec, clientId);
|
|
16
42
|
this.destroyed = false;
|
|
@@ -19,7 +45,9 @@ export class WebSocketTransport extends Transport {
|
|
|
19
45
|
this.sendQueue = [];
|
|
20
46
|
this.tryConnect();
|
|
21
47
|
}
|
|
22
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Begins a new attempt to establish a WebSocket connection.
|
|
50
|
+
*/
|
|
23
51
|
async tryConnect() {
|
|
24
52
|
// wait until it's ready or we get an error
|
|
25
53
|
this.reconnectPromise ??= new Promise(async (resolve) => {
|
|
@@ -73,6 +101,12 @@ export class WebSocketTransport extends Transport {
|
|
|
73
101
|
this.reconnectPromise = undefined;
|
|
74
102
|
setTimeout(() => this.tryConnect(), this.options.retryIntervalMs);
|
|
75
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Sends a message over the WebSocket connection. If the WebSocket connection is
|
|
106
|
+
* not healthy, it will queue until the connection is successful.
|
|
107
|
+
* @param msg The message to send.
|
|
108
|
+
* @returns The ID of the sent message.
|
|
109
|
+
*/
|
|
76
110
|
send(msg) {
|
|
77
111
|
const id = msg.id;
|
|
78
112
|
if (this.destroyed) {
|
|
@@ -92,6 +126,9 @@ export class WebSocketTransport extends Transport {
|
|
|
92
126
|
}
|
|
93
127
|
return id;
|
|
94
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Destroys the WebSocket transport and marks it as unusable.
|
|
131
|
+
*/
|
|
95
132
|
async close() {
|
|
96
133
|
log?.info('manually closed ws');
|
|
97
134
|
this.destroyed = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.test.d.ts","sourceRoot":"","sources":["../../../transport/impls/ws.test.ts"],"names":[],"mappings":""}
|