@actuallab/rpc 14.3.16 → 14.3.32
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 +91 -0
- package/dist/index.cjs +27 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @actuallab/rpc
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@actuallab/rpc)
|
|
4
|
+
[](https://fusion.actuallab.net/PartTS-Rpc)
|
|
5
|
+
[](https://github.com/ActualLab/Fusion/blob/master/LICENSE)
|
|
6
|
+
|
|
7
|
+
TypeScript client (and Node.js host) for
|
|
8
|
+
[`ActualLab.Rpc`](https://fusion.actuallab.net/PartR) — the WebSocket RPC layer behind
|
|
9
|
+
[Fusion](https://fusion.actuallab.net/). It speaks the same wire protocol as the .NET
|
|
10
|
+
implementation, and handles connection management, serialization, streaming, system calls, and
|
|
11
|
+
automatic reconnection for you.
|
|
12
|
+
|
|
13
|
+
Use it on its own for plain RPC. For compute calls with server-driven invalidation, use
|
|
14
|
+
[`@actuallab/fusion-rpc`](https://www.npmjs.com/package/@actuallab/fusion-rpc), which extends this
|
|
15
|
+
package's `RpcHub`.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @actuallab/rpc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
ESM-first with a CJS fallback; ships its own `.d.ts`. Works in browsers and in Node.js (supply a
|
|
24
|
+
`ws` WebSocket via `peer.webSocketFactory`).
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { RpcHub, RpcClientPeer, defineRpcService, RpcType } from '@actuallab/rpc';
|
|
30
|
+
|
|
31
|
+
// The name must match the .NET interface name exactly.
|
|
32
|
+
const SimpleServiceDef = defineRpcService('ISimpleService', {
|
|
33
|
+
Greet: { args: [''] }, // (message) → string
|
|
34
|
+
Counter: { args: [], returns: RpcType.stream }, // () → RpcStream<number>
|
|
35
|
+
Ping: { args: [''], returns: RpcType.noWait }, // fire-and-forget
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const hub = new RpcHub();
|
|
39
|
+
const peer = new RpcClientPeer(hub, 'ws://localhost:5005/rpc/ws'); // auto-starts
|
|
40
|
+
hub.addPeer(peer); // registers it, so hub.close() shuts it down too
|
|
41
|
+
const client = hub.addClient<ISimpleService>(peer, SimpleServiceDef);
|
|
42
|
+
|
|
43
|
+
console.log(await client.Greet('World'));
|
|
44
|
+
|
|
45
|
+
for await (const i of await client.Counter()) {
|
|
46
|
+
if (i > 10) break; // sends AckEnd to the server
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
client.Ping('hello'); // returns void, dropped while disconnected
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`args` is only read for its `length` — the values are placeholders that document the signature.
|
|
53
|
+
Services can also be declared with the `@rpcService` / `@rpcMethod` decorators, which work for both
|
|
54
|
+
clients and server-side implementations.
|
|
55
|
+
|
|
56
|
+
## API surface
|
|
57
|
+
|
|
58
|
+
| API | Description |
|
|
59
|
+
|-----|-------------|
|
|
60
|
+
| `RpcHub` | Peers, registered services, shared reconnect delayer, `addClient` / `addService` / `close` |
|
|
61
|
+
| `RpcClientPeer` | A client connection with handshake, buffering, and exponential-backoff reconnect (100 ms → 10 s) |
|
|
62
|
+
| `RpcServerPeer`, `RpcWebSocketConnection` | Hosting side: accept an inbound WebSocket |
|
|
63
|
+
| `defineRpcService`, `rpcService`, `rpcMethod`, `RpcType` | Service definitions |
|
|
64
|
+
| `RpcStream<T>`, `toRpcStream` | `AsyncIterable<T>` streams with ack-based backpressure and resume-after-reconnect |
|
|
65
|
+
| `RpcPeerStateMonitor`, `RpcPeerStateKind` | Connection-status state machine with `JustConnected` / `JustDisconnected` grace periods, for UI |
|
|
66
|
+
| `RpcCallTimeouts`, `RpcLimits` | Per-call timeouts and connection-lifecycle timing (keep-alive, handshake, …) |
|
|
67
|
+
| `genericErrorFilter` | Replaces outbound error messages with a generic one when your host serves RPC |
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- **Reconnection is automatic.** The delayer lives on the hub and is shared by all client peers;
|
|
72
|
+
`hub.reconnectDelayer.cancelDelays()` forces an immediate retry (useful for a "Reconnect now"
|
|
73
|
+
button). A connection that drops within 15 s of connecting doesn't reset the backoff.
|
|
74
|
+
- **Serialization.** `json5np` (default) or MessagePack (`msgpack6` / `msgpack6c`), selected with
|
|
75
|
+
the `?f=` URL key. MemoryPack and polymorphic payloads are not supported — they fail loudly.
|
|
76
|
+
- **Node.js.** Construct the peer with `mustStart = false`, set `peer.webSocketFactory`, then call
|
|
77
|
+
`peer.start()`.
|
|
78
|
+
- **Security.** Serve RPC over `https`/`wss`: reconnect proofs need `crypto.subtle`, which is
|
|
79
|
+
unavailable in insecure browser contexts. If your Node host *serves* RPC, consider
|
|
80
|
+
`hub.systemCallSender.errorFilter = genericErrorFilter` so handler exception text (paths,
|
|
81
|
+
connection strings) doesn't reach callers.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- [`@actuallab/rpc` reference](https://fusion.actuallab.net/PartTS-Rpc) — full API tables, reconnect proof, wire protocol
|
|
86
|
+
- [TypeScript port overview](https://fusion.actuallab.net/PartTS)
|
|
87
|
+
- [ActualLab.Rpc](https://fusion.actuallab.net/PartR) and [RpcStream](https://fusion.actuallab.net/PartR-RpcStream) — the concepts, in .NET terms
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT — see [LICENSE](https://github.com/ActualLab/Fusion/blob/master/LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -1285,6 +1285,15 @@ var RpcSystemCallHandler = class {
|
|
|
1285
1285
|
);
|
|
1286
1286
|
return;
|
|
1287
1287
|
}
|
|
1288
|
+
if (!peer.tryClaimReconnect()) {
|
|
1289
|
+
peer.hub.systemCallSender.error(
|
|
1290
|
+
peer.connection,
|
|
1291
|
+
peer.serializationFormat,
|
|
1292
|
+
relatedId,
|
|
1293
|
+
new Error("TooLateToReconnect: this connection was already reconnected")
|
|
1294
|
+
);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1288
1297
|
let unknownIds = [];
|
|
1289
1298
|
try {
|
|
1290
1299
|
const stagesRaw = args[1];
|
|
@@ -2749,6 +2758,9 @@ var RpcPeer = class {
|
|
|
2749
2758
|
* stale `$sys.Reconnect` that references an older connection generation
|
|
2750
2759
|
* (C# `RpcSystemCalls.Reconnect`'s `ownHandshake.Index` check). */
|
|
2751
2760
|
_ownHandshakeIndex = 0;
|
|
2761
|
+
/** Whether the current connection generation has already used its single
|
|
2762
|
+
* `$sys.Reconnect`. Mirrors C# `RpcPeerConnectionState.TryClaimReconnect`. */
|
|
2763
|
+
_isReconnectClaimed = false;
|
|
2752
2764
|
_connection;
|
|
2753
2765
|
/** Mirror of `_connectionState === Connected`. True only after the
|
|
2754
2766
|
* handshake round-trip completes; outbound `call()` checks this to
|
|
@@ -2819,6 +2831,19 @@ var RpcPeer = class {
|
|
|
2819
2831
|
get ownHandshakeIndex() {
|
|
2820
2832
|
return this._ownHandshakeIndex;
|
|
2821
2833
|
}
|
|
2834
|
+
// A conforming peer sends `$sys.Reconnect` at most once per connection, so anything
|
|
2835
|
+
// beyond that is a replay - and the reconciliation it performs is proportional to the
|
|
2836
|
+
// call ids it carries, with nothing else capping how often it can arrive.
|
|
2837
|
+
tryClaimReconnect() {
|
|
2838
|
+
if (this._isReconnectClaimed)
|
|
2839
|
+
return false;
|
|
2840
|
+
this._isReconnectClaimed = true;
|
|
2841
|
+
return true;
|
|
2842
|
+
}
|
|
2843
|
+
nextOwnHandshakeIndex() {
|
|
2844
|
+
this._isReconnectClaimed = false;
|
|
2845
|
+
return ++this._ownHandshakeIndex;
|
|
2846
|
+
}
|
|
2822
2847
|
_setConnectionState(value) {
|
|
2823
2848
|
if (this._connectionState === value) return;
|
|
2824
2849
|
const previousState = this._connectionState;
|
|
@@ -3354,7 +3379,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3354
3379
|
this.serializationFormat,
|
|
3355
3380
|
this.id,
|
|
3356
3381
|
this.hub.hubId,
|
|
3357
|
-
|
|
3382
|
+
this.nextOwnHandshakeIndex()
|
|
3358
3383
|
);
|
|
3359
3384
|
} catch (e) {
|
|
3360
3385
|
warnLog7?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
@@ -3619,7 +3644,7 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
3619
3644
|
this.serializationFormat,
|
|
3620
3645
|
this.id,
|
|
3621
3646
|
this.hub.hubId,
|
|
3622
|
-
|
|
3647
|
+
this.nextOwnHandshakeIndex()
|
|
3623
3648
|
);
|
|
3624
3649
|
this._setConnectionState(3 /* Connected */);
|
|
3625
3650
|
}
|