@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.
Files changed (74) hide show
  1. package/dist/__tests__/bandwidth.bench.d.ts +1 -0
  2. package/dist/__tests__/bandwidth.bench.d.ts.map +1 -0
  3. package/dist/__tests__/bandwidth.bench.js +12 -5
  4. package/dist/__tests__/e2e.test.d.ts +2 -0
  5. package/dist/__tests__/e2e.test.d.ts.map +1 -0
  6. package/dist/__tests__/e2e.test.js +153 -0
  7. package/dist/__tests__/fixtures.d.ts +155 -0
  8. package/dist/__tests__/fixtures.d.ts.map +1 -0
  9. package/dist/__tests__/fixtures.js +129 -0
  10. package/dist/__tests__/handler.test.d.ts +2 -0
  11. package/dist/__tests__/handler.test.d.ts.map +1 -0
  12. package/dist/__tests__/handler.test.js +71 -0
  13. package/dist/__tests__/serialize.test.d.ts +2 -0
  14. package/dist/__tests__/serialize.test.d.ts.map +1 -0
  15. package/dist/__tests__/serialize.test.js +135 -0
  16. package/dist/__tests__/typescript-stress.test.d.ts +736 -98
  17. package/dist/__tests__/typescript-stress.test.d.ts.map +1 -0
  18. package/dist/__tests__/typescript-stress.test.js +13 -1
  19. package/dist/codec/codec.test.d.ts +1 -0
  20. package/dist/codec/codec.test.d.ts.map +1 -0
  21. package/dist/codec/index.d.ts +1 -0
  22. package/dist/codec/index.d.ts.map +1 -0
  23. package/dist/codec/json.d.ts +5 -0
  24. package/dist/codec/json.d.ts.map +1 -0
  25. package/dist/codec/json.js +4 -0
  26. package/dist/codec/types.d.ts +15 -0
  27. package/dist/codec/types.d.ts.map +1 -0
  28. package/dist/logging/index.d.ts +13 -0
  29. package/dist/logging/index.d.ts.map +1 -0
  30. package/dist/logging/index.js +12 -0
  31. package/dist/router/builder.d.ts +91 -7
  32. package/dist/router/builder.d.ts.map +1 -0
  33. package/dist/router/builder.js +32 -0
  34. package/dist/router/client.d.ts +28 -3
  35. package/dist/router/client.d.ts.map +1 -0
  36. package/dist/router/client.js +37 -6
  37. package/dist/router/context.d.ts +2 -0
  38. package/dist/router/context.d.ts.map +1 -0
  39. package/dist/router/index.d.ts +1 -0
  40. package/dist/router/index.d.ts.map +1 -0
  41. package/dist/router/result.d.ts +25 -0
  42. package/dist/router/result.d.ts.map +1 -0
  43. package/dist/router/result.js +18 -0
  44. package/dist/router/server.d.ts +13 -0
  45. package/dist/router/server.d.ts.map +1 -0
  46. package/dist/router/server.js +85 -56
  47. package/dist/testUtils.d.ts +69 -2
  48. package/dist/testUtils.d.ts.map +1 -0
  49. package/dist/testUtils.js +91 -4
  50. package/dist/transport/impls/stdio.d.ts +25 -0
  51. package/dist/transport/impls/stdio.d.ts.map +1 -0
  52. package/dist/transport/impls/stdio.js +24 -0
  53. package/dist/transport/impls/stdio.test.d.ts +1 -0
  54. package/dist/transport/impls/stdio.test.d.ts.map +1 -0
  55. package/dist/transport/impls/stdio.test.js +2 -8
  56. package/dist/transport/impls/ws.d.ts +40 -1
  57. package/dist/transport/impls/ws.d.ts.map +1 -0
  58. package/dist/transport/impls/ws.js +39 -2
  59. package/dist/transport/impls/ws.test.d.ts +1 -0
  60. package/dist/transport/impls/ws.test.d.ts.map +1 -0
  61. package/dist/transport/impls/ws.test.js +8 -20
  62. package/dist/transport/index.d.ts +9 -2
  63. package/dist/transport/index.d.ts.map +1 -0
  64. package/dist/transport/index.js +7 -1
  65. package/dist/transport/message.d.ts +94 -36
  66. package/dist/transport/message.d.ts.map +1 -0
  67. package/dist/transport/message.js +66 -19
  68. package/dist/transport/message.test.d.ts +1 -0
  69. package/dist/transport/message.test.d.ts.map +1 -0
  70. package/dist/transport/message.test.js +39 -6
  71. package/dist/transport/types.d.ts +38 -2
  72. package/dist/transport/types.d.ts.map +1 -0
  73. package/dist/transport/types.js +44 -5
  74. package/package.json +1 -2
@@ -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 = `No context found for ${service.name}`;
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
- for (const [serviceName, service] of Object.entries(services)) {
17
- // populate the context map
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
- if (msg.serviceName in services) {
48
- const service = services[msg.serviceName];
49
- if (msg.procedureName in service.procedures) {
50
- const procedure = service.procedures[msg.procedureName];
51
- const inputMessage = msg;
52
- if (procedure.type === 'rpc' &&
53
- Value.Check(procedure.input, inputMessage.payload)) {
54
- const response = await procedure.handler(getContext(service), inputMessage);
55
- transport.send(response);
56
- return;
57
- }
58
- else if (procedure.type === 'stream' &&
59
- Value.Check(procedure.input, inputMessage.payload)) {
60
- // async stream, push to associated stream. code above handles sending responses
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
- streams.incoming.push(inputMessage);
68
- return;
69
- }
70
- else {
71
- log?.error(`${transport.clientId} -- procedure ${msg.serviceName}.${msg.procedureName} received invalid payload: ${inputMessage.payload}`);
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
  };
@@ -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
- export declare function asClientRpc<State extends object | unknown, I extends TObject, O extends TObject>(state: State, proc: Procedure<State, 'rpc', I, O>, extendedContext?: Omit<ServiceContext, 'state'>): (msg: Static<I>) => Promise<Static<O>>;
14
- export declare function asClientStream<State extends object | unknown, I extends TObject, O extends TObject>(state: State, proc: Procedure<State, 'stream', I, O>, extendedContext?: Omit<ServiceContext, 'state'>): [Pushable<Static<I>>, Pushable<Static<O>>];
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 { payloadToTransportMessage } from './transport';
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({ objectMode: true });
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
- await proc.handler({ ...extendedContext, state }, transportInput, transportOutput);
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
  }
@@ -1 +1,2 @@
1
1
  export {};
2
+ //# sourceMappingURL=stdio.test.d.ts.map
@@ -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
- options: Options;
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
- options;
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
- // postcondition: ws is concretely a WebSocket
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;
@@ -1 +1,2 @@
1
1
  export {};
2
+ //# sourceMappingURL=ws.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.test.d.ts","sourceRoot":"","sources":["../../../transport/impls/ws.test.ts"],"names":[],"mappings":""}