@replit/river 0.10.5 → 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.
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-SZTOUKL7.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-V2YJRBRX.js";
9
+ } from "./chunk-MF3Z3IDF.js";
10
10
  import {
11
11
  log
12
12
  } from "./chunk-SLUSVGQH.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Connection
3
- } from "./chunk-V2YJRBRX.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-SZTOUKL7.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-V2YJRBRX.js";
9
+ } from "./chunk-MF3Z3IDF.js";
10
10
  import {
11
11
  log
12
12
  } from "./chunk-SLUSVGQH.js";
@@ -184,9 +184,6 @@ var Transport = class {
184
184
  }
185
185
  } else {
186
186
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
187
- if (msg.to !== this.clientId) {
188
- return;
189
- }
190
187
  this.eventDispatcher.dispatchEvent("message", msg);
191
188
  if (!isAck(msg.controlFlags)) {
192
189
  const ackMsg = reply(msg, { ack: msg.id });
@@ -515,6 +515,12 @@ function handleRpc(transport, serverId, input, serviceName, procName) {
515
515
  transport.removeEventListener("connectionStatus", onConnectionStatus);
516
516
  }
517
517
  function onMessage(msg2) {
518
+ if (msg2.streamId !== streamId) {
519
+ return;
520
+ }
521
+ if (msg2.to !== transport.clientId) {
522
+ return;
523
+ }
518
524
  if (msg2.streamId === streamId) {
519
525
  cleanup();
520
526
  resolve(msg2.payload);
@@ -560,6 +566,9 @@ function handleStream(transport, serverId, init, serviceName, procName) {
560
566
  if (msg2.streamId !== streamId) {
561
567
  return;
562
568
  }
569
+ if (msg2.to !== transport.clientId) {
570
+ return;
571
+ }
563
572
  if (isStreamClose(msg2.controlFlags)) {
564
573
  cleanup();
565
574
  } else {
@@ -606,6 +615,9 @@ function handleSubscribe(transport, serverId, input, serviceName, procName) {
606
615
  if (msg2.streamId !== streamId) {
607
616
  return;
608
617
  }
618
+ if (msg2.to !== transport.clientId) {
619
+ return;
620
+ }
609
621
  if (isStreamClose(msg2.controlFlags)) {
610
622
  cleanup();
611
623
  } else {
@@ -680,6 +692,9 @@ function handleUpload(transport, serverId, input, serviceName, procName) {
680
692
  transport.removeEventListener("connectionStatus", onConnectionStatus);
681
693
  }
682
694
  function onMessage(msg2) {
695
+ if (msg2.to !== transport.clientId) {
696
+ return;
697
+ }
683
698
  if (msg2.streamId === streamId) {
684
699
  cleanup();
685
700
  resolve(msg2.payload);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Connection
3
- } from "./chunk-V2YJRBRX.js";
3
+ } from "./chunk-MF3Z3IDF.js";
4
4
 
5
5
  // transport/transforms/delim.ts
6
6
  import { Transform } from "node:stream";
@@ -595,6 +595,12 @@ function handleRpc(transport, serverId, input, serviceName, procName) {
595
595
  transport.removeEventListener("connectionStatus", onConnectionStatus);
596
596
  }
597
597
  function onMessage(msg2) {
598
+ if (msg2.streamId !== streamId) {
599
+ return;
600
+ }
601
+ if (msg2.to !== transport.clientId) {
602
+ return;
603
+ }
598
604
  if (msg2.streamId === streamId) {
599
605
  cleanup();
600
606
  resolve(msg2.payload);
@@ -640,6 +646,9 @@ function handleStream(transport, serverId, init, serviceName, procName) {
640
646
  if (msg2.streamId !== streamId) {
641
647
  return;
642
648
  }
649
+ if (msg2.to !== transport.clientId) {
650
+ return;
651
+ }
643
652
  if (isStreamClose(msg2.controlFlags)) {
644
653
  cleanup();
645
654
  } else {
@@ -686,6 +695,9 @@ function handleSubscribe(transport, serverId, input, serviceName, procName) {
686
695
  if (msg2.streamId !== streamId) {
687
696
  return;
688
697
  }
698
+ if (msg2.to !== transport.clientId) {
699
+ return;
700
+ }
689
701
  if (isStreamClose(msg2.controlFlags)) {
690
702
  cleanup();
691
703
  } else {
@@ -760,6 +772,9 @@ function handleUpload(transport, serverId, input, serviceName, procName) {
760
772
  transport.removeEventListener("connectionStatus", onConnectionStatus);
761
773
  }
762
774
  function onMessage(msg2) {
775
+ if (msg2.to !== transport.clientId) {
776
+ return;
777
+ }
763
778
  if (msg2.streamId === streamId) {
764
779
  cleanup();
765
780
  resolve(msg2.payload);
@@ -8,7 +8,7 @@ import {
8
8
  createClient,
9
9
  createServer,
10
10
  serializeService
11
- } from "../chunk-IYRPZPSQ.js";
11
+ } from "../chunk-SCULZ4KS.js";
12
12
  import "../chunk-ZE4MX7DF.js";
13
13
  import "../chunk-SLUSVGQH.js";
14
14
  export {
@@ -341,9 +341,6 @@ var Transport = class {
341
341
  }
342
342
  } else {
343
343
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
344
- if (msg.to !== this.clientId) {
345
- return;
346
- }
347
344
  this.eventDispatcher.dispatchEvent("message", msg);
348
345
  if (!isAck(msg.controlFlags)) {
349
346
  const ackMsg = reply(msg, { ack: msg.id });
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  StreamConnection,
3
3
  createDelimitedStream
4
- } from "../../../chunk-TZSX5KM2.js";
4
+ } from "../../../chunk-XWRKNZSC.js";
5
5
  import "../../../chunk-ORAG7IAU.js";
6
6
  import {
7
7
  NaiveJsonCodec
8
8
  } from "../../../chunk-R6H2BIMC.js";
9
9
  import {
10
10
  Transport
11
- } from "../../../chunk-V2YJRBRX.js";
11
+ } from "../../../chunk-MF3Z3IDF.js";
12
12
  import "../../../chunk-ZE4MX7DF.js";
13
13
  import {
14
14
  log
@@ -241,9 +241,6 @@ var Transport = class {
241
241
  }
242
242
  } else {
243
243
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
244
- if (msg.to !== this.clientId) {
245
- return;
246
- }
247
244
  this.eventDispatcher.dispatchEvent("message", msg);
248
245
  if (!isAck(msg.controlFlags)) {
249
246
  const ackMsg = reply(msg, { ack: msg.id });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StreamConnection,
3
3
  createDelimitedStream
4
- } from "../../../chunk-TZSX5KM2.js";
4
+ } from "../../../chunk-XWRKNZSC.js";
5
5
  import "../../../chunk-ORAG7IAU.js";
6
6
  import "../../../chunk-WVT5QXMZ.js";
7
7
  import {
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../../chunk-R6H2BIMC.js";
10
10
  import {
11
11
  Transport
12
- } from "../../../chunk-V2YJRBRX.js";
12
+ } from "../../../chunk-MF3Z3IDF.js";
13
13
  import "../../../chunk-ZE4MX7DF.js";
14
14
  import {
15
15
  log
@@ -241,9 +241,6 @@ var Transport = class {
241
241
  }
242
242
  } else {
243
243
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
244
- if (msg.to !== this.clientId) {
245
- return;
246
- }
247
244
  this.eventDispatcher.dispatchEvent("message", msg);
248
245
  if (!isAck(msg.controlFlags)) {
249
246
  const ackMsg = reply(msg, { ack: msg.id });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StreamConnection,
3
3
  createDelimitedStream
4
- } from "../../../chunk-TZSX5KM2.js";
4
+ } from "../../../chunk-XWRKNZSC.js";
5
5
  import "../../../chunk-ORAG7IAU.js";
6
6
  import "../../../chunk-WVT5QXMZ.js";
7
7
  import {
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../../chunk-R6H2BIMC.js";
10
10
  import {
11
11
  Transport
12
- } from "../../../chunk-V2YJRBRX.js";
12
+ } from "../../../chunk-MF3Z3IDF.js";
13
13
  import "../../../chunk-ZE4MX7DF.js";
14
14
  import {
15
15
  log
@@ -241,9 +241,6 @@ var Transport = class {
241
241
  }
242
242
  } else {
243
243
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
244
- if (msg.to !== this.clientId) {
245
- return;
246
- }
247
244
  this.eventDispatcher.dispatchEvent("message", msg);
248
245
  if (!isAck(msg.controlFlags)) {
249
246
  const ackMsg = reply(msg, { ack: msg.id });
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  WebSocketClientTransport
3
- } from "../../../chunk-IEU7OE5W.js";
4
- import "../../../chunk-SZTOUKL7.js";
3
+ } from "../../../chunk-5735JCTZ.js";
4
+ import "../../../chunk-KRYKORQT.js";
5
5
  import "../../../chunk-R6H2BIMC.js";
6
- import "../../../chunk-V2YJRBRX.js";
6
+ import "../../../chunk-MF3Z3IDF.js";
7
7
  import "../../../chunk-ZE4MX7DF.js";
8
8
  import "../../../chunk-SLUSVGQH.js";
9
9
  export {
@@ -287,9 +287,6 @@ var Transport = class {
287
287
  }
288
288
  } else {
289
289
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg)}`);
290
- if (msg.to !== this.clientId) {
291
- return;
292
- }
293
290
  this.eventDispatcher.dispatchEvent("message", msg);
294
291
  if (!isAck(msg.controlFlags)) {
295
292
  const ackMsg = reply(msg, { ack: msg.id });
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  WebSocketServerTransport
3
- } from "../../../chunk-PNZXYQME.js";
4
- import "../../../chunk-SZTOUKL7.js";
3
+ } from "../../../chunk-LRQGTUTI.js";
4
+ import "../../../chunk-KRYKORQT.js";
5
5
  import "../../../chunk-WVT5QXMZ.js";
6
6
  import "../../../chunk-R6H2BIMC.js";
7
- import "../../../chunk-V2YJRBRX.js";
7
+ import "../../../chunk-MF3Z3IDF.js";
8
8
  import "../../../chunk-ZE4MX7DF.js";
9
9
  import "../../../chunk-SLUSVGQH.js";
10
10
  export {
@@ -258,9 +258,6 @@ var Transport = class {
258
258
  }
259
259
  } else {
260
260
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg2)}`);
261
- if (msg2.to !== this.clientId) {
262
- return;
263
- }
264
261
  this.eventDispatcher.dispatchEvent("message", msg2);
265
262
  if (!isAck(msg2.controlFlags)) {
266
263
  const ackMsg = reply(msg2, { ack: msg2.id });
@@ -2,7 +2,7 @@ import "../chunk-ORAG7IAU.js";
2
2
  import {
3
3
  Connection,
4
4
  Transport
5
- } from "../chunk-V2YJRBRX.js";
5
+ } from "../chunk-MF3Z3IDF.js";
6
6
  import {
7
7
  OpaqueTransportMessageSchema,
8
8
  TransportMessageSchema,
@@ -278,9 +278,6 @@ var Transport = class {
278
278
  }
279
279
  } else {
280
280
  log?.info(`${this.clientId} -- received msg: ${JSON.stringify(msg2)}`);
281
- if (msg2.to !== this.clientId) {
282
- return;
283
- }
284
281
  this.eventDispatcher.dispatchEvent("message", msg2);
285
282
  if (!isAck(msg2.controlFlags)) {
286
283
  const ackMsg = reply(msg2, { ack: msg2.id });
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  UNCAUGHT_ERROR,
3
3
  pushable
4
- } from "../chunk-IYRPZPSQ.js";
4
+ } from "../chunk-SCULZ4KS.js";
5
5
  import {
6
6
  WebSocketClientTransport
7
- } from "../chunk-IEU7OE5W.js";
7
+ } from "../chunk-5735JCTZ.js";
8
8
  import {
9
9
  WebSocketServerTransport
10
- } from "../chunk-PNZXYQME.js";
11
- import "../chunk-SZTOUKL7.js";
10
+ } from "../chunk-LRQGTUTI.js";
11
+ import "../chunk-KRYKORQT.js";
12
12
  import "../chunk-ORAG7IAU.js";
13
13
  import "../chunk-WVT5QXMZ.js";
14
14
  import "../chunk-R6H2BIMC.js";
15
- import "../chunk-V2YJRBRX.js";
15
+ import "../chunk-MF3Z3IDF.js";
16
16
  import {
17
17
  msg
18
18
  } from "../chunk-ZE4MX7DF.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@replit/river",
3
3
  "description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!",
4
- "version": "0.10.5",
4
+ "version": "0.10.6",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {