I took a deep dive into CapnWeb. What I expected to find was a browser-first Cap'n Proto successor with rigid schemas replaced by flexibile JSON-compatible data structures. What I found instead was a pared-down pairwise protocol with an interesting expression format.
OCapN on the otherhand has the three-way protcol interactions with a flexible data model with object reference stability.
Hi, I'm kumavis and I'm implementing a javascript implementation of OCapN.
While both projects draw on the same rich history of distributed object capabilities, there's room for a lot of design choices in both protocol design and user-facing developer experience.
Both projects are in development. OCapN's javascript implementation has not yet been published for use.
Both OCapN and CapnWeb javascript implementations are in development and so any comparison of them may be quickly out of date.
spec (wip) https://github.com/OCapN/OCapN/
implementations:
- guile scheme: https://codeberg.org/spritely/goblins
- javascript (wip): https://github.com/endojs/endo/tree/master/packages/OCapN
CapnWeb lacks a formal, language-agnostic spec; its behavior is defined implicitly via its JavaScript implementation and a prose description of the wire protocol.
implementation:
- javascript: https://github.com/cloudflare/CapnWeb/
lets start by comparing the specification of OCapN and in lieu of a spec, the wire protocol of CapnWeb.
Cap'n Proto (also by CapnWeb author Kenton Varda, inspired by OCapN co-author Mark Miller) describes its features in 4 layers. Let's use these to examine the feature set of OCapN and CapnWeb, first touching on connection initialization and how remote procedure calls are sent.
capnproto's 4 layers
- Level 1: Object references and promise pipelining
- Level 2: Persistent capabilities
- Level 3: Three-way interactions
- Level 4: Reference equality / joining
OCapN initializes a session by performing a handshake indicating protocol version and exchanging session keys for authenticated third party handoff.
CapnWeb does not specify a handshake.
Function calls may be invoked on remote objects. Generally they include a target and arguments.
In OCapN this is performed via a "op:deliver" or "op:deliver-only" and the args and return value are some supported Value.
In CapnWeb, rather than having a system message for sending calls, there is a system message for specifying an "expression" to be evaluated and stored at a promise on the receiving end. This expression can include method invocations. This approach is much more flexible.
For example it allows to send and reuse a large (bytes on the wire) call arg to the remote by sending an expression for that large value. (However its not clear how this specifically can be done with the implementation.)
The expressions also allow specifying a path of property lookups of References.
This can also be done in OCapN via a handful of system messages "op:get", "op:index", "op:untag"
The CapnWeb approach has an advantage that is can just be reference inline in the expression format without requiring creating an answer promise for the value.
Additionally this expression system supports "remaps", a means of remotely applying a mapping over an array. The implementation uses a neat "record-replay" trick to construct a remap expression for the remote side to run before returning the data.
There is no equivalent to "remaps" in OCapN.
In both protocols, when invoking an RPC, call args and return values may contain other RPC targets.
OCapN has a behavioral specification that RPC Targets should round-trip as the same object id.
Both protocols support promise pipelining and specifying promises in call args.
Weirdly, CapnWeb does not use a separate id space for Targets and Promises, and so its possible to refer to the same export as a Promise and a Target via the expression syntax.
OCapN provides a way of requesting persistent capabilities (bootstrap object "fetch" method), but does not specify how these are made available or what their lifetimes are.
CapnWeb does not mention persistent capabilities.
OCapN specifies three party handoff. Handoffs are signed to the recipient and cannot be redeemed by others even if observed. They are not vulnerable to replay attacks.
CapnWeb only specifies pairwise communication and does not handle three party handoff. Of course pairwise connections can be composed, and message invocations forwarded. This has drawbacks for latency and availability.
CapnWeb states they are considering adding three-way interactions.
OCapN specifies Reference equality. This means that for that the same Target (while in the export table) should always traverse the wire as the same id, so clients can use the reference as a key in a Map or similar situations where object Reference Identity is important. OCapN does not specify this same behavior for Promises.
While the CapnWeb wire protocol allows for Reference stability, the implementation does not support it.
both protocols implement importer-driven pairwise garbage collection. OCapN does not (yet) specify three-party GC cycle detection. There is some research detailing possible approaches.
At present, the CapnWeb implementation requires the user to manually manage the lifetime of objects received across the wire, suggesting automatic GC may not be timely enough for practicality.
They do state they are considering automatic garbage collection via FinalizationRegistry
.
The OCapN javascript implementation uses automatic GC, using WeakMaps
and FinalizationRegistry
.
OCapN uses Syrup.
CapnWeb uses JSON serializeable structures.
Both projects help make object capability protocols easier to adopt and explore different approaches to the design space. While I don't care what protocol wins, I do suspect three-party interactions will be a key fundamental component for an efficient distributed system.