Expand description
Unified, effect-separated protocol abstraction shared by native and browser.
Design: functional core, imperative shell. A protocol author writes only a pure
state transition over its own typed events and effects; all IO is performed by the
extension’s own Interpret shell, which is handed a namespace-scoped capability
(Scope) — send/inject confined to the interpreter’s own namespace.
Notation (used throughout the doc-comments here):
decode : Wire ⇀ Event
step : (Ctx S, Event) → Transition (S, Effect) where Transition (S,E) ≅ (S, [E])The effect algebra is not global: each extension defines Protocol::Effect and the
interpreter that runs it. The core owns no Effect enum — adding an extension never
touches the core, and a protocol can only emit its own effects (no global command bus).
step is pure (no IO, clocks, globals) and total over well-typed events; the decode
boundary makes “undecodable/foreign input” an explicit Reject instead of a silent
no-op. The abstraction is identical on both targets; the sole divergence is the Send /
?Send bound, isolated in MaybeSend.
§Module layout
envelope— the wireEnvelope.protocol— the pure core:Wire/Reject(decode boundary),Ctx,Inbound(router-internal),Transition, and theProtocoltrait.interpret— the per-extension imperative shell (Interpret).registry— the scoped capabilityScopehanded to shells, plus the router-internalCore/Handlerand the namespace registry (Extensions).
Structs§
- Ctx
- Read-only state carrier passed into a step: the protocol’s current state
Splus read-only node facts. The state is borrowed; a step returns the next state in itsTransitionrather than mutating in place. - Envelope
- Namespaced message envelope carried over the P2P transport (bincode), in place of
the old closed
BackendMessageenum.payloadis opaque to the core. - Extensions
- Registry of
(Protocol, Interpret)pairs by namespace, plus the router-internalCore. Cheaply cloneable and shared (interior mutability) so theProviderand the inbound callback see the same table. - Reject
- The explicit result of a failed
Protocol::decode: the input is malformed or not for this protocol. The router drops it (a defined no-op) instead of the pure step silently returning an unchanged state — so “valid no-op” and “undecodable input” are distinguishable. - Scope
- A namespace-scoped capability handed to an
Interpretshell — the effectful counterpart of the pure side’s read-onlyCtx. Every action is confined to the interpreter’s own namespace: it maysendto peers and self-injectonly there, and it can neither reach another namespace nor forge a remotefrom. This is what keeps the capability honest: an extension shell cannot use the router as a generic inject-any-namespace bus (e.g. manufacture another extension’s lifecycle events). Cloneable and'static, so a long-running engine task can keep a copy. - Transition
- The output of a step: the next state and the protocol’s own effects to run.
Transition (S, E) ≅ (S, [E])— the Writer-over-State pair, now parameterized by the protocol’s private effect typeE.PartialEq/Eqare derived (whenS/Eare) so tests can compare whole transitions. - Wire
- The raw boundary input handed to
Protocol::decode: an inbound message’s authenticated sender, this node’s own did, and the opaque payload bytes.decodeturns this into the protocol’s typedEvent(or rejects it).
Traits§
- Interpret
- Runs the effects produced by a protocol’s pure
step.runreturns the payloads to re-inject into the router; each is re-delivered to this protocol’s own namespace withfrom = this node— the router sets the provenance, so a shell can forge neither a namespace nor a remotefrom. A hard failure is anErr. Defined per-effect outcomes (e.g. “addressed no live session”) are the extension’s own concern and surfaced however it likes (its own return shapes / tests), not a core enum. - Maybe
Send - Auto-trait bound that is
Send + Syncon native and empty on browser. - Protocol
- A protocol: a
namespace, an initial state, a decode boundary, and a state transition that is pure by contract.