@replit/river 0.10.4 → 0.10.6

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 (40) hide show
  1. package/README.md +121 -6
  2. package/dist/{chunk-UU2Z7LDR.js → chunk-5735JCTZ.js} +2 -5
  3. package/dist/{chunk-PC65ZFWJ.js → chunk-KRYKORQT.js} +1 -1
  4. package/dist/{chunk-RGMHF6PF.js → chunk-LRQGTUTI.js} +6 -8
  5. package/dist/{chunk-AJQU4AZG.js → chunk-MF3Z3IDF.js} +1 -3
  6. package/dist/{chunk-FWPZDOFL.js → chunk-SCULZ4KS.js} +18 -0
  7. package/dist/chunk-XWRKNZSC.js +80 -0
  8. package/dist/connection-2529fc14.d.ts +10 -0
  9. package/dist/connection-316d6e3a.d.ts +10 -0
  10. package/dist/router/index.cjs +18 -0
  11. package/dist/router/index.js +1 -1
  12. package/dist/transport/impls/stdio/stdio.cjs +78 -38
  13. package/dist/transport/impls/stdio/stdio.d.cts +4 -11
  14. package/dist/transport/impls/stdio/stdio.d.ts +4 -11
  15. package/dist/transport/impls/stdio/stdio.js +21 -36
  16. package/dist/transport/impls/unixsocket/client.cjs +506 -0
  17. package/dist/transport/impls/unixsocket/client.d.cts +16 -0
  18. package/dist/transport/impls/unixsocket/client.d.ts +16 -0
  19. package/dist/transport/impls/unixsocket/client.js +67 -0
  20. package/dist/transport/impls/unixsocket/server.cjs +510 -0
  21. package/dist/transport/impls/unixsocket/server.d.cts +18 -0
  22. package/dist/transport/impls/unixsocket/server.d.ts +18 -0
  23. package/dist/transport/impls/unixsocket/server.js +73 -0
  24. package/dist/transport/impls/ws/client.cjs +1 -6
  25. package/dist/transport/impls/ws/client.d.cts +0 -1
  26. package/dist/transport/impls/ws/client.d.ts +0 -1
  27. package/dist/transport/impls/ws/client.js +3 -3
  28. package/dist/transport/impls/ws/server.cjs +5 -9
  29. package/dist/transport/impls/ws/server.d.cts +0 -2
  30. package/dist/transport/impls/ws/server.d.ts +0 -2
  31. package/dist/transport/impls/ws/server.js +3 -3
  32. package/dist/transport/index.cjs +1 -3
  33. package/dist/transport/index.d.cts +6 -7
  34. package/dist/transport/index.d.ts +6 -7
  35. package/dist/transport/index.js +1 -1
  36. package/dist/util/testHelpers.cjs +24 -15
  37. package/dist/util/testHelpers.d.cts +6 -3
  38. package/dist/util/testHelpers.d.ts +6 -3
  39. package/dist/util/testHelpers.js +19 -8
  40. package/package.json +23 -14
package/README.md CHANGED
@@ -1,15 +1,130 @@
1
1
  # river - Streaming Remote Procedure Calls
2
2
 
3
- It's like tRPC but...
3
+ It's like tRPC/gRPC but with
4
4
 
5
- - with JSON Schema Support
6
- - with full-duplex streaming
7
- - with support for service multiplexing
8
- - with Result types and error handling
9
- - over WebSockets
5
+ - JSON Schema Support + run-time schema validation
6
+ - full-duplex streaming
7
+ - service multiplexing
8
+ - result types and error handling
9
+ - snappy DX (no code-generation)
10
+ - over any transport (WebSockets, stdio, Unix Domain Socket out of the box)
11
+
12
+ ## Installation
10
13
 
11
14
  To use River, you must be on least Typescript 5 with `"moduleResolution": "bundler"`.
12
15
 
16
+ ```bash
17
+ npm i @replit/river @sinclair/typebox
18
+
19
+ # if you plan on using WebSocket for transport, also install
20
+ npm i ws isomorphic-ws
21
+ ```
22
+
23
+ ## Writing Services
24
+
25
+ ### Concepts
26
+
27
+ - Router: a collection of services, namespaced by service name.
28
+ - Service: a collection of procedures with shared state.
29
+ - Procedure: a single procedure. A procedure declares its type, an input message type, an output message type, optionally an error type, and the associated handler. Valid types are:
30
+ - `rpc` whose handler has a signature of `Input -> Result<Output, Error>`.
31
+ - `upload` whose handler has a signature of `AsyncIterableIterator<Input> -> Result<Output, Error>`.
32
+ - `subscription` whose handler has a signature of `Input -> Pushable<Result<Output, Error>>`.
33
+ - `stream` whose handler has a signature of `AsyncIterableIterator<Input> -> Pushable<Result<Output, Error>>`.
34
+ - Transport: manages the lifecycle (creation/deletion) of connections and multiplexing read/writes from clients. Both the client and the server must be passed in a subclass of `Transport` to work.
35
+ - Codec: encodes messages between clients/servers before the transport sends it across the wire.
36
+
37
+ ### A basic router
38
+
39
+ First, we create a service using the `ServiceBuilder`
40
+
41
+ ```ts
42
+ import { ServiceBuilder, Ok, buildServiceDefs } from '@replit/river';
43
+ import { Type } from '@sinclair/typebox';
44
+
45
+ export const ExampleServiceConstructor = () =>
46
+ ServiceBuilder.create('example')
47
+ .initialState({
48
+ count: 0,
49
+ })
50
+ .defineProcedure('add', {
51
+ type: 'rpc',
52
+ input: Type.Object({ n: Type.Number() }),
53
+ output: Type.Object({ result: Type.Number() }),
54
+ errors: Type.Never(),
55
+ async handler(ctx, { n }) {
56
+ ctx.state.count += n;
57
+ return Ok({ result: ctx.state.count });
58
+ },
59
+ })
60
+ .finalize();
61
+
62
+ // expore a listing of all the services that we have
63
+ export const serviceDefs = buildServiceDefs([ExampleServiceConstructor()]);
64
+ ```
65
+
66
+ Then, we create the server
67
+
68
+ ```ts
69
+ import http from 'http';
70
+ import { WebSocketServer } from 'ws';
71
+ import { WebSocketServerTransport } from '@replit/river/transport/ws/server';
72
+ import { createServer } from '@replit/river';
73
+
74
+ // start websocket server on port 3000
75
+ const httpServer = http.createServer();
76
+ const port = 3000;
77
+ const wss = new WebSocketServer({ server: httpServer });
78
+ const transport = new WebSocketServerTransport(wss, 'SERVER');
79
+
80
+ export const server = createServer(transport, serviceDefs);
81
+ export type ServiceSurface = typeof server;
82
+
83
+ httpServer.listen(port);
84
+ ```
85
+
86
+ In another file for the client (to create a separate entrypoint),
87
+
88
+ ```ts
89
+ import WebSocket from 'isomorphic-ws';
90
+ import { WebSocketClientTransport } from '@replit/river/transport/ws/client';
91
+ import { createClient } from '@replit/river';
92
+
93
+ const websocketUrl = `ws://localhost:3000`;
94
+ const transport = new WebSocketClientTransport(
95
+ async () => new WebSocket(websocketUrl),
96
+ 'my-client-id',
97
+ 'SERVER',
98
+ );
99
+
100
+ const client = createClient<ServiceSurface>(transport, 'SERVER');
101
+
102
+ // we get full type safety on `client`
103
+ // client.<service name>.<procedure name>.<procedure type>()
104
+ // e.g.
105
+ const result = await client.example.add.rpc({ n: 3 });
106
+ if (result.ok) {
107
+ const msg = result.payload;
108
+ console.log(msg.result); // 0 + 3 = 3
109
+ }
110
+ ```
111
+
112
+ To add logging,
113
+
114
+ ```ts
115
+ import { bindLogger, setLevel } from '@replit/river/logging';
116
+
117
+ bindLogger(console.log);
118
+ setLevel('info');
119
+ ```
120
+
121
+ ### Further examples
122
+
123
+ We've also provided an end-to-end testing environment using Next.js, and a simple backend connected
124
+ with the WebSocket transport that you can [play with on Replit](https://replit.com/@jzhao-replit/riverbed).
125
+
126
+ You can find more service examples in the [E2E test fixtures](https://github.com/replit/river/blob/main/__tests__/fixtures/services.ts)
127
+
13
128
  ## Developing
14
129
 
15
130
  [![Run on Repl.it](https://replit.com/badge/github/replit/river)](https://replit.com/new/github/replit/river)
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  WebSocketConnection
3
- } from "./chunk-PC65ZFWJ.js";
3
+ } from "./chunk-KRYKORQT.js";
4
4
  import {
5
5
  NaiveJsonCodec
6
6
  } from "./chunk-R6H2BIMC.js";
7
7
  import {
8
8
  Transport
9
- } from "./chunk-AJQU4AZG.js";
9
+ } from "./chunk-MF3Z3IDF.js";
10
10
  import {
11
11
  log
12
12
  } from "./chunk-SLUSVGQH.js";
@@ -39,9 +39,6 @@ var WebSocketClientTransport = class extends Transport {
39
39
  this.serverId = serverId;
40
40
  this.options = options;
41
41
  this.reconnectPromises = /* @__PURE__ */ new Map();
42
- this.setupConnectionStatusListeners();
43
- }
44
- setupConnectionStatusListeners() {
45
42
  this.createNewConnection(this.serverId);
46
43
  }
47
44
  async createNewConnection(to, attempt = 0) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Connection
3
- } from "./chunk-AJQU4AZG.js";
3
+ } from "./chunk-MF3Z3IDF.js";
4
4
 
5
5
  // transport/impls/ws/connection.ts
6
6
  var WebSocketConnection = class extends Connection {
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  WebSocketConnection
3
- } from "./chunk-PC65ZFWJ.js";
3
+ } from "./chunk-KRYKORQT.js";
4
4
  import {
5
5
  NaiveJsonCodec
6
6
  } from "./chunk-R6H2BIMC.js";
7
7
  import {
8
8
  Transport
9
- } from "./chunk-AJQU4AZG.js";
9
+ } from "./chunk-MF3Z3IDF.js";
10
10
  import {
11
11
  log
12
12
  } from "./chunk-SLUSVGQH.js";
@@ -17,13 +17,14 @@ var defaultOptions = {
17
17
  };
18
18
  var WebSocketServerTransport = class extends Transport {
19
19
  wss;
20
- clientId;
21
20
  constructor(wss, clientId, providedOptions) {
22
21
  const options = { ...defaultOptions, ...providedOptions };
23
22
  super(options.codec, clientId);
24
23
  this.wss = wss;
25
- this.clientId = clientId;
26
- this.setupConnectionStatusListeners();
24
+ wss.on("listening", () => {
25
+ log?.info(`${this.clientId} -- server is listening`);
26
+ });
27
+ this.wss.on("connection", this.connectionHandler);
27
28
  }
28
29
  connectionHandler = (ws) => {
29
30
  let conn = void 0;
@@ -46,9 +47,6 @@ var WebSocketServerTransport = class extends Transport {
46
47
  );
47
48
  };
48
49
  };
49
- setupConnectionStatusListeners() {
50
- this.wss.on("connection", this.connectionHandler);
51
- }
52
50
  async createNewConnection(to) {
53
51
  const err = `${this.clientId} -- failed to send msg to ${to}, client probably dropped`;
54
52
  log?.warn(err);
@@ -81,6 +81,7 @@ var Transport = class {
81
81
  eventDispatcher;
82
82
  /**
83
83
  * Creates a new Transport instance.
84
+ * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
84
85
  * @param codec The codec used to encode and decode messages.
85
86
  * @param clientId The client ID of this transport.
86
87
  */
@@ -183,9 +184,6 @@ var Transport = class {
183
184
  }
184
185
  } else {
185
186
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
186
- if (msg.to !== this.clientId) {
187
- return;
188
- }
189
187
  this.eventDispatcher.dispatchEvent("message", msg);
190
188
  if (!isAck(msg.controlFlags)) {
191
189
  const ackMsg = reply(msg, { ack: msg.id });
@@ -373,6 +373,9 @@ function _pushable(getNext, options) {
373
373
  },
374
374
  get readableLength() {
375
375
  return _pushable2.readableLength;
376
+ },
377
+ onEmpty: (opts) => {
378
+ return _pushable2.onEmpty(opts);
376
379
  }
377
380
  };
378
381
  return pushable2;
@@ -512,6 +515,12 @@ function handleRpc(transport, serverId, input, serviceName, procName) {
512
515
  transport.removeEventListener("connectionStatus", onConnectionStatus);
513
516
  }
514
517
  function onMessage(msg2) {
518
+ if (msg2.streamId !== streamId) {
519
+ return;
520
+ }
521
+ if (msg2.to !== transport.clientId) {
522
+ return;
523
+ }
515
524
  if (msg2.streamId === streamId) {
516
525
  cleanup();
517
526
  resolve(msg2.payload);
@@ -557,6 +566,9 @@ function handleStream(transport, serverId, init, serviceName, procName) {
557
566
  if (msg2.streamId !== streamId) {
558
567
  return;
559
568
  }
569
+ if (msg2.to !== transport.clientId) {
570
+ return;
571
+ }
560
572
  if (isStreamClose(msg2.controlFlags)) {
561
573
  cleanup();
562
574
  } else {
@@ -603,6 +615,9 @@ function handleSubscribe(transport, serverId, input, serviceName, procName) {
603
615
  if (msg2.streamId !== streamId) {
604
616
  return;
605
617
  }
618
+ if (msg2.to !== transport.clientId) {
619
+ return;
620
+ }
606
621
  if (isStreamClose(msg2.controlFlags)) {
607
622
  cleanup();
608
623
  } else {
@@ -677,6 +692,9 @@ function handleUpload(transport, serverId, input, serviceName, procName) {
677
692
  transport.removeEventListener("connectionStatus", onConnectionStatus);
678
693
  }
679
694
  function onMessage(msg2) {
695
+ if (msg2.to !== transport.clientId) {
696
+ return;
697
+ }
680
698
  if (msg2.streamId === streamId) {
681
699
  cleanup();
682
700
  resolve(msg2.payload);
@@ -0,0 +1,80 @@
1
+ import {
2
+ Connection
3
+ } from "./chunk-MF3Z3IDF.js";
4
+
5
+ // transport/transforms/delim.ts
6
+ import { Transform } from "node:stream";
7
+ var DelimiterParser = class extends Transform {
8
+ delimiter;
9
+ buffer;
10
+ maxBufferSizeBytes;
11
+ constructor({ delimiter, maxBufferSizeBytes, ...options }) {
12
+ super(options);
13
+ this.maxBufferSizeBytes = maxBufferSizeBytes;
14
+ this.delimiter = Buffer.from(delimiter);
15
+ this.buffer = Buffer.alloc(0);
16
+ }
17
+ // tldr; backpressure will be automatically applied for transform streams
18
+ // but it relies on both the input/output streams connected on either end having
19
+ // implemented backpressure properly
20
+ // see: https://nodejs.org/en/guides/backpressuring-in-streams#lifecycle-of-pipe
21
+ _transform(chunk, _encoding, cb) {
22
+ let data = Buffer.concat([this.buffer, chunk]);
23
+ let position;
24
+ while ((position = data.indexOf(this.delimiter)) !== -1) {
25
+ this.push(data.subarray(0, position));
26
+ data = data.subarray(position + this.delimiter.length);
27
+ }
28
+ if (data.byteLength > this.maxBufferSizeBytes) {
29
+ const err = new Error(
30
+ `buffer overflow: ${data.byteLength}B > ${this.maxBufferSizeBytes}B`
31
+ );
32
+ this.emit("error", err);
33
+ return cb(err);
34
+ }
35
+ this.buffer = data;
36
+ cb();
37
+ }
38
+ _flush(cb) {
39
+ if (this.buffer.length) {
40
+ this.push(this.buffer);
41
+ }
42
+ this.buffer = Buffer.alloc(0);
43
+ cb();
44
+ }
45
+ _destroy(error, callback) {
46
+ this.buffer = Buffer.alloc(0);
47
+ super._destroy(error, callback);
48
+ }
49
+ };
50
+ var defaultDelimiter = Buffer.from("\n");
51
+ function createDelimitedStream(options) {
52
+ return new DelimiterParser({
53
+ delimiter: options?.delimiter ?? defaultDelimiter,
54
+ maxBufferSizeBytes: options?.maxBufferSizeBytes ?? 16 * 1024 * 1024
55
+ // 16MB
56
+ });
57
+ }
58
+
59
+ // transport/impls/stdio/connection.ts
60
+ var StreamConnection = class extends Connection {
61
+ output;
62
+ constructor(transport, connectedTo, output) {
63
+ super(transport, connectedTo);
64
+ this.output = output;
65
+ }
66
+ send(payload) {
67
+ if (!this.output.writable) {
68
+ return false;
69
+ }
70
+ return this.output.write(Buffer.concat([payload, defaultDelimiter]));
71
+ }
72
+ async close() {
73
+ this.output.end();
74
+ }
75
+ };
76
+
77
+ export {
78
+ createDelimitedStream,
79
+ StreamConnection
80
+ };
@@ -0,0 +1,10 @@
1
+ import { Connection, Transport, TransportClientId } from './transport/index.cjs';
2
+
3
+ declare class StreamConnection extends Connection {
4
+ output: NodeJS.WritableStream;
5
+ constructor(transport: Transport<StreamConnection>, connectedTo: TransportClientId, output: NodeJS.WritableStream);
6
+ send(payload: Uint8Array): boolean;
7
+ close(): Promise<void>;
8
+ }
9
+
10
+ export { StreamConnection as S };
@@ -0,0 +1,10 @@
1
+ import { Connection, Transport, TransportClientId } from './transport/index.js';
2
+
3
+ declare class StreamConnection extends Connection {
4
+ output: NodeJS.WritableStream;
5
+ constructor(transport: Transport<StreamConnection>, connectedTo: TransportClientId, output: NodeJS.WritableStream);
6
+ send(payload: Uint8Array): boolean;
7
+ close(): Promise<void>;
8
+ }
9
+
10
+ export { StreamConnection as S };
@@ -395,6 +395,9 @@ function _pushable(getNext, options) {
395
395
  },
396
396
  get readableLength() {
397
397
  return _pushable2.readableLength;
398
+ },
399
+ onEmpty: (opts) => {
400
+ return _pushable2.onEmpty(opts);
398
401
  }
399
402
  };
400
403
  return pushable2;
@@ -592,6 +595,12 @@ function handleRpc(transport, serverId, input, serviceName, procName) {
592
595
  transport.removeEventListener("connectionStatus", onConnectionStatus);
593
596
  }
594
597
  function onMessage(msg2) {
598
+ if (msg2.streamId !== streamId) {
599
+ return;
600
+ }
601
+ if (msg2.to !== transport.clientId) {
602
+ return;
603
+ }
595
604
  if (msg2.streamId === streamId) {
596
605
  cleanup();
597
606
  resolve(msg2.payload);
@@ -637,6 +646,9 @@ function handleStream(transport, serverId, init, serviceName, procName) {
637
646
  if (msg2.streamId !== streamId) {
638
647
  return;
639
648
  }
649
+ if (msg2.to !== transport.clientId) {
650
+ return;
651
+ }
640
652
  if (isStreamClose(msg2.controlFlags)) {
641
653
  cleanup();
642
654
  } else {
@@ -683,6 +695,9 @@ function handleSubscribe(transport, serverId, input, serviceName, procName) {
683
695
  if (msg2.streamId !== streamId) {
684
696
  return;
685
697
  }
698
+ if (msg2.to !== transport.clientId) {
699
+ return;
700
+ }
686
701
  if (isStreamClose(msg2.controlFlags)) {
687
702
  cleanup();
688
703
  } else {
@@ -757,6 +772,9 @@ function handleUpload(transport, serverId, input, serviceName, procName) {
757
772
  transport.removeEventListener("connectionStatus", onConnectionStatus);
758
773
  }
759
774
  function onMessage(msg2) {
775
+ if (msg2.to !== transport.clientId) {
776
+ return;
777
+ }
760
778
  if (msg2.streamId === streamId) {
761
779
  cleanup();
762
780
  resolve(msg2.payload);
@@ -8,7 +8,7 @@ import {
8
8
  createClient,
9
9
  createServer,
10
10
  serializeService
11
- } from "../chunk-FWPZDOFL.js";
11
+ } from "../chunk-SCULZ4KS.js";
12
12
  import "../chunk-ZE4MX7DF.js";
13
13
  import "../chunk-SLUSVGQH.js";
14
14
  export {
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,20 +15,11 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // transport/impls/stdio/stdio.ts
31
21
  var stdio_exports = {};
32
22
  __export(stdio_exports, {
33
- StdioConnection: () => StdioConnection,
34
23
  StdioTransport: () => StdioTransport
35
24
  });
36
25
  module.exports = __toCommonJS(stdio_exports);
@@ -84,6 +73,60 @@ var NaiveJsonCodec = {
84
73
  // logging/index.ts
85
74
  var log;
86
75
 
76
+ // transport/transforms/delim.ts
77
+ var import_node_stream = require("stream");
78
+ var DelimiterParser = class extends import_node_stream.Transform {
79
+ delimiter;
80
+ buffer;
81
+ maxBufferSizeBytes;
82
+ constructor({ delimiter, maxBufferSizeBytes, ...options }) {
83
+ super(options);
84
+ this.maxBufferSizeBytes = maxBufferSizeBytes;
85
+ this.delimiter = Buffer.from(delimiter);
86
+ this.buffer = Buffer.alloc(0);
87
+ }
88
+ // tldr; backpressure will be automatically applied for transform streams
89
+ // but it relies on both the input/output streams connected on either end having
90
+ // implemented backpressure properly
91
+ // see: https://nodejs.org/en/guides/backpressuring-in-streams#lifecycle-of-pipe
92
+ _transform(chunk, _encoding, cb) {
93
+ let data = Buffer.concat([this.buffer, chunk]);
94
+ let position;
95
+ while ((position = data.indexOf(this.delimiter)) !== -1) {
96
+ this.push(data.subarray(0, position));
97
+ data = data.subarray(position + this.delimiter.length);
98
+ }
99
+ if (data.byteLength > this.maxBufferSizeBytes) {
100
+ const err = new Error(
101
+ `buffer overflow: ${data.byteLength}B > ${this.maxBufferSizeBytes}B`
102
+ );
103
+ this.emit("error", err);
104
+ return cb(err);
105
+ }
106
+ this.buffer = data;
107
+ cb();
108
+ }
109
+ _flush(cb) {
110
+ if (this.buffer.length) {
111
+ this.push(this.buffer);
112
+ }
113
+ this.buffer = Buffer.alloc(0);
114
+ cb();
115
+ }
116
+ _destroy(error, callback) {
117
+ this.buffer = Buffer.alloc(0);
118
+ super._destroy(error, callback);
119
+ }
120
+ };
121
+ var defaultDelimiter = Buffer.from("\n");
122
+ function createDelimitedStream(options) {
123
+ return new DelimiterParser({
124
+ delimiter: options?.delimiter ?? defaultDelimiter,
125
+ maxBufferSizeBytes: options?.maxBufferSizeBytes ?? 16 * 1024 * 1024
126
+ // 16MB
127
+ });
128
+ }
129
+
87
130
  // transport/transport.ts
88
131
  var import_value = require("@sinclair/typebox/value");
89
132
 
@@ -195,6 +238,7 @@ var Transport = class {
195
238
  eventDispatcher;
196
239
  /**
197
240
  * Creates a new Transport instance.
241
+ * This should also set up {@link onConnect}, and {@link onDisconnect} listeners.
198
242
  * @param codec The codec used to encode and decode messages.
199
243
  * @param clientId The client ID of this transport.
200
244
  */
@@ -297,9 +341,6 @@ var Transport = class {
297
341
  }
298
342
  } else {
299
343
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
300
- if (msg.to !== this.clientId) {
301
- return;
302
- }
303
344
  this.eventDispatcher.dispatchEvent("message", msg);
304
345
  if (!isAck(msg.controlFlags)) {
305
346
  const ackMsg = reply(msg, { ack: msg.id });
@@ -392,30 +433,29 @@ var Transport = class {
392
433
  }
393
434
  };
394
435
 
395
- // transport/impls/stdio/stdio.ts
396
- var import_readline = __toESM(require("readline"), 1);
397
- var newlineBuff = new TextEncoder().encode("\n");
398
- var StdioConnection = class extends Connection {
436
+ // transport/impls/stdio/connection.ts
437
+ var StreamConnection = class extends Connection {
399
438
  output;
400
439
  constructor(transport, connectedTo, output) {
401
440
  super(transport, connectedTo);
402
441
  this.output = output;
403
442
  }
404
443
  send(payload) {
405
- const out = new Uint8Array(payload.length + newlineBuff.length);
406
- out.set(payload, 0);
407
- out.set(newlineBuff, payload.length);
408
- return this.output.write(out);
444
+ if (!this.output.writable) {
445
+ return false;
446
+ }
447
+ return this.output.write(Buffer.concat([payload, defaultDelimiter]));
409
448
  }
410
449
  async close() {
411
450
  this.output.end();
412
451
  }
413
452
  };
453
+
454
+ // transport/impls/stdio/stdio.ts
414
455
  var defaultOptions = {
415
456
  codec: NaiveJsonCodec
416
457
  };
417
458
  var StdioTransport = class extends Transport {
418
- clientId;
419
459
  input = process.stdin;
420
460
  output = process.stdout;
421
461
  /**
@@ -427,29 +467,30 @@ var StdioTransport = class extends Transport {
427
467
  constructor(clientId, input = process.stdin, output = process.stdout, providedOptions) {
428
468
  const options = { ...defaultOptions, ...providedOptions };
429
469
  super(options.codec, clientId);
430
- this.clientId = clientId;
431
- this.input = input;
470
+ const delimStream = createDelimitedStream();
471
+ this.input = input.pipe(delimStream);
432
472
  this.output = output;
433
- this.setupConnectionStatusListeners();
434
- }
435
- setupConnectionStatusListeners() {
436
473
  let conn = void 0;
437
- const rl = import_readline.default.createInterface({
438
- input: this.input
439
- });
440
- const encoder2 = new TextEncoder();
441
- rl.on("line", (msg) => {
442
- const parsedMsg = this.parseMsg(encoder2.encode(msg));
474
+ this.input.on("data", (msg) => {
475
+ const parsedMsg = this.parseMsg(msg);
443
476
  if (parsedMsg && !this.connections.has(parsedMsg.from)) {
444
- conn = new StdioConnection(this, parsedMsg.from, this.output);
477
+ conn = new StreamConnection(this, parsedMsg.from, this.output);
445
478
  this.onConnect(conn);
446
479
  }
447
480
  this.handleMsg(parsedMsg);
448
481
  });
449
- rl.on("close", () => {
482
+ const cleanup = () => {
483
+ delimStream.destroy();
450
484
  if (conn) {
451
485
  this.onDisconnect(conn);
452
486
  }
487
+ };
488
+ this.input.on("close", cleanup);
489
+ this.input.on("error", (err) => {
490
+ log?.warn(
491
+ `${this.clientId} -- stdio error in connection to ${conn?.connectedTo ?? "unknown"}: ${err}`
492
+ );
493
+ cleanup();
453
494
  });
454
495
  }
455
496
  async createNewConnection(to) {
@@ -457,12 +498,11 @@ var StdioTransport = class extends Transport {
457
498
  throw new Error("cant reopen a destroyed connection");
458
499
  }
459
500
  log?.info(`${this.clientId} -- establishing a new stream to ${to}`);
460
- const conn = new StdioConnection(this, to, this.output);
501
+ const conn = new StreamConnection(this, to, this.output);
461
502
  this.onConnect(conn);
462
503
  }
463
504
  };
464
505
  // Annotate the CommonJS export names for ESM import in node:
465
506
  0 && (module.exports = {
466
- StdioConnection,
467
507
  StdioTransport
468
508
  });
@@ -1,13 +1,8 @@
1
1
  import { C as Codec } from '../../../types-3e5768ec.js';
2
- import { Connection, Transport, TransportClientId } from '../../index.cjs';
2
+ import { Transport, TransportClientId } from '../../index.cjs';
3
+ import { S as StreamConnection } from '../../../connection-2529fc14.js';
3
4
  import '@sinclair/typebox';
4
5
 
5
- declare class StdioConnection extends Connection {
6
- output: NodeJS.WritableStream;
7
- constructor(transport: Transport<StdioConnection>, connectedTo: TransportClientId, output: NodeJS.WritableStream);
8
- send(payload: Uint8Array): boolean;
9
- close(): Promise<void>;
10
- }
11
6
  interface Options {
12
7
  codec: Codec;
13
8
  }
@@ -15,8 +10,7 @@ interface Options {
15
10
  * A transport implementation that uses standard input and output streams.
16
11
  * @extends Transport
17
12
  */
18
- declare class StdioTransport extends Transport<StdioConnection> {
19
- clientId: TransportClientId;
13
+ declare class StdioTransport extends Transport<StreamConnection> {
20
14
  input: NodeJS.ReadableStream;
21
15
  output: NodeJS.WritableStream;
22
16
  /**
@@ -26,8 +20,7 @@ declare class StdioTransport extends Transport<StdioConnection> {
26
20
  * @param output - The writable stream to use as output. Defaults to process.stdout.
27
21
  */
28
22
  constructor(clientId: TransportClientId, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream, providedOptions?: Partial<Options>);
29
- setupConnectionStatusListeners(): void;
30
23
  createNewConnection(to: TransportClientId): Promise<void>;
31
24
  }
32
25
 
33
- export { StdioConnection, StdioTransport };
26
+ export { StdioTransport };