Skip to content

Architecture

osra is three layers: transports normalize any channel into two messaging primitives, connections run the handshake and per-peer protocol state, and revivables translate values into wire-safe boxes and back. This page maps each layer to its place in src/ and explains the two mechanisms everything else is built on: the box → revive walk and portId routing.

  1. Transports (src/utils/transport.ts, src/utils/type-guards.ts) normalize any supported channel — Window, Worker, DedicatedWorkerGlobalScope, SharedWorker, MessagePort, WebSocket, ServiceWorker/ServiceWorkerContainer, WebExtension runtime/Port/onConnect/onMessage, or a custom { emit, receive } pair — into two primitives: sendOsraMessage(transport, envelope, origin, transferables) and registerOsraMessageListener({ listener, transport, key, remoteName, origin, unregisterSignal }). See transports for the user-facing catalogue and low-level messaging for the primitives themselves.

  2. Connections (src/connections/) own the handshake, per-peer connection state, and the envelope protocol. One expose() call creates one protocol instance with its own uuid; each discovered peer gets a ConnectionContext keyed by the peer’s uuid. Envelope shapes are documented in the wire protocol reference; how peers find each other is covered in the handshake.

  3. Revivables (src/revivables/) are an ordered list of modules, each owning one value type with isType / box / revive (and optionally init for per-connection state). Non-JSON values inside envelope data are replaced by boxes: plain objects tagged with __OSRA_BOX__ that name the module that owns them. You can add, drop, reorder, or override modules — see custom revivables.

recursiveBox (src/revivables/index.ts) walks a value depth-first: the first module whose isType matches handles it; otherwise arrays and plain objects (prototype exactly Object.prototype) are descended into, and anything else passes through as-is. recursiveRevive mirrors it, dispatching each box to the module with the matching type string.

The walk’s rules have direct consequences:

  • First match wins. Module order decides which module claims a value; this is why custom modules usually go ahead of the defaults, which end in catch-all fallbacks. See custom revivables.
  • Null-prototype objects are not descended into. Live values nested inside one are never boxed: a function in a null-prototype object throws DataCloneError at postMessage on clone transports, or is dropped on JSON.
  • Circular structures throw. Both walks track the current ancestor path and throw a TypeError on circular structures instead of recursing forever.
  • Pre-built boxes pass through. Already-boxed values pass through recursiveBox untouched, so modules can embed pre-built boxes in their payloads.

Live values (functions, promises, streams, ports, signals, …) all reduce to MessagePort semantics. The message-port module (src/revivables/message-port.ts) multiplexes every such port over the single underlying transport: each boxed port gets a random portId, its traffic rides { type: 'message', portId, data } envelopes, and a per-connection Map<portId, handler> gives O(1) dispatch. { type: 'message-port-close', portId } tears one port down without touching the rest.

On clone transports a real MessagePort is transferred directly when possible; everything else — and everything on JSON transports — routes via portId. The distinction matters at teardown: wire-routed traffic dies with the connection, while real transferred ports survive it. See lifecycle.

EventChannel/EventPort (src/utils/event-channel.ts) is an in-memory MessageChannel look-alike — postMessage queues until start(), close() notifies the peer — that never touches structured clone. It is used wherever a real MessageChannel can’t be:

  • on JSON transports, where ports can’t be transferred, and
  • for every function call channel (src/revivables/function.ts), even on clone transports, because revived live values appearing in call arguments aren’t structured-clonable.

Synthetic ports are always wire-routed via portId.