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 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>("https://example.com/api");
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 refereces. 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.
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 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:
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 "brokennness" with its `onRpcBroken()` method:
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 "capnpweb";
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);