@replit/river 0.15.1 → 0.15.3
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 +41 -22
- package/dist/{builder-660d3140.d.ts → builder-4d392f6c.d.ts} +1 -1
- package/dist/{chunk-O6YQ3JAH.js → chunk-5XMMDOLH.js} +148 -37
- package/dist/{chunk-MNWOTQWX.js → chunk-MXHIHC3U.js} +1 -1
- package/dist/{chunk-5TX4BKAD.js → chunk-NSJVTNVQ.js} +1 -1
- package/dist/{connection-162c0f7b.d.ts → connection-94896f3b.d.ts} +1 -1
- package/dist/{connection-93daccc3.d.ts → connection-99346822.d.ts} +1 -1
- package/dist/{index-76b801f8.d.ts → index-2e402bb8.d.ts} +84 -11
- package/dist/router/index.d.cts +3 -3
- package/dist/router/index.d.ts +3 -3
- package/dist/transport/impls/uds/client.cjs +198 -88
- package/dist/transport/impls/uds/client.d.cts +3 -3
- package/dist/transport/impls/uds/client.d.ts +3 -3
- package/dist/transport/impls/uds/client.js +2 -2
- package/dist/transport/impls/uds/server.cjs +67 -69
- package/dist/transport/impls/uds/server.d.cts +3 -3
- package/dist/transport/impls/uds/server.d.ts +3 -3
- package/dist/transport/impls/uds/server.js +2 -2
- package/dist/transport/impls/ws/client.cjs +198 -90
- package/dist/transport/impls/ws/client.d.cts +3 -3
- package/dist/transport/impls/ws/client.d.ts +3 -3
- package/dist/transport/impls/ws/client.js +2 -2
- package/dist/transport/impls/ws/server.cjs +67 -69
- package/dist/transport/impls/ws/server.d.cts +3 -3
- package/dist/transport/impls/ws/server.d.ts +3 -3
- package/dist/transport/impls/ws/server.js +2 -2
- package/dist/transport/index.cjs +198 -90
- package/dist/transport/index.d.cts +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/util/testHelpers.cjs +61 -64
- package/dist/util/testHelpers.d.cts +10 -4
- package/dist/util/testHelpers.d.ts +10 -4
- package/dist/util/testHelpers.js +13 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,36 +1,60 @@
|
|
|
1
1
|
# River
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
161
|
+
### Connection status
|
|
138
162
|
|
|
139
|
-
River
|
|
163
|
+
River defines two types of reconnects:
|
|
140
164
|
|
|
141
|
-
1. Transparent reconnects
|
|
142
|
-
2. Hard reconnect
|
|
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
|
-
|
|
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
|
|
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,
|
|
3
|
+
import { b as TransportClientId, e as Session, C as Connection } from './index-2e402bb8.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The context for services/procedures. This is used only on
|
|
@@ -58,15 +58,6 @@ var Connection = class {
|
|
|
58
58
|
this.debugId = `conn-${unsafeId()}`;
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
-
var HEARTBEAT_INTERVAL_MS = 1e3;
|
|
62
|
-
var HEARTBEATS_TILL_DEAD = 2;
|
|
63
|
-
var SESSION_DISCONNECT_GRACE_MS = 5e3;
|
|
64
|
-
var defaultSessionOptions = {
|
|
65
|
-
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
|
|
66
|
-
heartbeatsUntilDead: HEARTBEATS_TILL_DEAD,
|
|
67
|
-
sessionDisconnectGraceMs: SESSION_DISCONNECT_GRACE_MS,
|
|
68
|
-
codec: NaiveJsonCodec
|
|
69
|
-
};
|
|
70
61
|
var Session = class {
|
|
71
62
|
codec;
|
|
72
63
|
options;
|
|
@@ -253,13 +244,96 @@ var Session = class {
|
|
|
253
244
|
// transport/transport.ts
|
|
254
245
|
import { Value } from "@sinclair/typebox/value";
|
|
255
246
|
import { nanoid as nanoid2 } from "nanoid";
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
321
|
var defaultTransportOptions = {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
322
|
+
heartbeatIntervalMs: 1e3,
|
|
323
|
+
heartbeatsUntilDead: 2,
|
|
324
|
+
sessionDisconnectGraceMs: 5e3,
|
|
325
|
+
codec: NaiveJsonCodec
|
|
326
|
+
};
|
|
327
|
+
var defaultConnectionRetryOptions = {
|
|
328
|
+
baseIntervalMs: 250,
|
|
329
|
+
maxJitterMs: 200,
|
|
330
|
+
maxBackoffMs: 32e3,
|
|
331
|
+
attemptBudgetCapacity: 15,
|
|
332
|
+
budgetRestoreIntervalMs: 200
|
|
333
|
+
};
|
|
334
|
+
var defaultClientTransportOptions = {
|
|
335
|
+
connectionRetryOptions: defaultConnectionRetryOptions,
|
|
336
|
+
...defaultTransportOptions
|
|
263
337
|
};
|
|
264
338
|
var Transport = class {
|
|
265
339
|
/**
|
|
@@ -534,14 +608,26 @@ var Transport = class {
|
|
|
534
608
|
}
|
|
535
609
|
};
|
|
536
610
|
var ClientTransport = class extends Transport {
|
|
611
|
+
/**
|
|
612
|
+
* The options for this transport.
|
|
613
|
+
*/
|
|
614
|
+
options;
|
|
537
615
|
/**
|
|
538
616
|
* The map of reconnect promises for each client ID.
|
|
539
617
|
*/
|
|
540
618
|
inflightConnectionPromises;
|
|
619
|
+
retryBudget;
|
|
541
620
|
tryReconnecting = true;
|
|
542
621
|
constructor(clientId, providedOptions) {
|
|
543
622
|
super(clientId, providedOptions);
|
|
623
|
+
this.options = {
|
|
624
|
+
...defaultClientTransportOptions,
|
|
625
|
+
...providedOptions
|
|
626
|
+
};
|
|
544
627
|
this.inflightConnectionPromises = /* @__PURE__ */ new Map();
|
|
628
|
+
this.retryBudget = new LeakyBucketRateLimit(
|
|
629
|
+
this.options.connectionRetryOptions
|
|
630
|
+
);
|
|
545
631
|
}
|
|
546
632
|
handleConnection(conn, to) {
|
|
547
633
|
if (this.state !== "open")
|
|
@@ -572,7 +658,10 @@ var ClientTransport = class extends Transport {
|
|
|
572
658
|
log?.info(
|
|
573
659
|
`${this.clientId} -- connection (id: ${conn.debugId}) to ${to} disconnected`
|
|
574
660
|
);
|
|
575
|
-
|
|
661
|
+
this.inflightConnectionPromises.delete(to);
|
|
662
|
+
if (this.tryReconnecting) {
|
|
663
|
+
void this.connect(to);
|
|
664
|
+
}
|
|
576
665
|
});
|
|
577
666
|
conn.addErrorListener((err) => {
|
|
578
667
|
log?.warn(
|
|
@@ -623,41 +712,64 @@ var ClientTransport = class extends Transport {
|
|
|
623
712
|
* Manually attempts to connect to a client.
|
|
624
713
|
* @param to The client ID of the node to connect to.
|
|
625
714
|
*/
|
|
626
|
-
async connect(to
|
|
627
|
-
|
|
715
|
+
async connect(to) {
|
|
716
|
+
const canProceedWithConnection = () => this.state === "open";
|
|
717
|
+
if (!canProceedWithConnection()) {
|
|
628
718
|
log?.info(
|
|
629
|
-
`${this.clientId} -- transport state is no longer open,
|
|
719
|
+
`${this.clientId} -- transport state is no longer open, cancelling attempt to connect to ${to}`
|
|
630
720
|
);
|
|
631
721
|
return;
|
|
632
722
|
}
|
|
633
723
|
let reconnectPromise = this.inflightConnectionPromises.get(to);
|
|
634
724
|
if (!reconnectPromise) {
|
|
635
|
-
|
|
725
|
+
log?.info(`${this.clientId} -- attempting connection to ${to}`);
|
|
726
|
+
const budgetConsumed = this.retryBudget.getBudgetConsumed(to);
|
|
727
|
+
if (!this.retryBudget.hasBudget(to)) {
|
|
728
|
+
const errMsg = `not attempting to connect to ${to}, retry budget exceeded (more than ${budgetConsumed} attempts in the last ${this.retryBudget.totalBudgetRestoreTime}ms)`;
|
|
729
|
+
log?.warn(`${this.clientId} -- ${errMsg}`);
|
|
730
|
+
this.protocolError(ProtocolError.RetriesExceeded, errMsg);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
let sleep = Promise.resolve();
|
|
734
|
+
const backoffMs = this.retryBudget.getBackoffMs(to);
|
|
735
|
+
if (backoffMs > 0) {
|
|
736
|
+
sleep = new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
737
|
+
}
|
|
738
|
+
this.retryBudget.consumeBudget(to);
|
|
739
|
+
reconnectPromise = sleep.then(() => {
|
|
740
|
+
if (!canProceedWithConnection()) {
|
|
741
|
+
throw new Error("transport state is no longer open");
|
|
742
|
+
}
|
|
743
|
+
}).then(() => this.createNewOutgoingConnection(to)).then((conn) => {
|
|
744
|
+
if (!canProceedWithConnection()) {
|
|
745
|
+
log?.info(
|
|
746
|
+
`${this.clientId} -- transport state is no longer open, closing pre-handshake connection (id: ${conn.debugId}) to ${to}`
|
|
747
|
+
);
|
|
748
|
+
conn.close();
|
|
749
|
+
throw new Error("transport state is no longer open");
|
|
750
|
+
}
|
|
751
|
+
this.retryBudget.startRestoringBudget(to);
|
|
636
752
|
this.sendHandshake(to, conn);
|
|
637
753
|
return conn;
|
|
638
754
|
});
|
|
639
755
|
this.inflightConnectionPromises.set(to, reconnectPromise);
|
|
756
|
+
} else {
|
|
757
|
+
log?.info(
|
|
758
|
+
`${this.clientId} -- attempting connection to ${to} (reusing previous attempt)`
|
|
759
|
+
);
|
|
640
760
|
}
|
|
641
761
|
try {
|
|
642
762
|
await reconnectPromise;
|
|
643
763
|
} catch (error) {
|
|
644
|
-
const errStr = coerceErrorString(error);
|
|
645
764
|
this.inflightConnectionPromises.delete(to);
|
|
646
|
-
const
|
|
647
|
-
if (!
|
|
648
|
-
|
|
649
|
-
if (attempt >= this.options.retryAttemptsMax) {
|
|
650
|
-
const errMsg = `connection to ${to} failed after ${attempt} attempts (${errStr}), giving up`;
|
|
651
|
-
log?.error(`${this.clientId} -- ${errMsg}`);
|
|
652
|
-
this.protocolError(ProtocolError.RetriesExceeded, errMsg);
|
|
653
|
-
return;
|
|
765
|
+
const errStr = coerceErrorString(error);
|
|
766
|
+
if (!this.tryReconnecting || !canProceedWithConnection()) {
|
|
767
|
+
log?.warn(`${this.clientId} -- connection to ${to} failed (${errStr})`);
|
|
654
768
|
} else {
|
|
655
|
-
const jitter = Math.floor(Math.random() * this.options.retryJitterMs);
|
|
656
|
-
const backoffMs = this.options.retryIntervalMs * 2 ** attempt + jitter;
|
|
657
769
|
log?.warn(
|
|
658
|
-
`${this.clientId} -- connection to ${to} failed (${errStr}),
|
|
770
|
+
`${this.clientId} -- connection to ${to} failed (${errStr}), retrying`
|
|
659
771
|
);
|
|
660
|
-
|
|
772
|
+
return this.connect(to);
|
|
661
773
|
}
|
|
662
774
|
}
|
|
663
775
|
}
|
|
@@ -670,9 +782,9 @@ var ClientTransport = class extends Transport {
|
|
|
670
782
|
log?.debug(`${this.clientId} -- sending handshake request to ${to}`);
|
|
671
783
|
conn.send(this.codec.toBuffer(requestMsg));
|
|
672
784
|
}
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
super.
|
|
785
|
+
close() {
|
|
786
|
+
this.retryBudget.close();
|
|
787
|
+
super.close();
|
|
676
788
|
}
|
|
677
789
|
};
|
|
678
790
|
var ServerTransport = class extends Transport {
|
|
@@ -788,7 +900,6 @@ var ServerTransport = class extends Transport {
|
|
|
788
900
|
export {
|
|
789
901
|
ProtocolError,
|
|
790
902
|
Connection,
|
|
791
|
-
defaultSessionOptions,
|
|
792
903
|
Session,
|
|
793
904
|
Transport,
|
|
794
905
|
ClientTransport,
|
|
@@ -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
|
/**
|
|
@@ -236,6 +249,58 @@ declare class EventDispatcher<T extends EventTypes> {
|
|
|
236
249
|
dispatchEvent<K extends T>(eventType: K, event: EventMap[K]): void;
|
|
237
250
|
}
|
|
238
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
|
+
|
|
239
304
|
/**
|
|
240
305
|
* Represents the possible states of a transport.
|
|
241
306
|
* @property {'open'} open - The transport is open and operational (note that this doesn't mean it is actively connected)
|
|
@@ -243,11 +308,14 @@ declare class EventDispatcher<T extends EventTypes> {
|
|
|
243
308
|
* @property {'destroyed'} destroyed - The transport is permanently destroyed and cannot be reopened.
|
|
244
309
|
*/
|
|
245
310
|
type TransportStatus = 'open' | 'closed' | 'destroyed';
|
|
246
|
-
type
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
} &
|
|
311
|
+
type ProvidedTransportOptions = Partial<SessionOptions>;
|
|
312
|
+
type TransportOptions = Required<ProvidedTransportOptions>;
|
|
313
|
+
type ProvidedClientTransportOptions = {
|
|
314
|
+
connectionRetryOptions?: Partial<ConnectionRetryOptions>;
|
|
315
|
+
} & ProvidedTransportOptions;
|
|
316
|
+
type ClientTransportOptions = SessionOptions & {
|
|
317
|
+
connectionRetryOptions: ConnectionRetryOptions;
|
|
318
|
+
};
|
|
251
319
|
/**
|
|
252
320
|
* Transports manage the lifecycle (creation/deletion) of sessions and connections. Its responsibilities include:
|
|
253
321
|
*
|
|
@@ -321,7 +389,7 @@ declare abstract class Transport<ConnType extends Connection> {
|
|
|
321
389
|
* @param codec The codec used to encode and decode messages.
|
|
322
390
|
* @param clientId The client ID of this transport.
|
|
323
391
|
*/
|
|
324
|
-
constructor(clientId: TransportClientId, providedOptions?:
|
|
392
|
+
constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions);
|
|
325
393
|
/**
|
|
326
394
|
* This is called immediately after a new connection is established and we
|
|
327
395
|
* may or may not know the identity of the connected client.
|
|
@@ -392,12 +460,17 @@ declare abstract class Transport<ConnType extends Connection> {
|
|
|
392
460
|
destroy(): void;
|
|
393
461
|
}
|
|
394
462
|
declare abstract class ClientTransport<ConnType extends Connection> extends Transport<ConnType> {
|
|
463
|
+
/**
|
|
464
|
+
* The options for this transport.
|
|
465
|
+
*/
|
|
466
|
+
protected options: ClientTransportOptions;
|
|
395
467
|
/**
|
|
396
468
|
* The map of reconnect promises for each client ID.
|
|
397
469
|
*/
|
|
398
470
|
inflightConnectionPromises: Map<TransportClientId, Promise<ConnType>>;
|
|
471
|
+
retryBudget: LeakyBucketRateLimit;
|
|
399
472
|
tryReconnecting: boolean;
|
|
400
|
-
constructor(clientId: TransportClientId, providedOptions?:
|
|
473
|
+
constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions);
|
|
401
474
|
protected handleConnection(conn: ConnType, to: TransportClientId): void;
|
|
402
475
|
receiveHandshakeResponseMessage(data: Uint8Array): false | {
|
|
403
476
|
instanceId: string;
|
|
@@ -416,12 +489,12 @@ declare abstract class ClientTransport<ConnType extends Connection> extends Tran
|
|
|
416
489
|
* Manually attempts to connect to a client.
|
|
417
490
|
* @param to The client ID of the node to connect to.
|
|
418
491
|
*/
|
|
419
|
-
connect(to: TransportClientId
|
|
492
|
+
connect(to: TransportClientId): Promise<void>;
|
|
420
493
|
protected sendHandshake(to: TransportClientId, conn: ConnType): void;
|
|
421
|
-
|
|
494
|
+
close(): void;
|
|
422
495
|
}
|
|
423
496
|
declare abstract class ServerTransport<ConnType extends Connection> extends Transport<ConnType> {
|
|
424
|
-
constructor(clientId: TransportClientId, providedOptions?:
|
|
497
|
+
constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions);
|
|
425
498
|
protected handleConnection(conn: ConnType): void;
|
|
426
499
|
receiveHandshakeRequestMessage(data: Uint8Array, conn: ConnType): false | {
|
|
427
500
|
instanceId: string;
|
|
@@ -429,4 +502,4 @@ declare abstract class ServerTransport<ConnType extends Connection> extends Tran
|
|
|
429
502
|
};
|
|
430
503
|
}
|
|
431
504
|
|
|
432
|
-
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,
|
|
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 };
|
package/dist/router/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AnyService, P as PayloadType, b as Result, R as RiverError, S as ServiceContext, d as ProcType, e as ProcInput, f as ProcOutput, g as ProcErrors, h as ProcHasInit, i as ProcInit } from '../builder-
|
|
2
|
-
export { E as Err, O as Ok, m as ProcHandler, k as ProcListing, a as Procedure, p as RiverErrorSchema, c as RiverUncaughtSchema, l as Service, j as ServiceBuilder, n as ServiceContextWithState, o as ServiceContextWithTransportInfo, U as UNCAUGHT_ERROR, V as ValidProcType, s as serializeService } from '../builder-
|
|
3
|
-
import { S as ServerTransport, C as Connection, a as ClientTransport, b as TransportClientId } from '../index-
|
|
1
|
+
import { A as AnyService, P as PayloadType, b as Result, R as RiverError, S as ServiceContext, d as ProcType, e as ProcInput, f as ProcOutput, g as ProcErrors, h as ProcHasInit, i as ProcInit } from '../builder-4d392f6c.js';
|
|
2
|
+
export { E as Err, O as Ok, m as ProcHandler, k as ProcListing, a as Procedure, p as RiverErrorSchema, c as RiverUncaughtSchema, l as Service, j as ServiceBuilder, n as ServiceContextWithState, o as ServiceContextWithTransportInfo, U as UNCAUGHT_ERROR, V as ValidProcType, s as serializeService } from '../builder-4d392f6c.js';
|
|
3
|
+
import { S as ServerTransport, C as Connection, a as ClientTransport, b as TransportClientId } from '../index-2e402bb8.js';
|
|
4
4
|
import { Pushable } from 'it-pushable';
|
|
5
5
|
import { Static } from '@sinclair/typebox';
|
|
6
6
|
import '../types-3e5768ec.js';
|
package/dist/router/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AnyService, P as PayloadType, b as Result, R as RiverError, S as ServiceContext, d as ProcType, e as ProcInput, f as ProcOutput, g as ProcErrors, h as ProcHasInit, i as ProcInit } from '../builder-
|
|
2
|
-
export { E as Err, O as Ok, m as ProcHandler, k as ProcListing, a as Procedure, p as RiverErrorSchema, c as RiverUncaughtSchema, l as Service, j as ServiceBuilder, n as ServiceContextWithState, o as ServiceContextWithTransportInfo, U as UNCAUGHT_ERROR, V as ValidProcType, s as serializeService } from '../builder-
|
|
3
|
-
import { S as ServerTransport, C as Connection, a as ClientTransport, b as TransportClientId } from '../index-
|
|
1
|
+
import { A as AnyService, P as PayloadType, b as Result, R as RiverError, S as ServiceContext, d as ProcType, e as ProcInput, f as ProcOutput, g as ProcErrors, h as ProcHasInit, i as ProcInit } from '../builder-4d392f6c.js';
|
|
2
|
+
export { E as Err, O as Ok, m as ProcHandler, k as ProcListing, a as Procedure, p as RiverErrorSchema, c as RiverUncaughtSchema, l as Service, j as ServiceBuilder, n as ServiceContextWithState, o as ServiceContextWithTransportInfo, U as UNCAUGHT_ERROR, V as ValidProcType, s as serializeService } from '../builder-4d392f6c.js';
|
|
3
|
+
import { S as ServerTransport, C as Connection, a as ClientTransport, b as TransportClientId } from '../index-2e402bb8.js';
|
|
4
4
|
import { Pushable } from 'it-pushable';
|
|
5
5
|
import { Static } from '@sinclair/typebox';
|
|
6
6
|
import '../types-3e5768ec.js';
|