/** * @file An OCapN netlayer whose transport is an RPC session: Cap'n Web, or * Workers RPC, or one proxied through the other. * * Each OCapN connection is a pair of {@link ByteInbox}es, one per * direction, exchanged through a gateway; each `deliver` call carries * exactly one OCapN message, so no framing is needed. The RPC session * provides ordering and only-the-peer-can-inject; OCapN does the rest. * * After vattr97's ertp-clerk (`ocapn-capnweb-netlayer.ts`), with the stub * methods only Cap'n Web has (`onRpcBroken`) made optional. * * @see https://github.com/ocapn/ocapn/blob/main/draft-specifications/Netlayers.md */ import { RpcTarget } from "capnweb"; export const TRANSPORT = "capnweb"; /** * A peer's location, by designator alone: a router finds the peer. * * @param {string} designator */ export const locationOf = (designator) => ({ type: "ocapn-peer", transport: TRANSPORT, designator, hints: false }); /** Same as `locationToLocationId` in `@endo/ocapn`, which its exports map doesn't expose. */ const locationId = ({ designator, transport }) => `ocapn://${designator}.${transport}`; /** Receives one direction of an OCapN connection, one message per call. */ export class ByteInbox extends RpcTarget { #onMessage; constructor(onMessage) { super(); this.#onMessage = onMessage; } /** @param {Uint8Array} bytes */ deliver(bytes) { // OCapN's syrup reader needs offset 0; a pooled Buffer view may not have it. this.#onMessage(bytes.byteOffset === 0 ? bytes : new Uint8Array(bytes)); } } const dispose = (stub) => stub?.[Symbol.dispose]?.(); /** Write to a remote inbox, discarding each result at once so the session retains none. */ const makeSocket = (inbox, close) => ({ write: (bytes) => dispose(inbox.deliver(bytes)), end: close, }); /** * A remote inbox reached through a gateway that is only a promise (a * local async call, not a pipelining stub): writes queue in order behind it. */ const deferredInbox = (gatewayP, myInbox) => { const inboxP = Promise.resolve(gatewayP).then((gateway) => gateway.connect(myInbox)); return { deliver: (bytes) => void inboxP.then((inbox) => dispose(inbox.deliver(bytes))), onRpcBroken: (cb) => void inboxP.then((inbox) => inbox.onRpcBroken?.(cb), cb), }; }; /** * Make an OCapN netlayer for `client.registerNetlayer`. * * @param {object} opts * @param {string} opts.designator this peer's designator * @param {(designator: string) => any} opts.openGateway the gateway of * the peer with this designator: a stub or pipelining promise * (`openGateway(d).connect(...)`), or a plain promise for one * @returns a netlayer maker; the netlayer has a `gateway` that accepts * incoming connections */ export const makeRpcNetLayer = ({ designator, openGateway }) => { const location = locationOf(designator); const connections = new Set(); return (handlers, logger) => { const track = (connection, inbox) => { connections.add(connection); inbox.onRpcBroken?.((reason) => { if (connections.delete(connection)) logger.log("rpc netlayer: connection broken", reason); handlers.handleConnectionClose(connection, reason); }); }; const netlayer = { location, locationId: locationId(location), connect: (peer) => { const gateway = openGateway(peer.designator); let connection; const myInbox = new ByteInbox((bytes) => handlers.handleMessageData(connection, bytes)); // Pipelined: writes before the peer answers queue in order. const peerInbox = typeof gateway.connect === "function" ? gateway.connect(myInbox) : deferredInbox(gateway, myInbox); connection = handlers.makeConnection( netlayer, true, makeSocket(peerInbox, () => { connections.delete(connection); dispose(gateway); }), ); track(connection, peerInbox); return connection; }, shutdown: () => { for (const connection of [...connections]) connection.end(); }, }; class Gateway extends RpcTarget { connect(peerInbox) { const peer = peerInbox.dup?.() ?? peerInbox; const connection = handlers.makeConnection( netlayer, false, makeSocket(peer, () => { connections.delete(connection); dispose(peer); }), ); track(connection, peer); return new ByteInbox((bytes) => handlers.handleMessageData(connection, bytes)); } } return Object.assign(netlayer, { gateway: new Gateway() }); }; };