@replit/river 0.10.10 → 0.10.12

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
@@ -89,6 +89,7 @@ In another file for the client (to create a separate entrypoint),
89
89
  import WebSocket from 'isomorphic-ws';
90
90
  import { WebSocketClientTransport } from '@replit/river/transport/ws/client';
91
91
  import { createClient } from '@replit/river';
92
+ import type ServiceSurface from './server';
92
93
 
93
94
  const websocketUrl = `ws://localhost:3000`;
94
95
  const transport = new WebSocketClientTransport(
@@ -118,6 +119,24 @@ bindLogger(console.log);
118
119
  setLevel('info');
119
120
  ```
120
121
 
122
+ To listen for connection status changes,
123
+
124
+ ```ts
125
+ transport.addEventListener('connectionStatus', (evt) => {
126
+ if (evt.status === 'connect') {
127
+ // do something
128
+ } else if (evt.status === 'disconnect') {
129
+ // do something else
130
+ }
131
+ });
132
+ ```
133
+
134
+ > [!note] WebSocket connection behaviour
135
+ > WebSocket is an idle protocol. This means that when the underlying connection drops, the WebSocket
136
+ > may still think it is still connected (e.g. turning off the network via devtools).
137
+ > You can use `window.addEventListener('online', ...);` and `window.addEventListener('offline', ...);` to
138
+ > know if you need to recreate the transport.
139
+
121
140
  ### Further examples
122
141
 
123
142
  We've also provided an end-to-end testing environment using Next.js, and a simple backend connected
@@ -27,20 +27,28 @@ var WebSocketClientTransport = class extends Transport {
27
27
  reconnectPromises;
28
28
  tryReconnecting = true;
29
29
  /**
30
- * Creates a new WebSocketTransport instance.
30
+ * Creates a new WebSocketClientTransport instance.
31
31
  * @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
32
- * @param clientId The ID of the client using the transport.
32
+ * @param sessionId The ID of the client using the transport. This should be unique per session.
33
+ * @param serverId The ID of the server this transport is connecting to.
33
34
  * @param providedOptions An optional object containing configuration options for the transport.
34
35
  */
35
- constructor(wsGetter, clientId, serverId, providedOptions) {
36
+ constructor(wsGetter, sessionId, serverId, providedOptions) {
36
37
  const options = { ...defaultOptions, ...providedOptions };
37
- super(options.codec, clientId);
38
+ super(options.codec, sessionId);
38
39
  this.wsGetter = wsGetter;
39
40
  this.serverId = serverId;
40
41
  this.options = options;
41
42
  this.reconnectPromises = /* @__PURE__ */ new Map();
42
43
  this.createNewConnection(this.serverId);
43
44
  }
45
+ reopen() {
46
+ if (this.state === "destroyed") {
47
+ throw new Error("cant reopen a destroyed connection");
48
+ }
49
+ this.state = "open";
50
+ this.createNewConnection(this.serverId);
51
+ }
44
52
  async createNewConnection(to, attempt = 0) {
45
53
  if (this.state === "destroyed") {
46
54
  throw new Error("cant reopen a destroyed connection");
@@ -55,23 +63,28 @@ var WebSocketClientTransport = class extends Transport {
55
63
  }
56
64
  reconnectPromise = new Promise(async (resolve) => {
57
65
  log?.info(`${this.clientId} -- establishing a new websocket to ${to}`);
58
- const ws = await this.wsGetter(to);
59
- if (ws.readyState === ws.OPEN) {
60
- return resolve({ ws });
61
- }
62
- if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
63
- return resolve({ err: "ws is closing or closed" });
66
+ try {
67
+ const ws = await this.wsGetter(to);
68
+ if (ws.readyState === ws.OPEN) {
69
+ return resolve({ ws });
70
+ }
71
+ if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
72
+ return resolve({ err: "ws is closing or closed" });
73
+ }
74
+ const onOpen = () => {
75
+ ws.removeEventListener("open", onOpen);
76
+ resolve({ ws });
77
+ };
78
+ const onClose = (evt) => {
79
+ ws.removeEventListener("close", onClose);
80
+ resolve({ err: evt.reason });
81
+ };
82
+ ws.addEventListener("open", onOpen);
83
+ ws.addEventListener("close", onClose);
84
+ } catch (e) {
85
+ const reason = e instanceof Error ? e.message : "unknown reason";
86
+ return resolve({ err: `couldn't get a new websocket: ${reason}` });
64
87
  }
65
- const onOpen = () => {
66
- ws.removeEventListener("open", onOpen);
67
- resolve({ ws });
68
- };
69
- const onClose = (evt) => {
70
- ws.removeEventListener("close", onClose);
71
- resolve({ err: evt.reason });
72
- };
73
- ws.addEventListener("open", onOpen);
74
- ws.addEventListener("close", onClose);
75
88
  });
76
89
  this.reconnectPromises.set(to, reconnectPromise);
77
90
  }
@@ -417,20 +417,28 @@ var WebSocketClientTransport = class extends Transport {
417
417
  reconnectPromises;
418
418
  tryReconnecting = true;
419
419
  /**
420
- * Creates a new WebSocketTransport instance.
420
+ * Creates a new WebSocketClientTransport instance.
421
421
  * @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
422
- * @param clientId The ID of the client using the transport.
422
+ * @param sessionId The ID of the client using the transport. This should be unique per session.
423
+ * @param serverId The ID of the server this transport is connecting to.
423
424
  * @param providedOptions An optional object containing configuration options for the transport.
424
425
  */
425
- constructor(wsGetter, clientId, serverId, providedOptions) {
426
+ constructor(wsGetter, sessionId, serverId, providedOptions) {
426
427
  const options = { ...defaultOptions, ...providedOptions };
427
- super(options.codec, clientId);
428
+ super(options.codec, sessionId);
428
429
  this.wsGetter = wsGetter;
429
430
  this.serverId = serverId;
430
431
  this.options = options;
431
432
  this.reconnectPromises = /* @__PURE__ */ new Map();
432
433
  this.createNewConnection(this.serverId);
433
434
  }
435
+ reopen() {
436
+ if (this.state === "destroyed") {
437
+ throw new Error("cant reopen a destroyed connection");
438
+ }
439
+ this.state = "open";
440
+ this.createNewConnection(this.serverId);
441
+ }
434
442
  async createNewConnection(to, attempt = 0) {
435
443
  if (this.state === "destroyed") {
436
444
  throw new Error("cant reopen a destroyed connection");
@@ -445,23 +453,28 @@ var WebSocketClientTransport = class extends Transport {
445
453
  }
446
454
  reconnectPromise = new Promise(async (resolve) => {
447
455
  log?.info(`${this.clientId} -- establishing a new websocket to ${to}`);
448
- const ws = await this.wsGetter(to);
449
- if (ws.readyState === ws.OPEN) {
450
- return resolve({ ws });
451
- }
452
- if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
453
- return resolve({ err: "ws is closing or closed" });
456
+ try {
457
+ const ws = await this.wsGetter(to);
458
+ if (ws.readyState === ws.OPEN) {
459
+ return resolve({ ws });
460
+ }
461
+ if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
462
+ return resolve({ err: "ws is closing or closed" });
463
+ }
464
+ const onOpen = () => {
465
+ ws.removeEventListener("open", onOpen);
466
+ resolve({ ws });
467
+ };
468
+ const onClose = (evt) => {
469
+ ws.removeEventListener("close", onClose);
470
+ resolve({ err: evt.reason });
471
+ };
472
+ ws.addEventListener("open", onOpen);
473
+ ws.addEventListener("close", onClose);
474
+ } catch (e) {
475
+ const reason = e instanceof Error ? e.message : "unknown reason";
476
+ return resolve({ err: `couldn't get a new websocket: ${reason}` });
454
477
  }
455
- const onOpen = () => {
456
- ws.removeEventListener("open", onOpen);
457
- resolve({ ws });
458
- };
459
- const onClose = (evt) => {
460
- ws.removeEventListener("close", onClose);
461
- resolve({ err: evt.reason });
462
- };
463
- ws.addEventListener("open", onOpen);
464
- ws.addEventListener("close", onClose);
465
478
  });
466
479
  this.reconnectPromises.set(to, reconnectPromise);
467
480
  }
@@ -29,12 +29,14 @@ declare class WebSocketClientTransport extends Transport<WebSocketConnection> {
29
29
  reconnectPromises: Map<TransportClientId, Promise<WebSocketResult>>;
30
30
  tryReconnecting: boolean;
31
31
  /**
32
- * Creates a new WebSocketTransport instance.
32
+ * Creates a new WebSocketClientTransport instance.
33
33
  * @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
34
- * @param clientId The ID of the client using the transport.
34
+ * @param sessionId The ID of the client using the transport. This should be unique per session.
35
+ * @param serverId The ID of the server this transport is connecting to.
35
36
  * @param providedOptions An optional object containing configuration options for the transport.
36
37
  */
37
- constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, serverId: TransportClientId, providedOptions?: Partial<Options>);
38
+ constructor(wsGetter: () => Promise<WebSocket>, sessionId: TransportClientId, serverId: TransportClientId, providedOptions?: Partial<Options>);
39
+ reopen(): void;
38
40
  createNewConnection(to: string, attempt?: number): Promise<void>;
39
41
  close(): Promise<void>;
40
42
  destroy(): Promise<void>;
@@ -29,12 +29,14 @@ declare class WebSocketClientTransport extends Transport<WebSocketConnection> {
29
29
  reconnectPromises: Map<TransportClientId, Promise<WebSocketResult>>;
30
30
  tryReconnecting: boolean;
31
31
  /**
32
- * Creates a new WebSocketTransport instance.
32
+ * Creates a new WebSocketClientTransport instance.
33
33
  * @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
34
- * @param clientId The ID of the client using the transport.
34
+ * @param sessionId The ID of the client using the transport. This should be unique per session.
35
+ * @param serverId The ID of the server this transport is connecting to.
35
36
  * @param providedOptions An optional object containing configuration options for the transport.
36
37
  */
37
- constructor(wsGetter: () => Promise<WebSocket>, clientId: TransportClientId, serverId: TransportClientId, providedOptions?: Partial<Options>);
38
+ constructor(wsGetter: () => Promise<WebSocket>, sessionId: TransportClientId, serverId: TransportClientId, providedOptions?: Partial<Options>);
39
+ reopen(): void;
38
40
  createNewConnection(to: string, attempt?: number): Promise<void>;
39
41
  close(): Promise<void>;
40
42
  destroy(): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WebSocketClientTransport
3
- } from "../../../chunk-FTVAZZ6B.js";
3
+ } from "../../../chunk-5IC5XMWK.js";
4
4
  import "../../../chunk-L7D75G4K.js";
5
5
  import "../../../chunk-R6H2BIMC.js";
6
6
  import "../../../chunk-LQXPKF3A.js";
@@ -205,6 +205,10 @@ declare abstract class Transport<ConnType extends Connection> {
205
205
  * The downstream implementation needs to implement this. If the downstream
206
206
  * transport cannot make new outgoing connections (e.g. a server transport),
207
207
  * it is ok to log an error and return.
208
+ *
209
+ * Consumers of river should never need to call this directly.
210
+ * Instead, look for a `reopen` method on the transport.
211
+ *
208
212
  * @param to The client ID of the node to connect to.
209
213
  * @returns The new connection object.
210
214
  */
@@ -205,6 +205,10 @@ declare abstract class Transport<ConnType extends Connection> {
205
205
  * The downstream implementation needs to implement this. If the downstream
206
206
  * transport cannot make new outgoing connections (e.g. a server transport),
207
207
  * it is ok to log an error and return.
208
+ *
209
+ * Consumers of river should never need to call this directly.
210
+ * Instead, look for a `reopen` method on the transport.
211
+ *
208
212
  * @param to The client ID of the node to connect to.
209
213
  * @returns The new connection object.
210
214
  */
@@ -454,20 +454,28 @@ var WebSocketClientTransport = class extends Transport {
454
454
  reconnectPromises;
455
455
  tryReconnecting = true;
456
456
  /**
457
- * Creates a new WebSocketTransport instance.
457
+ * Creates a new WebSocketClientTransport instance.
458
458
  * @param wsGetter A function that returns a Promise that resolves to a WebSocket instance.
459
- * @param clientId The ID of the client using the transport.
459
+ * @param sessionId The ID of the client using the transport. This should be unique per session.
460
+ * @param serverId The ID of the server this transport is connecting to.
460
461
  * @param providedOptions An optional object containing configuration options for the transport.
461
462
  */
462
- constructor(wsGetter, clientId, serverId, providedOptions) {
463
+ constructor(wsGetter, sessionId, serverId, providedOptions) {
463
464
  const options = { ...defaultOptions, ...providedOptions };
464
- super(options.codec, clientId);
465
+ super(options.codec, sessionId);
465
466
  this.wsGetter = wsGetter;
466
467
  this.serverId = serverId;
467
468
  this.options = options;
468
469
  this.reconnectPromises = /* @__PURE__ */ new Map();
469
470
  this.createNewConnection(this.serverId);
470
471
  }
472
+ reopen() {
473
+ if (this.state === "destroyed") {
474
+ throw new Error("cant reopen a destroyed connection");
475
+ }
476
+ this.state = "open";
477
+ this.createNewConnection(this.serverId);
478
+ }
471
479
  async createNewConnection(to, attempt = 0) {
472
480
  if (this.state === "destroyed") {
473
481
  throw new Error("cant reopen a destroyed connection");
@@ -482,23 +490,28 @@ var WebSocketClientTransport = class extends Transport {
482
490
  }
483
491
  reconnectPromise = new Promise(async (resolve) => {
484
492
  log?.info(`${this.clientId} -- establishing a new websocket to ${to}`);
485
- const ws = await this.wsGetter(to);
486
- if (ws.readyState === ws.OPEN) {
487
- return resolve({ ws });
488
- }
489
- if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
490
- return resolve({ err: "ws is closing or closed" });
493
+ try {
494
+ const ws = await this.wsGetter(to);
495
+ if (ws.readyState === ws.OPEN) {
496
+ return resolve({ ws });
497
+ }
498
+ if (ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) {
499
+ return resolve({ err: "ws is closing or closed" });
500
+ }
501
+ const onOpen = () => {
502
+ ws.removeEventListener("open", onOpen);
503
+ resolve({ ws });
504
+ };
505
+ const onClose = (evt) => {
506
+ ws.removeEventListener("close", onClose);
507
+ resolve({ err: evt.reason });
508
+ };
509
+ ws.addEventListener("open", onOpen);
510
+ ws.addEventListener("close", onClose);
511
+ } catch (e) {
512
+ const reason = e instanceof Error ? e.message : "unknown reason";
513
+ return resolve({ err: `couldn't get a new websocket: ${reason}` });
491
514
  }
492
- const onOpen = () => {
493
- ws.removeEventListener("open", onOpen);
494
- resolve({ ws });
495
- };
496
- const onClose = (evt) => {
497
- ws.removeEventListener("close", onClose);
498
- resolve({ err: evt.reason });
499
- };
500
- ws.addEventListener("open", onOpen);
501
- ws.addEventListener("close", onClose);
502
515
  });
503
516
  this.reconnectPromises.set(to, reconnectPromise);
504
517
  }
@@ -4,7 +4,7 @@ import {
4
4
  } from "../chunk-3JGVFWKQ.js";
5
5
  import {
6
6
  WebSocketClientTransport
7
- } from "../chunk-FTVAZZ6B.js";
7
+ } from "../chunk-5IC5XMWK.js";
8
8
  import {
9
9
  WebSocketServerTransport
10
10
  } from "../chunk-PJ2EUO7O.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.10",
4
+ "version": "0.10.12",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -63,11 +63,11 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/ws": "^8.5.5",
66
- "@vitest/ui": "^1.1.0",
66
+ "@vitest/ui": "^1.2.1",
67
67
  "prettier": "^3.0.0",
68
68
  "tsup": "^7.2.0",
69
69
  "typescript": "^5.2.2",
70
- "vitest": "^1.1.0"
70
+ "vitest": "^1.2.1"
71
71
  },
72
72
  "scripts": {
73
73
  "check": "tsc --noEmit && npx prettier . --check",