// Spec for the port of tutuca's path traversal (src/path.js) plus the
// leaf-update core of the transactor (src/transactor.js updateRootValue).
// READONLY by convention: concrete public data types plus `declare` stubs.
// Implement the declares in separate files; this file and the *_test.mbt
// suites define the contract.
//
// Layering note: this package owns the path CORE (steps, resolve/update,
// dispatch-path compaction + teleporting via DispatchPath::to_transaction_path
// below). The pieces that need the view registry, the rendered DOM, or the
// component stack live one layer up, where they were implemented:
// - frame-only steps + stack rebuild (JS enterFrame/buildStack) -> render's
// build_stack (render/build_stack.mbt), one arm per Step kind;
// - event-path reconstruction (JS Path.fromNodeAndEventName/fromEvent) ->
// render's from_node_and_event_name (render/events.mbt), driven by the
// backend glue + App::dispatch (which extract target/event-name, the job
// of the JS Path.fromEvent wrapper);
// - dispatch channels (send/bubble/request/response) -> the transactor
// package.
// DynStep teleporting itself (JS toTransactionPath) IS here — see
// DispatchPath::to_transaction_path.
///|
/// Dispatch buckets a handler can live in (semantics.md §6). Alter is
/// render-time only and never dispatched, so it is not a bucket here.
///
/// TWO of these, and one question separates them: does the sender know who
/// handles this? `Receive` holds every ADDRESSED message whoever sent it — a
/// view, a parent, the host, or an answer to an intent; `Intent` holds every
/// ROUTED one, dispatched by somebody who named a job rather than a target.
///
/// A view's name is addressed at the component the view belongs to, which is
/// what `send` means, so there is no separate bucket for one.
pub(all) enum HandlerBucket {
Receive // addressed: a view, ctx.send, the host, an answer
Intent // routed: dispatched by someone who did not name a target
} derive(Debug, Eq)
///|
/// A leg of an intent's route.
///
/// `Dyn` walks the dynamic scope — the dispatch path, from the sender's PARENT
/// up to the root, one `pop_step()` per hop — and its handlers are component
/// instances with state. `Lex` walks the lexical scope — the registration
/// chain — and its handlers are static functions with none. A route is a list
/// of these, in the order written, and `[Dyn, Lex]` when nothing says
/// otherwise.
pub(all) enum Leg {
Dyn
Lex
} derive(Debug, Eq)
///|
/// The leg word a route is written with, so a route has one spelling and not
/// one per package that prints it.
pub fn Leg::word(self : Leg) -> String {
match self {
Dyn => "dyn"
Lex => "lex"
}
}
///|
/// A route as it is written: the leg words, space-separated. `[Dyn, Lex]` is
/// `"dyn lex"`, and the empty route is the empty string.
pub fn route_label(route : Array[Leg]) -> String {
route.map(l => l.word()).join(" ")
}
///|
/// Options for `Ctx::intent`.
///
/// `route` is the walk itself. The three answer names are the three ways a walk
/// can end, and each carries its own payload: a hop replied, a hop failed, or
/// the route ran out with nobody answering — which is not the same answer as a
/// handler failing. `live_path` opts out of pinning field-resolved keys at
/// dispatch time.
///
/// There is deliberately no combined result-or-error arm. An outcome a handler
/// has to discriminate is one it can discriminate wrongly.
pub(all) struct IntentOpts {
/// The legs to walk, in order. `[Dyn, Lex]` when nothing says otherwise.
route : Array[Leg]
on_ok_name : String?
on_error_name : String?
on_unhandled_name : String?
live_path : Bool
} derive(Debug, Eq)
///|
declare pub fn IntentOpts::new(
route? : Array[Leg],
on_ok_name? : String,
on_error_name? : String,
on_unhandled_name? : String,
live_path? : Bool,
) -> IntentOpts
///|
/// What a static intent handler is handed.
///
/// `from` is the sender's position. A host reads it to decide whether the
/// sender may ask — which is what `dyncomp/SECURITY.md` §5 says it cannot do
/// today — and the walk needs it anyway to route the answer, so the handler
/// gets it for free.
///
/// This lives in `core` rather than beside `Dispatch` in `component` for a
/// mechanical reason: `transactor` constructs one and `component` consumes one,
/// and neither imports the other. It is pure data over `core`'s own types, so
/// this is the only place both can see it.
pub(all) struct IntentCall {
name : String
args : Array[Value]
from : DispatchPath
} derive(Debug, Eq)
///|
/// What a static intent handler answers.
///
/// `Pass` is the case a walk needs: a handler that has nothing to say declines,
/// and the walk goes on to the next one — the same freedom a component's
/// `update` arm has when it answers `Unhandled`. Without it, a handler with
/// nothing to contribute could only invent an error.
pub(all) enum IntentAnswer {
Ok(Value)
Failed(Value)
/// Decline. The walk continues to the next handler on the route.
Pass
} derive(Debug, Eq)
///|
/// What a handler can do besides transforming its leaf (JS EventContext).
/// Defined here so Handler can carry it without depending on the transactor;
/// every method defaults to a no-op so tests and Path::update can run
/// handlers without a dispatcher. The transactor implements the real thing.
pub(open) trait Ctx {
fn path(Self) -> DispatchPath = _
fn target_path(Self) -> DispatchPath = _
fn name(Self) -> String? = _
fn send(Self, name : String, args : Array[Value]) -> Unit = _
fn send_at_path(Self, path : DispatchPath, name : String, args : Array[Value]) -> Unit = _
/// Dispatch an intent along `opts.route`. The `dyn` leg starts at this
/// ctx's PARENT, so a component never re-enters its own handler.
fn intent(Self, name : String, args : Array[Value], opts : IntentOpts) -> Unit = _
fn intent_at_path(
Self,
path : DispatchPath,
name : String,
args : Array[Value],
opts : IntentOpts,
) -> Unit = _
/// Dispatch the message being handled as an intent, or — in an intent body —
/// continue the walk after this position. `None` arguments pass on the ones
/// that arrived; `None` opts keep the route the intent is already walking, or
/// take the default when a message is picking one for the first time. The
/// answer target does not move, which is the whole of what makes this
/// different from dispatching the intent again.
fn forward(Self, args : Array[Value]?, opts : IntentOpts?) -> Unit = _
/// Answer the intent this handler received, and end the walk.
fn reply(Self, value : Value) -> Unit = _
/// Answer it with a failure, and end the walk.
fn fail(Self, error : Value) -> Unit = _
fn stop_propagation(Self) -> Unit = _
/// `ctx.at`: a relative-path builder rooted at this ctx's dispatch path.
fn at(Self) -> PathChanges = _
/// Walk the component instances on this ctx's path leaf→root (JS
/// walkPath), invoking `callback(component_id, instance)` for each; return
/// false to stop early. Non-component chain elements are skipped. The pure
/// default walks nothing (no state root); the transactor implements it.
fn walk_path(Self, callback : (Int, Value) -> Bool) -> Unit = _
}
///|
impl Ctx with fn path(_self) {
DispatchPath::new()
}
///|
impl Ctx with fn target_path(_self) {
DispatchPath::new()
}
///|
impl Ctx with fn name(_self) {
None
}
///|
/// `ctx.send` is `send_at_path` at the ctx's OWN path — which is what the
/// transactor's ctx used to say twice, once as a default that did nothing and
/// once as an implementation. Spelling it here leaves one method to implement,
/// and no way for the relative and absolute forms to disagree about what
/// "here" means.
impl Ctx with fn send(self, name, args) {
self.send_at_path(self.path(), name, args)
}
///|
impl Ctx with fn send_at_path(_self, _path, _name, _args) {
}
///|
/// `ctx.intent` is `intent_at_path` at the ctx's OWN path, the way `send` is —
/// the walk's "start at the parent" rule belongs to the transactor and not to
/// the address, so both spellings hand it the same place.
impl Ctx with fn intent(self, name, args, opts) {
self.intent_at_path(self.path(), name, args, opts)
}
///|
impl Ctx with fn intent_at_path(_self, _path, _name, _args, _opts) {
}
///|
/// The four new methods default to nothing, for the reason every method on this
/// trait does: a handler has to be runnable with no dispatcher, so `Path::update`
/// and a test can call one. A `reply` with no transactor is a no-op and not a
/// crash — there is no walk to answer, and nothing to report to.
impl Ctx with fn forward(_self, _args, _opts) {
}
///|
impl Ctx with fn reply(_self, _value) {
}
///|
impl Ctx with fn fail(_self, _error) {
}
///|
impl Ctx with fn stop_propagation(_self) {
}
///|
impl Ctx with fn at(self) {
PathChanges::of(self)
}
///|
impl Ctx with fn walk_path(_self, _callback) {
}
///|
/// A Ctx that dispatches nothing (JS `eval(null)` analogue for handlers).
pub(all) struct NullCtx {}
///|
pub impl Ctx for NullCtx
///|
/// A leaf-update handler, resolved ON the leaf so `self` is pre-bound (the JS
/// `handler.apply(instance, args)` binds `this` at call time; binding at
/// resolution time is equivalent — same instance either way). Receives its
/// args and the dispatch Ctx (JS appends ctx as the last handler argument).
/// Returns the new leaf, or None for "no change" (the port of the JS
/// `newLeaf !== curLeaf` identity check — answering None lets the traversal
/// skip the spine rebuild without comparing anything).
pub(all) struct Handler((Array[Value], &Ctx) -> Value?)
///|
/// Resolve one addressing step against a node: the child it addresses, or
/// None when unresolvable (missing field, bad index, key field not a key…).
/// One interpreter over the `value_dyn.mbt` accessors, so step semantics —
/// especially SeqAccessStep's live key resolution — are written once.
///
/// The tree used to be addressed through a `PathNode` TRAIT, so that a node
/// could be something other than a `Value`. Nothing ever was: the only
/// implementation outside the tests was `Value` itself, while typed component
/// instances travelled — as they still do — inside `Value::Obj`. What the
/// trait did buy was a box per traversal step, an `as_value` unwrap at every
/// consumer (with an impossible failure branch each time), and a `same_node`
/// that existed because two boxes carrying the SAME value are not
/// `physical_equal` — which is the one comparison the whole copy-on-write
/// design is keyed on.
declare pub fn step_get(node : Value, step : Step) -> Value?
///|
/// Rebuild one spine level: a new node with the child this step addresses
/// replaced by `child`. None when the step is unaddressable on `node`
/// (the caller leaves the tree untouched).
declare fn step_put(node : Value, step : Step, child : Value) -> Value?
///|
/// A generic `{field, key?}` descriptor of an addressing step (JS
/// Step.toKey), so tooling can introspect a path without matching on Step.
pub(all) struct StepKey {
field : String
key : PathKey?
} derive(Debug, Eq)
///|
/// An address into the component/state tree: a sequence of steps root→leaf.
pub(all) struct Path {
steps : Array[Step]
} derive(Debug, Eq)
///|
declare pub fn Path::new(steps? : Array[Step]) -> Path
///|
declare pub fn Path::concat(self : Path, steps : Array[Step]) -> Path
///|
/// The path one component closer to the root (bubbling walks this).
declare pub fn Path::pop_step(self : Path) -> Path
///|
/// The value this path resolves to, or None at the first unresolvable step.
declare pub fn Path::lookup(self : Path, root : Value) -> Value?
///|
/// Replace the leaf: walk down collecting intermediates, rebuild the spine
/// bottom-up via step_put (structural sharing). If any step is unresolvable
/// the SAME `root` object is returned (physically — callers detect "nothing
/// happened" by `physical_equal`, matching JS setValue).
declare pub fn Path::set_value(self : Path, root : Value, v : Value) -> Value
///|
/// The nodes entered along the path, root included (index 0), stopping at
/// the first unresolvable step. JS Path.resolveChain. Powers the
/// transactor's `ctx.walk_path` (leaf→root component walk).
declare pub fn Path::resolve_chain(self : Path, root : Value) -> Array[Value]
///|
/// Freeze every field-resolved key (SeqAccessStep) against `root` as it is
/// NOW: SeqAccessStep(seq, keyField) becomes SeqStep(seq, key) so a later
/// lookup/set_value lands on the same item even if keyField changes meanwhile
/// (async response races). Returns `self` (physically) when nothing pinned.
declare pub fn Path::pin_keys(self : Path, root : Value) -> Path
///|
/// Flat StepKey list of the addressing steps (JS Path.toKeys). SeqAccessStep
/// reports its seq field with no key (the key is live; pin_keys first for a
/// concrete one) rather than being dropped, which would shift later indices.
declare pub fn Path::to_keys(self : Path) -> Array[StepKey]
///|
/// One hop of a dispatch path — the component-boundary projection of an event
/// path. Plain wraps an addressing step with its origin (the id of the
/// component that contributed it — teleport provenance); Dyn marks a dynamic
/// variable (`*dyn`) used as a render target: the rendered data lives at the
/// PRODUCER component (where the dynamic was defined), not at the consumer
/// that wrote ``, so `interior` lists the component ids
/// crossed between producer and consumer, and `steps` is the producer's own
/// path to the data. `key` is set when the dynamic is ITERATED (the item lives
/// at the producer's sequence field under that key) — the one thing that used
/// to be a whole second variant, `DynEach`, which differed in nothing else and
/// so was matched alongside `Dyn` everywhere except the one line that reads
/// the key.
pub(all) enum DispatchStep {
Plain(step~ : Step, origin~ : Int?)
Dyn(
producer~ : Int,
steps~ : Array[Step],
interior~ : Array[Int],
key~ : PathKey?
)
} derive(Debug, Eq)
///|
/// A path that still knows which component contributed each hop. Bubbling
/// pops one hop per component; converting to a transaction Path teleports
/// every Dyn marker so mutations land on the data's real location.
pub(all) struct DispatchPath {
items : Array[DispatchStep]
} derive(Debug, Eq)
///|
declare pub fn DispatchPath::new(items? : Array[DispatchStep]) -> DispatchPath
///|
/// Wrap plain addressing steps (no origins, no dynamics).
declare pub fn DispatchPath::of_steps(steps : Array[Step]) -> DispatchPath
///|
/// Append plain addressing steps (ctx.at-style refinement of a handler path).
declare pub fn DispatchPath::concat(
self : DispatchPath,
steps : Array[Step],
) -> DispatchPath
///|
/// One component closer to the root (bubbling walks this).
declare pub fn DispatchPath::pop_step(self : DispatchPath) -> DispatchPath
///|
/// The dispatch projection: frame-only plain steps dropped, EachRenderItStep
/// abstracted, Dyn markers KEPT (bubbling must still visit the components
/// interior to a dynamic-var render). Origins are preserved.
declare pub fn DispatchPath::compact(self : DispatchPath) -> DispatchPath
///|
/// The abstract path used to apply a transaction: every Dyn marker is
/// teleported — the trailing steps whose origin is interior to the
/// producer..consumer span are dropped and the producer's own steps spliced
/// in (stamped with the producer id, so a Dyn nested inside another
/// teleports correctly). An iterated Dyn splices a keyed SeqStep for its last
/// FieldStep; a seq-access dynamic cannot be iterated and splices unchanged.
declare pub fn DispatchPath::to_transaction_path(self : DispatchPath) -> Path
///|
/// The transactor's updateRootValue: resolve the leaf, resolve its handler by
/// exact name — ONE lookup, with no fallback sentinel behind it (see
/// path_path.mbt) — invoke it, and rebuild the spine only when the handler
/// produced a new leaf. Unresolvable path,
/// missing handler, a handler answering None, or a physically-unchanged leaf
/// all return the SAME `root` object — the spine is not rebuilt when nothing
/// changed.
declare pub fn Path::update(
self : Path,
root : Value,
bucket : HandlerBucket,
name : String,
args : Array[Value],
) -> Value