capnweb 0.1.0 → 0.2.0
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 +71 -8
- package/dist/index-workers.cjs +2652 -0
- package/dist/index-workers.cjs.map +1 -0
- package/dist/index-workers.d.cts +2 -0
- package/dist/index-workers.d.ts +2 -0
- package/dist/index-workers.js +2618 -0
- package/dist/index-workers.js.map +1 -0
- package/dist/index.cjs +2629 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +364 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/package.json +20 -9
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ Cap'n Web is more expressive than almost every other RPC system, because it impl
|
|
|
16
16
|
* Supports promise pipelining. When you start an RPC, you get back a promise. Instead of awaiting it, you can immediately use the promise in dependent RPCs, thus performing a chain of calls in a single network round trip.
|
|
17
17
|
* Supports capability-based security patterns.
|
|
18
18
|
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
[Cap'n Web is an npm package.](https://www.npmjs.com/package/capnweb)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npm i capnweb
|
|
25
|
+
```
|
|
26
|
+
|
|
19
27
|
## Example
|
|
20
28
|
|
|
21
29
|
A client looks like this:
|
|
@@ -138,7 +146,7 @@ let friendsPromise = authedApi.getFriendIds();
|
|
|
138
146
|
// too, so use the magic .map() function to get them, too! Still one round
|
|
139
147
|
// trip.
|
|
140
148
|
let friendProfilesPromise = friendsPromise.map((id: RpcPromise<number>) => {
|
|
141
|
-
return { id, profile: api.getUserProfile(id)
|
|
149
|
+
return { id, profile: api.getUserProfile(id) };
|
|
142
150
|
});
|
|
143
151
|
|
|
144
152
|
// Now await the promises. The batch is sent at this point. It's important
|
|
@@ -165,7 +173,7 @@ import { newWebSocketRpcSession } from "capnweb";
|
|
|
165
173
|
// feature, part of the "explicit resource management" spec. Alternatively,
|
|
166
174
|
// we could declare `api` with `let` or `const` and make sure to call
|
|
167
175
|
// `api[Symbol.dispose]()` to dispose it and close the connection later.
|
|
168
|
-
using api = newWebSocketRpcSession<PublicApi>("
|
|
176
|
+
using api = newWebSocketRpcSession<PublicApi>("wss://example.com/api");
|
|
169
177
|
|
|
170
178
|
// Usage is exactly the same, except we don't have to await all the promises
|
|
171
179
|
// at once.
|
|
@@ -297,13 +305,30 @@ The trick here is record-replay: On the calling side, Cap'n Web will invoke your
|
|
|
297
305
|
|
|
298
306
|
Since all of the not-yet-determined values seen by the callback are represented as `RpcPromise`s, the callback's behavior is deterministic. Any actual computation (arithmetic, branching, etc.) can't possibly use these promises as (meaningful) inputs, so would logically produce the same results for every invocation of the callback. Any such computation will actually end up being performed on the sending side, just once, with the results being imbued into the recording.
|
|
299
307
|
|
|
308
|
+
### Cloudflare Workers RPC interoperability
|
|
309
|
+
|
|
310
|
+
Cap'n Web works on any JavaScript platform. But, on Cloudflare Workers specifically, it's designed to play nicely with the [the built-in RPC system](https://blog.cloudflare.com/javascript-native-rpc/). The two have basically the same semantics, the only difference being that Workers RPC is a built-in API provided by the Workers Runtime, whereas Cap'n Web is implemented in pure JavaScript.
|
|
311
|
+
|
|
312
|
+
To facilitate interoperability:
|
|
313
|
+
* On Workers, the `RpcTarget` class exported by "capnweb" is just an alias of the built-in one, so you can use them interchangeably.
|
|
314
|
+
* RPC stubs and promises originating from one RPC system can be passed over the other. This will automatically set up proxying.
|
|
315
|
+
* You can also send Workers Service Bindings and Durable Object stubs over Cap'n Web -- again, this sets up proxying.
|
|
316
|
+
|
|
317
|
+
So basically, it "just works".
|
|
318
|
+
|
|
319
|
+
With that said, as of this writing, the feature set is not exactly the same between the two. We aim to fix this over time, by adding missing features to both sides until they match. In particular, as of this writing:
|
|
320
|
+
* Workers RPC supports some types that Cap'n Web does not yet, like `Map`, streams, etc.
|
|
321
|
+
* Workers RPC supports sending values that contain aliases and cycles. This can actually cause problems, so we actually plan to *remove* this feature from Workers RPC (with a compatibility flag, of course).
|
|
322
|
+
* Workers RPC does not yet support placing an `RpcPromise` into the parameters of a request, to be replaced by its resolution.
|
|
323
|
+
* Workers RPC does not yet support the magic `.map()` method.
|
|
324
|
+
|
|
300
325
|
## Resource Management and Disposal
|
|
301
326
|
|
|
302
327
|
Unfortunately, garbage collection does not work well when remote resources are involved, for two reasons:
|
|
303
328
|
|
|
304
329
|
1. Many JavaScript runtimes only run the garbage collector when they sense "memory pressure" -- if memory is not running low, then they figure there's no need to try to reclaim any. However, the runtime has no way to know if the other side of an RPC connection is suffering memory pressure.
|
|
305
330
|
|
|
306
|
-
2. Garbage collectors need to trace the full object graph in order to detect which objects are unreachable, especially when those objects contain cyclic
|
|
331
|
+
2. Garbage collectors need to trace the full object graph in order to detect which objects are unreachable, especially when those objects contain cyclic references. However, the garbage collector can only see local objects; it has no ability to trace through the remote graph to discover cycles that may cross RPC connections.
|
|
307
332
|
|
|
308
333
|
Both of these problems might be solvable with sufficient work, but the problem seems exceedingly difficult. We make no attempt to solve it in this library.
|
|
309
334
|
|
|
@@ -313,6 +338,8 @@ Instead, you may choose one of two strategies:
|
|
|
313
338
|
|
|
314
339
|
2. Use short-lived sessions. When the session ends, all stubs are implicitly disposed. In particular, when using HTTP batch request, there's generally no need to dispose stubs. When using long-lived WebSocket sessions, however, disposal may be important.
|
|
315
340
|
|
|
341
|
+
Note: We might extend Cap'n Web to use `FinalizationRegistry` to automatically dispose abandoned stubs in the future, but even if we do, it should not be relied upon, due to problems discussed above.
|
|
342
|
+
|
|
316
343
|
### How to dispose
|
|
317
344
|
|
|
318
345
|
Stubs integrate with JavaScript's [explicit resource management](https://v8.dev/features/explicit-resource-management), which became widely available in mid-2025 (and has been supported via transpilers and polyfills going back a few years earlier). In short:
|
|
@@ -328,7 +355,7 @@ The basic principle is: **The caller is responsible for disposing all stubs.** T
|
|
|
328
355
|
* Stubs passed in the params of a call remain property of the caller, and must be disposed by the caller, not by the callee.
|
|
329
356
|
* Stubs returned in the result of a call have their ownership transferred from the callee to the caller, and must be disposed by the caller.
|
|
330
357
|
|
|
331
|
-
In practice, though, the callee and caller do not actually share the same stubs. When stubs are passed over RPC, they are _duplicated_, and the
|
|
358
|
+
In practice, though, the callee and caller do not actually share the same stubs. When stubs are passed over RPC, they are _duplicated_, and the target object is only disposed when all duplicates of the stub are disposed. Thus, to achieve the rule that only the caller needs to dispose stubs, the RPC system implicitly disposes the callee's duplicates of all stubs when the call completes. That is:
|
|
332
359
|
* Any stubs the callee receives in the parameters are implicitly disposed when the call completes.
|
|
333
360
|
* Any stubs returned in the results are implicitly disposed some time after the call completes. (Specifically, the RPC system will dispose them once it knows there will be no more pipelined calls.)
|
|
334
361
|
|
|
@@ -355,7 +382,7 @@ Note that if you pass the same `RpcTarget` instance to RPC multiple times -- thu
|
|
|
355
382
|
|
|
356
383
|
### Listening for disconnect
|
|
357
384
|
|
|
358
|
-
You can monitor any stub for "
|
|
385
|
+
You can monitor any stub for "brokenness" with its `onRpcBroken()` method:
|
|
359
386
|
|
|
360
387
|
```ts
|
|
361
388
|
stub.onRpcBroken((error: any) => {
|
|
@@ -508,7 +535,7 @@ A server on Node.js is a bit more involved, due to the awkward handling of WebSo
|
|
|
508
535
|
```ts
|
|
509
536
|
import http from "node:http";
|
|
510
537
|
import { WebSocketServer } from 'ws'; // npm package
|
|
511
|
-
import { RpcTarget, newWebSocketRpcSession, nodeHttpBatchRpcResponse } from "
|
|
538
|
+
import { RpcTarget, newWebSocketRpcSession, nodeHttpBatchRpcResponse } from "capnweb";
|
|
512
539
|
|
|
513
540
|
class MyApiImpl extends RpcTarget implements MyApi {
|
|
514
541
|
// ... define API, same as above ...
|
|
@@ -524,7 +551,7 @@ httpServer = http.createServer(async (request, response) => {
|
|
|
524
551
|
// Accept Cap'n Web requests at `/api`.
|
|
525
552
|
if (request.url === "/api") {
|
|
526
553
|
try {
|
|
527
|
-
nodeHttpBatchRpcResponse(request, response, new MyApiImpl(), {
|
|
554
|
+
await nodeHttpBatchRpcResponse(request, response, new MyApiImpl(), {
|
|
528
555
|
// If you are accepting WebSockets, then you might as well accept cross-origin HTTP, since
|
|
529
556
|
// WebSockets always permit cross-origin request anyway. But, see security considerations
|
|
530
557
|
// for further discussion.
|
|
@@ -555,6 +582,42 @@ wsServer.on('connection', (ws) => {
|
|
|
555
582
|
httpServer.listen(8080);
|
|
556
583
|
```
|
|
557
584
|
|
|
585
|
+
### HTTP server on Deno
|
|
586
|
+
```ts
|
|
587
|
+
import {
|
|
588
|
+
newHttpBatchRpcResponse,
|
|
589
|
+
newWebSocketRpcSession,
|
|
590
|
+
RpcTarget,
|
|
591
|
+
} from "npm:capnweb";
|
|
592
|
+
|
|
593
|
+
// This is the server implementation.
|
|
594
|
+
class MyApiImpl extends RpcTarget implements MyApi {
|
|
595
|
+
// ... define API, same as above ...
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
Deno.serve(async (req) => {
|
|
599
|
+
const url = new URL(req.url);
|
|
600
|
+
if (url.pathname === "/api") {
|
|
601
|
+
if (req.headers.get("upgrade") === "websocket") {
|
|
602
|
+
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
603
|
+
socket.addEventListener("open", () => {
|
|
604
|
+
newWebSocketRpcSession(socket, new MyApiImpl());
|
|
605
|
+
});
|
|
606
|
+
return response;
|
|
607
|
+
} else {
|
|
608
|
+
const response = await newHttpBatchRpcResponse(req, new MyApiImpl());
|
|
609
|
+
// If you are accepting WebSockets, then you might as well accept cross-origin HTTP, since
|
|
610
|
+
// WebSockets always permit cross-origin request anyway. But, see security considerations
|
|
611
|
+
// for further discussion.
|
|
612
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return new Response("Not Found", { status: 404 });
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
558
621
|
### HTTP server on other runtimes
|
|
559
622
|
|
|
560
623
|
Every runtime does HTTP handling and WebSockets a little differently, although most modern runtimes use the standard `Request` and `Response` types from the Fetch API, as well as the standard `WebSocket` API. You should be able to use these two functions (exported by `capnweb`) to implement both HTTP batch and WebSocket handling on all platforms:
|
|
@@ -645,7 +708,7 @@ You can then set up a connection over it:
|
|
|
645
708
|
let transport: RpcTransport = new MyTransport();
|
|
646
709
|
|
|
647
710
|
// Create the main interface we will expose to the other end.
|
|
648
|
-
let localMain: RpcTarget = new MyMainInterface()
|
|
711
|
+
let localMain: RpcTarget = new MyMainInterface();
|
|
649
712
|
|
|
650
713
|
// Start the session.
|
|
651
714
|
let session = new RpcSession<RemoteMainInterface>(transport, localMain);
|