@replit/river 0.15.0 → 0.15.2

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 (35) hide show
  1. package/README.md +41 -22
  2. package/dist/{builder-ca6c4259.d.ts → builder-ebd945c0.d.ts} +1 -1
  3. package/dist/{chunk-IVNV5HBI.js → chunk-B7VTDQR7.js} +160 -51
  4. package/dist/{chunk-BSAIT634.js → chunk-UJHTHOTT.js} +1 -1
  5. package/dist/{chunk-FFT7PSUV.js → chunk-ZRB6IKPV.js} +1 -1
  6. package/dist/{connection-0c5eeb14.d.ts → connection-10a24478.d.ts} +1 -1
  7. package/dist/{connection-14675d77.d.ts → connection-f4492948.d.ts} +1 -1
  8. package/dist/{index-f922ec84.d.ts → index-bbccacef.d.ts} +90 -16
  9. package/dist/router/index.d.cts +3 -3
  10. package/dist/router/index.d.ts +3 -3
  11. package/dist/transport/impls/uds/client.cjs +206 -92
  12. package/dist/transport/impls/uds/client.d.cts +3 -3
  13. package/dist/transport/impls/uds/client.d.ts +3 -3
  14. package/dist/transport/impls/uds/client.js +2 -2
  15. package/dist/transport/impls/uds/server.cjs +75 -73
  16. package/dist/transport/impls/uds/server.d.cts +3 -3
  17. package/dist/transport/impls/uds/server.d.ts +3 -3
  18. package/dist/transport/impls/uds/server.js +2 -2
  19. package/dist/transport/impls/ws/client.cjs +206 -94
  20. package/dist/transport/impls/ws/client.d.cts +3 -3
  21. package/dist/transport/impls/ws/client.d.ts +3 -3
  22. package/dist/transport/impls/ws/client.js +2 -2
  23. package/dist/transport/impls/ws/server.cjs +75 -73
  24. package/dist/transport/impls/ws/server.d.cts +3 -3
  25. package/dist/transport/impls/ws/server.d.ts +3 -3
  26. package/dist/transport/impls/ws/server.js +2 -2
  27. package/dist/transport/index.cjs +211 -105
  28. package/dist/transport/index.d.cts +1 -1
  29. package/dist/transport/index.d.ts +1 -1
  30. package/dist/transport/index.js +3 -3
  31. package/dist/util/testHelpers.cjs +61 -64
  32. package/dist/util/testHelpers.d.cts +10 -4
  33. package/dist/util/testHelpers.d.ts +10 -4
  34. package/dist/util/testHelpers.js +13 -5
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -1,36 +1,60 @@
1
1
  # River
2
2
 
3
- ## Long-lived Streaming Remote Procedure Calls
3
+ River allows multiple clients to connect to and make remote procedure calls to a remote server as if they were local procedures.
4
4
 
5
- It's like tRPC/gRPC but with
5
+ ## Long-lived streaming remote procedure calls
6
+
7
+ River provides a framework for long-lived streaming Remote Procedure Calls (RPCs) in modern web applications, featuring advanced error handling and customizable retry policies to ensure seamless communication between clients and servers.
8
+
9
+ River provides a framework similar to [tRPC](https://trpc.io/) and [gRPC](https://grpc.io/) but with additional features:
6
10
 
7
11
  - JSON Schema Support + run-time schema validation
8
12
  - full-duplex streaming
9
13
  - service multiplexing
10
14
  - result types and error handling
11
- - snappy DX (no code-generation)
15
+ - snappy DX (no code generation)
12
16
  - transparent reconnect support for long-lived sessions
13
17
  - over any transport (WebSockets and Unix Domain Socket out of the box)
14
18
 
15
19
  See [PROTOCOL.md](./PROTOCOL.md) for more information on the protocol.
16
20
 
17
- ## Installation
21
+ ### Prerequisites
22
+
23
+ Before proceeding, ensure you have TypeScript 5 installed and configured appropriately:
24
+
25
+ 1. **Ensure `"moduleResolution": "bundler"` in tsconfig.json**:
26
+
27
+ ```json
28
+ {
29
+ "compilerOptions": {
30
+ "moduleResolution": "bundler"
31
+ // Other compiler options...
32
+ }
33
+ }
34
+ ```
18
35
 
19
- To use River, you must be on least Typescript 5 with `"moduleResolution": "bundler"`.
36
+ If it exists but is set to a different value, modify it to `"bundler"`.
37
+
38
+ 2. Install River and Dependencies:
39
+
40
+ To use River, install the required packages using npm:
20
41
 
21
42
  ```bash
22
- npm i @replit/river @sinclair/typebox
43
+ npm i @replit/river @sinclair/typebox
44
+ ```
23
45
 
24
- # if you plan on using WebSocket for transport, also install
46
+ 3. If you plan on using WebSocket for the underlying transport, also install
47
+
48
+ ```bash
25
49
  npm i ws isomorphic-ws
26
50
  ```
27
51
 
28
- ## Writing Services
52
+ ## Writing services
29
53
 
30
54
  ### Concepts
31
55
 
32
56
  - Router: a collection of services, namespaced by service name.
33
- - Service: a collection of procedures with shared state.
57
+ - Service: a collection of procedures with a shared state.
34
58
  - 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:
35
59
  - `rpc` whose handler has a signature of `Input -> Result<Output, Error>`.
36
60
  - `upload` whose handler has a signature of `AsyncIterableIterator<Input> -> Result<Output, Error>`.
@@ -73,7 +97,7 @@ export const ExampleServiceConstructor = () =>
73
97
  export const serviceDefs = buildServiceDefs([ExampleServiceConstructor()]);
74
98
  ```
75
99
 
76
- Then, we create the server
100
+ Then, we create the server:
77
101
 
78
102
  ```ts
79
103
  import http from 'http';
@@ -134,20 +158,16 @@ bindLogger(console.log);
134
158
  setLevel('info');
135
159
  ```
136
160
 
137
- ### Connection Status
161
+ ### Connection status
138
162
 
139
- River define two types of reconnects:
163
+ River defines two types of reconnects:
140
164
 
141
- 1. Transparent reconnects: we lost the connection temporarily and reconnected without losing any messages. To the application level, nothing happened.
142
- 2. Hard reconnect: we've lost all server state and the client should setup the world again.
165
+ 1. **Transparent reconnects:** These occur when the connection is temporarily lost and reestablished without losing any messages. From the application's perspective, this process is seamless and does not disrupt ongoing operations.
166
+ 2. **Hard reconnect:** This occurs when all server state is lost, requiring the client to reinitialize anything stateful (e.g. subscriptions).
143
167
 
144
- We can listen for transparent reconnects via the `connectionStatus` events but realistically
145
- no applications should need to listen for this unless it is for debug purposes. Hard reconnects
146
- are signalled via `sessionStatus` events.
168
+ You can listen for transparent reconnects via the `connectionStatus` events, but realistically, no applications should need to listen for this unless it is for debugging purposes. Hard reconnects are signaled via `sessionStatus` events.
147
169
 
148
- If your application is stateful on either the server or the client, the service consumer _should_
149
- wrap all the client-side setup with `transport.addEventListener('sessionStatus', (evt) => ...)` to
150
- do appropriate setup and teardown.
170
+ If your application is stateful on either the server or the client, the service consumer _should_ wrap all the client-side setup with `transport.addEventListener('sessionStatus', (evt) => ...)` to do appropriate setup and teardown.
151
171
 
152
172
  ```ts
153
173
  transport.addEventListener('connectionStatus', (evt) => {
@@ -169,8 +189,7 @@ transport.addEventListener('sessionStatus', (evt) => {
169
189
 
170
190
  ### Further examples
171
191
 
172
- We've also provided an end-to-end testing environment using Next.js, and a simple backend connected
173
- with the WebSocket transport that you can [play with on Replit](https://replit.com/@jzhao-replit/riverbed).
192
+ We've also provided an end-to-end testing environment using `Next.js`, and a simple backend connected with the WebSocket transport that you can [play with on Replit](https://replit.com/@jzhao-replit/riverbed).
174
193
 
175
194
  You can find more service examples in the [E2E test fixtures](https://github.com/replit/river/blob/main/__tests__/fixtures/services.ts)
176
195
 
@@ -1,6 +1,6 @@
1
1
  import { TObject, TUnion, TString, TSchema, TNever, TLiteral, Static } from '@sinclair/typebox';
2
2
  import { Pushable } from 'it-pushable';
3
- import { b as TransportClientId, d as Session, C as Connection } from './index-f922ec84.js';
3
+ import { b as TransportClientId, e as Session, C as Connection } from './index-bbccacef.js';
4
4
 
5
5
  /**
6
6
  * The context for services/procedures. This is used only on
@@ -16,12 +16,11 @@ import {
16
16
  } from "./chunk-GZ7HCLLM.js";
17
17
 
18
18
  // transport/events.ts
19
- var ProtocolErrorType = /* @__PURE__ */ ((ProtocolErrorType2) => {
20
- ProtocolErrorType2["RetriesExceeded"] = "conn_retry_exceeded";
21
- ProtocolErrorType2["HandshakeFailed"] = "handshake_failed";
22
- ProtocolErrorType2["UseAfterDestroy"] = "use_after_destroy";
23
- return ProtocolErrorType2;
24
- })(ProtocolErrorType || {});
19
+ var ProtocolError = {
20
+ RetriesExceeded: "conn_retry_exceeded",
21
+ HandshakeFailed: "handshake_failed",
22
+ UseAfterDestroy: "use_after_destroy"
23
+ };
25
24
  var EventDispatcher = class {
26
25
  eventListeners = {};
27
26
  numberOfListeners(eventType) {
@@ -59,15 +58,6 @@ var Connection = class {
59
58
  this.debugId = `conn-${unsafeId()}`;
60
59
  }
61
60
  };
62
- var HEARTBEAT_INTERVAL_MS = 1e3;
63
- var HEARTBEATS_TILL_DEAD = 2;
64
- var SESSION_DISCONNECT_GRACE_MS = 5e3;
65
- var defaultSessionOptions = {
66
- heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
67
- heartbeatsUntilDead: HEARTBEATS_TILL_DEAD,
68
- sessionDisconnectGraceMs: SESSION_DISCONNECT_GRACE_MS,
69
- codec: NaiveJsonCodec
70
- };
71
61
  var Session = class {
72
62
  codec;
73
63
  options;
@@ -254,13 +244,95 @@ var Session = class {
254
244
  // transport/transport.ts
255
245
  import { Value } from "@sinclair/typebox/value";
256
246
  import { nanoid as nanoid2 } from "nanoid";
257
- var RECONNECT_JITTER_MAX_MS = 500;
258
- var RECONNECT_INTERVAL_MS = 250;
247
+
248
+ // transport/rateLimit.ts
249
+ var LeakyBucketRateLimit = class {
250
+ budgetConsumed;
251
+ intervalHandles;
252
+ options;
253
+ constructor(options) {
254
+ this.options = options;
255
+ this.budgetConsumed = /* @__PURE__ */ new Map();
256
+ this.intervalHandles = /* @__PURE__ */ new Map();
257
+ }
258
+ getBackoffMs(user) {
259
+ if (!this.budgetConsumed.has(user))
260
+ return 0;
261
+ const exponent = Math.max(0, this.getBudgetConsumed(user) - 1);
262
+ const jitter = Math.floor(Math.random() * this.options.maxJitterMs);
263
+ const backoffMs = Math.min(
264
+ this.options.baseIntervalMs * 2 ** exponent,
265
+ this.options.maxBackoffMs
266
+ );
267
+ return backoffMs + jitter;
268
+ }
269
+ get totalBudgetRestoreTime() {
270
+ return this.options.budgetRestoreIntervalMs * this.options.attemptBudgetCapacity;
271
+ }
272
+ consumeBudget(user) {
273
+ this.stopLeak(user);
274
+ this.budgetConsumed.set(user, this.getBudgetConsumed(user) + 1);
275
+ }
276
+ getBudgetConsumed(user) {
277
+ return this.budgetConsumed.get(user) ?? 0;
278
+ }
279
+ hasBudget(user) {
280
+ return this.getBudgetConsumed(user) < this.options.attemptBudgetCapacity;
281
+ }
282
+ startRestoringBudget(user) {
283
+ if (this.intervalHandles.has(user)) {
284
+ return;
285
+ }
286
+ const restoreBudgetForUser = () => {
287
+ const currentBudget = this.budgetConsumed.get(user);
288
+ if (!currentBudget) {
289
+ this.stopLeak(user);
290
+ return;
291
+ }
292
+ const newBudget = currentBudget - 1;
293
+ if (newBudget === 0) {
294
+ this.budgetConsumed.delete(user);
295
+ return;
296
+ }
297
+ this.budgetConsumed.set(user, newBudget);
298
+ };
299
+ restoreBudgetForUser();
300
+ const intervalHandle = setInterval(
301
+ restoreBudgetForUser,
302
+ this.options.budgetRestoreIntervalMs
303
+ );
304
+ this.intervalHandles.set(user, intervalHandle);
305
+ }
306
+ stopLeak(user) {
307
+ if (!this.intervalHandles.has(user)) {
308
+ return;
309
+ }
310
+ clearInterval(this.intervalHandles.get(user));
311
+ this.intervalHandles.delete(user);
312
+ }
313
+ close() {
314
+ for (const user of this.intervalHandles.keys()) {
315
+ this.stopLeak(user);
316
+ }
317
+ }
318
+ };
319
+
320
+ // transport/transport.ts
259
321
  var defaultTransportOptions = {
260
- retryIntervalMs: RECONNECT_INTERVAL_MS,
261
- retryJitterMs: RECONNECT_JITTER_MAX_MS,
262
- retryAttemptsMax: 5,
263
- ...defaultSessionOptions
322
+ heartbeatIntervalMs: 1e3,
323
+ heartbeatsUntilDead: 2,
324
+ sessionDisconnectGraceMs: 5e3,
325
+ codec: NaiveJsonCodec
326
+ };
327
+ var defaultClientTransportOptions = {
328
+ connectionRetryOptions: {
329
+ baseIntervalMs: 250,
330
+ maxJitterMs: 200,
331
+ maxBackoffMs: 32e3,
332
+ attemptBudgetCapacity: 15,
333
+ budgetRestoreIntervalMs: 200
334
+ },
335
+ ...defaultTransportOptions
264
336
  };
265
337
  var Transport = class {
266
338
  /**
@@ -475,7 +547,7 @@ var Transport = class {
475
547
  if (this.state === "destroyed") {
476
548
  const err = "transport is destroyed, cant send";
477
549
  log?.error(`${this.clientId} -- ` + err + `: ${JSON.stringify(msg)}`);
478
- this.protocolError("use_after_destroy" /* UseAfterDestroy */, err);
550
+ this.protocolError(ProtocolError.UseAfterDestroy, err);
479
551
  return void 0;
480
552
  } else if (this.state === "closed") {
481
553
  log?.info(
@@ -535,14 +607,26 @@ var Transport = class {
535
607
  }
536
608
  };
537
609
  var ClientTransport = class extends Transport {
610
+ /**
611
+ * The options for this transport.
612
+ */
613
+ options;
538
614
  /**
539
615
  * The map of reconnect promises for each client ID.
540
616
  */
541
617
  inflightConnectionPromises;
618
+ retryBudget;
542
619
  tryReconnecting = true;
543
620
  constructor(clientId, providedOptions) {
544
621
  super(clientId, providedOptions);
622
+ this.options = {
623
+ ...defaultClientTransportOptions,
624
+ ...providedOptions
625
+ };
545
626
  this.inflightConnectionPromises = /* @__PURE__ */ new Map();
627
+ this.retryBudget = new LeakyBucketRateLimit(
628
+ this.options.connectionRetryOptions
629
+ );
546
630
  }
547
631
  handleConnection(conn, to) {
548
632
  if (this.state !== "open")
@@ -573,7 +657,10 @@ var ClientTransport = class extends Transport {
573
657
  log?.info(
574
658
  `${this.clientId} -- connection (id: ${conn.debugId}) to ${to} disconnected`
575
659
  );
576
- void this.connect(to);
660
+ this.inflightConnectionPromises.delete(to);
661
+ if (this.tryReconnecting) {
662
+ void this.connect(to);
663
+ }
577
664
  });
578
665
  conn.addErrorListener((err) => {
579
666
  log?.warn(
@@ -585,7 +672,7 @@ var ClientTransport = class extends Transport {
585
672
  const parsed = this.parseMsg(data);
586
673
  if (!parsed) {
587
674
  this.protocolError(
588
- "handshake_failed" /* HandshakeFailed */,
675
+ ProtocolError.HandshakeFailed,
589
676
  "received non-transport message"
590
677
  );
591
678
  return false;
@@ -597,7 +684,7 @@ var ClientTransport = class extends Transport {
597
684
  )}`
598
685
  );
599
686
  this.protocolError(
600
- "handshake_failed" /* HandshakeFailed */,
687
+ ProtocolError.HandshakeFailed,
601
688
  "invalid handshake resp"
602
689
  );
603
690
  return false;
@@ -609,7 +696,7 @@ var ClientTransport = class extends Transport {
609
696
  )}`
610
697
  );
611
698
  this.protocolError(
612
- "handshake_failed" /* HandshakeFailed */,
699
+ ProtocolError.HandshakeFailed,
613
700
  parsed.payload.status.reason
614
701
  );
615
702
  return false;
@@ -624,41 +711,64 @@ var ClientTransport = class extends Transport {
624
711
  * Manually attempts to connect to a client.
625
712
  * @param to The client ID of the node to connect to.
626
713
  */
627
- async connect(to, attempt = 0) {
628
- if (this.state !== "open" || !this.tryReconnecting) {
714
+ async connect(to) {
715
+ const canProceedWithConnection = () => this.state === "open";
716
+ if (!canProceedWithConnection()) {
629
717
  log?.info(
630
- `${this.clientId} -- transport state is no longer open, not attempting connection`
718
+ `${this.clientId} -- transport state is no longer open, cancelling attempt to connect to ${to}`
631
719
  );
632
720
  return;
633
721
  }
634
722
  let reconnectPromise = this.inflightConnectionPromises.get(to);
635
723
  if (!reconnectPromise) {
636
- reconnectPromise = this.createNewOutgoingConnection(to).then((conn) => {
724
+ log?.info(`${this.clientId} -- attempting connection to ${to}`);
725
+ const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
726
+ if (!this.retryBudget.hasBudget(to)) {
727
+ const errMsg = `not attempting to connect to ${to}, retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
728
+ log?.warn(`${this.clientId} -- ${errMsg}`);
729
+ this.protocolError(ProtocolError.RetriesExceeded, errMsg);
730
+ return;
731
+ }
732
+ let sleep = Promise.resolve();
733
+ const backoffMs = this.retryBudget.getBackoffMs(to);
734
+ if (backoffMs > 0) {
735
+ sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
736
+ }
737
+ this.retryBudget.consumeBudget(to);
738
+ reconnectPromise = sleep.then(() => {
739
+ if (!canProceedWithConnection()) {
740
+ throw new Error("transport state is no longer open");
741
+ }
742
+ }).then(() => this.createNewOutgoingConnection(to)).then((conn) => {
743
+ if (!canProceedWithConnection()) {
744
+ log?.info(
745
+ `${this.clientId} -- transport state is no longer open, closing pre-handshake connection (id: ${conn.debugId}) to ${to}`
746
+ );
747
+ conn.close();
748
+ throw new Error("transport state is no longer open");
749
+ }
750
+ this.retryBudget.startRestoringBudget(to);
637
751
  this.sendHandshake(to, conn);
638
752
  return conn;
639
753
  });
640
754
  this.inflightConnectionPromises.set(to, reconnectPromise);
755
+ } else {
756
+ log?.info(
757
+ `${this.clientId} -- attempting connection to ${to} (reusing previous attempt)`
758
+ );
641
759
  }
642
760
  try {
643
761
  await reconnectPromise;
644
762
  } catch (error) {
645
- const errStr = coerceErrorString(error);
646
763
  this.inflightConnectionPromises.delete(to);
647
- const shouldRetry = this.state === "open" && this.tryReconnecting;
648
- if (!shouldRetry)
649
- return;
650
- if (attempt >= this.options.retryAttemptsMax) {
651
- const errMsg = `connection to ${to} failed after ${attempt} attempts (${errStr}), giving up`;
652
- log?.error(`${this.clientId} -- ${errMsg}`);
653
- this.protocolError("conn_retry_exceeded" /* RetriesExceeded */, errMsg);
654
- return;
764
+ const errStr = coerceErrorString(error);
765
+ if (!this.tryReconnecting || !canProceedWithConnection()) {
766
+ log?.warn(`${this.clientId} -- connection to ${to} failed (${errStr})`);
655
767
  } else {
656
- const jitter = Math.floor(Math.random() * this.options.retryJitterMs);
657
- const backoffMs = this.options.retryIntervalMs * 2 ** attempt + jitter;
658
768
  log?.warn(
659
- `${this.clientId} -- connection to ${to} failed (${errStr}), trying again in ${backoffMs}ms`
769
+ `${this.clientId} -- connection to ${to} failed (${errStr}), retrying`
660
770
  );
661
- setTimeout(() => void this.connect(to, attempt + 1), backoffMs);
771
+ return this.connect(to);
662
772
  }
663
773
  }
664
774
  }
@@ -671,9 +781,9 @@ var ClientTransport = class extends Transport {
671
781
  log?.debug(`${this.clientId} -- sending handshake request to ${to}`);
672
782
  conn.send(this.codec.toBuffer(requestMsg));
673
783
  }
674
- onDisconnect(conn, session) {
675
- this.inflightConnectionPromises.delete(session.to);
676
- super.onDisconnect(conn, session);
784
+ close() {
785
+ this.retryBudget.close();
786
+ super.close();
677
787
  }
678
788
  };
679
789
  var ServerTransport = class extends Transport {
@@ -729,7 +839,7 @@ var ServerTransport = class extends Transport {
729
839
  const parsed = this.parseMsg(data);
730
840
  if (!parsed) {
731
841
  this.protocolError(
732
- "handshake_failed" /* HandshakeFailed */,
842
+ ProtocolError.HandshakeFailed,
733
843
  "received non-transport message"
734
844
  );
735
845
  return false;
@@ -748,7 +858,7 @@ var ServerTransport = class extends Transport {
748
858
  )}`
749
859
  );
750
860
  this.protocolError(
751
- "handshake_failed" /* HandshakeFailed */,
861
+ ProtocolError.HandshakeFailed,
752
862
  "invalid handshake request"
753
863
  );
754
864
  return false;
@@ -766,7 +876,7 @@ var ServerTransport = class extends Transport {
766
876
  `${this.clientId} -- received handshake msg with incompatible protocol version (got: ${gotVersion}, expected: ${PROTOCOL_VERSION})`
767
877
  );
768
878
  this.protocolError(
769
- "handshake_failed" /* HandshakeFailed */,
879
+ ProtocolError.HandshakeFailed,
770
880
  `incorrect version (got: ${gotVersion} wanted ${PROTOCOL_VERSION})`
771
881
  );
772
882
  return false;
@@ -787,9 +897,8 @@ var ServerTransport = class extends Transport {
787
897
  };
788
898
 
789
899
  export {
790
- ProtocolErrorType,
900
+ ProtocolError,
791
901
  Connection,
792
- defaultSessionOptions,
793
902
  Session,
794
903
  Transport,
795
904
  ClientTransport,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Connection
3
- } from "./chunk-IVNV5HBI.js";
3
+ } from "./chunk-B7VTDQR7.js";
4
4
 
5
5
  // transport/transforms/messageFraming.ts
6
6
  import { Transform } from "node:stream";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Connection
3
- } from "./chunk-IVNV5HBI.js";
3
+ } from "./chunk-B7VTDQR7.js";
4
4
 
5
5
  // transport/impls/ws/connection.ts
6
6
  var WebSocketConnection = class extends Connection {
@@ -1,4 +1,4 @@
1
- import { C as Connection } from './index-f922ec84.js';
1
+ import { C as Connection } from './index-bbccacef.js';
2
2
  import { Socket } from 'node:net';
3
3
  import stream, { Transform, TransformCallback, TransformOptions } from 'node:stream';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
- import { C as Connection } from './index-f922ec84.js';
2
+ import { C as Connection } from './index-bbccacef.js';
3
3
 
4
4
  declare class WebSocketConnection extends Connection {
5
5
  ws: WebSocket;
@@ -129,9 +129,22 @@ declare abstract class Connection {
129
129
  abstract close(): void;
130
130
  }
131
131
  interface SessionOptions {
132
+ /**
133
+ * Frequency at which to send heartbeat acknowledgements
134
+ */
132
135
  heartbeatIntervalMs: number;
136
+ /**
137
+ * Number of elapsed heartbeats without a response message before we consider
138
+ * the connection dead.
139
+ */
133
140
  heartbeatsUntilDead: number;
141
+ /**
142
+ * Duration to wait between connection disconnect and actual session disconnect
143
+ */
134
144
  sessionDisconnectGraceMs: number;
145
+ /**
146
+ * The codec to use for encoding/decoding messages over the wire
147
+ */
135
148
  codec: Codec;
136
149
  }
137
150
  /**
@@ -205,11 +218,12 @@ declare class Session<ConnType extends Connection> {
205
218
  }
206
219
 
207
220
  type ConnectionStatus = 'connect' | 'disconnect';
208
- declare const enum ProtocolErrorType {
209
- RetriesExceeded = "conn_retry_exceeded",
210
- HandshakeFailed = "handshake_failed",
211
- UseAfterDestroy = "use_after_destroy"
212
- }
221
+ declare const ProtocolError: {
222
+ readonly RetriesExceeded: "conn_retry_exceeded";
223
+ readonly HandshakeFailed: "handshake_failed";
224
+ readonly UseAfterDestroy: "use_after_destroy";
225
+ };
226
+ type ProtocolErrorType = (typeof ProtocolError)[keyof typeof ProtocolError];
213
227
  interface EventMap {
214
228
  message: OpaqueTransportMessage;
215
229
  connectionStatus: {
@@ -235,6 +249,58 @@ declare class EventDispatcher<T extends EventTypes> {
235
249
  dispatchEvent<K extends T>(eventType: K, event: EventMap[K]): void;
236
250
  }
237
251
 
252
+ /**
253
+ * Options to control the backoff and retry behavior of the client transport's connection behaviour.
254
+ *
255
+ * River implements exponential backoff with jitter to prevent flooding the server
256
+ * when there's an issue with connection establishment.
257
+ *
258
+ * The backoff is calculated via the following:
259
+ * backOff = min(jitter + {@link baseIntervalMs} * 2 ^ budget_consumed, {@link maxBackoffMs})
260
+ *
261
+ * We use a leaky bucket rate limit with a budget of {@link attemptBudgetCapacity} reconnection attempts.
262
+ * Budget only starts to restore after a successful handshake at a rate of one budget per {@link budgetRestoreIntervalMs}.
263
+ */
264
+ interface ConnectionRetryOptions {
265
+ /**
266
+ * The base interval to wait before retrying a connection.
267
+ */
268
+ baseIntervalMs: number;
269
+ /**
270
+ * The maximum random jitter to add to the total backoff time.
271
+ */
272
+ maxJitterMs: number;
273
+ /**
274
+ * The maximum amount of time to wait before retrying a connection.
275
+ * This does not include the jitter.
276
+ */
277
+ maxBackoffMs: number;
278
+ /**
279
+ * The max number of times to attempt a connection before a successful handshake.
280
+ * This persists across connections but starts restoring budget after a successful handshake.
281
+ * The restoration interval depends on {@link budgetRestoreIntervalMs}
282
+ */
283
+ attemptBudgetCapacity: number;
284
+ /**
285
+ * After a successful connection attempt, how long to wait before we restore a single budget.
286
+ */
287
+ budgetRestoreIntervalMs: number;
288
+ }
289
+ declare class LeakyBucketRateLimit {
290
+ private budgetConsumed;
291
+ private intervalHandles;
292
+ private readonly options;
293
+ constructor(options: ConnectionRetryOptions);
294
+ getBackoffMs(user: TransportClientId): number;
295
+ get totalBudgetRestoreTime(): number;
296
+ consumeBudget(user: TransportClientId): void;
297
+ getBudgetConsumed(user: TransportClientId): number;
298
+ hasBudget(user: TransportClientId): boolean;
299
+ startRestoringBudget(user: TransportClientId): void;
300
+ private stopLeak;
301
+ close(): void;
302
+ }
303
+
238
304
  /**
239
305
  * Represents the possible states of a transport.
240
306
  * @property {'open'} open - The transport is open and operational (note that this doesn't mean it is actively connected)
@@ -242,11 +308,14 @@ declare class EventDispatcher<T extends EventTypes> {
242
308
  * @property {'destroyed'} destroyed - The transport is permanently destroyed and cannot be reopened.
243
309
  */
244
310
  type TransportStatus = 'open' | 'closed' | 'destroyed';
245
- type TransportOptions = {
246
- retryIntervalMs: number;
247
- retryJitterMs: number;
248
- retryAttemptsMax: number;
249
- } & SessionOptions;
311
+ type ProvidedTransportOptions = Partial<SessionOptions>;
312
+ type TransportOptions = Required<ProvidedTransportOptions>;
313
+ interface ProvidedClientTransportOptions extends ProvidedTransportOptions {
314
+ connectionRetryOptions?: Partial<ConnectionRetryOptions>;
315
+ }
316
+ interface ClientTransportOptions extends Required<ProvidedClientTransportOptions> {
317
+ connectionRetryOptions: ConnectionRetryOptions;
318
+ }
250
319
  /**
251
320
  * Transports manage the lifecycle (creation/deletion) of sessions and connections. Its responsibilities include:
252
321
  *
@@ -320,7 +389,7 @@ declare abstract class Transport<ConnType extends Connection> {
320
389
  * @param codec The codec used to encode and decode messages.
321
390
  * @param clientId The client ID of this transport.
322
391
  */
323
- constructor(clientId: TransportClientId, providedOptions?: Partial<TransportOptions>);
392
+ constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions);
324
393
  /**
325
394
  * This is called immediately after a new connection is established and we
326
395
  * may or may not know the identity of the connected client.
@@ -391,12 +460,17 @@ declare abstract class Transport<ConnType extends Connection> {
391
460
  destroy(): void;
392
461
  }
393
462
  declare abstract class ClientTransport<ConnType extends Connection> extends Transport<ConnType> {
463
+ /**
464
+ * The options for this transport.
465
+ */
466
+ protected options: ClientTransportOptions;
394
467
  /**
395
468
  * The map of reconnect promises for each client ID.
396
469
  */
397
470
  inflightConnectionPromises: Map<TransportClientId, Promise<ConnType>>;
471
+ retryBudget: LeakyBucketRateLimit;
398
472
  tryReconnecting: boolean;
399
- constructor(clientId: TransportClientId, providedOptions?: Partial<TransportOptions>);
473
+ constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions);
400
474
  protected handleConnection(conn: ConnType, to: TransportClientId): void;
401
475
  receiveHandshakeResponseMessage(data: Uint8Array): false | {
402
476
  instanceId: string;
@@ -415,12 +489,12 @@ declare abstract class ClientTransport<ConnType extends Connection> extends Tran
415
489
  * Manually attempts to connect to a client.
416
490
  * @param to The client ID of the node to connect to.
417
491
  */
418
- connect(to: TransportClientId, attempt?: number): Promise<void>;
492
+ connect(to: TransportClientId): Promise<void>;
419
493
  protected sendHandshake(to: TransportClientId, conn: ConnType): void;
420
- protected onDisconnect(conn: ConnType, session: Session<ConnType>): void;
494
+ close(): void;
421
495
  }
422
496
  declare abstract class ServerTransport<ConnType extends Connection> extends Transport<ConnType> {
423
- constructor(clientId: TransportClientId, providedOptions?: Partial<TransportOptions>);
497
+ constructor(clientId: TransportClientId, providedOptions?: Omit<Partial<ProvidedTransportOptions>, 'connectionRetryOptions'>);
424
498
  protected handleConnection(conn: ConnType): void;
425
499
  receiveHandshakeRequestMessage(data: Uint8Array, conn: ConnType): false | {
426
500
  instanceId: string;
@@ -428,4 +502,4 @@ declare abstract class ServerTransport<ConnType extends Connection> extends Tran
428
502
  };
429
503
  }
430
504
 
431
- export { Connection as C, EventMap as E, OpaqueTransportMessage as O, PartialTransportMessage as P, ServerTransport as S, Transport as T, ClientTransport as a, TransportClientId as b, TransportOptions as c, Session as d, TransportStatus as e, TransportMessageSchema as f, OpaqueTransportMessageSchema as g, TransportMessage as h, isStreamOpen as i, isStreamClose as j, EventTypes as k, EventHandler as l, ProtocolErrorType as m };
505
+ export { Connection as C, EventMap as E, OpaqueTransportMessage as O, PartialTransportMessage as P, ServerTransport as S, Transport as T, ClientTransport as a, TransportClientId as b, ProvidedClientTransportOptions as c, ProvidedTransportOptions as d, Session as e, TransportStatus as f, TransportMessageSchema as g, OpaqueTransportMessageSchema as h, TransportMessage as i, isStreamOpen as j, isStreamClose as k, EventTypes as l, EventHandler as m, ProtocolError as n, ProtocolErrorType as o };