#Runtime

    This package is the internal engine behind Rabbita's TEA-style update loop. Application code should normally use the public APIs from @rabbita, not this package directly.

    #Core Concepts

    internal/runtime defines the low-level pieces that coordinate state updates, effects, and DOM patching:

    • Cmd: deferred work handled by the scheduler.
    • Scheduler: runtime trait that can enqueue commands and route URL events.
    • IsCell: stateful unit with step, view, and flags.
    • Sandbox: concrete scheduler and render loop implementation.
    • VNode / Props / Children: virtual DOM data model and patching helpers.

    #Update and Render Pipeline

    The runtime executes updates in two phases: message draining and frame flushing.

    1. An emit function returns Cmd::Message(id, send).
    2. Scheduler::add executes the command:
      • Empty: no-op
      • Batch: enqueue each command recursively
      • Effect: run callback immediately with &Scheduler
      • Message: call send(), push id into the queue
    3. Sandbox::drain processes queued ids in FIFO order:
      • looks up the live cell by id
      • runs cell.step(self)
      • marks the id dirty
    4. Sandbox::flush schedules one requestAnimationFrame callback:
      • for each dirty live instance, compute new view()
      • run VDOM diff and patch DOM
      • clear dirty flags/set after commit

    This keeps model updates synchronous and deterministic while keeping side effects explicit.

    #Commands

    Cmd is an internal enum.

    The key rule is that update returns commands; effects are executed by runtime, not directly inside update.

    #Cells, Identity, and Liveness

    Each cell has Flags:

    • id: stable runtime id
    • dirty: whether the cell needs rerender

    Sandbox.live_map tracks all currently attached instances for each cell id. When a subtree is removed, drop_live_subtree removes stale instances from live_map. Messages for detached cells are ignored because they no longer resolve in live_map.

    VNode::link uses a special internal tag to install a captured click listener. At insert time it becomes an actual <a> element. The listener classifies URLs:

    • same origin -> @url.Internal(url)
    • different origin -> @url.External(href)

    When App::with_route(url_request=...) is configured, it emits add_url_request. Without that callback, the listener keeps native <a> navigation behavior (no interception).

    #Design Notes

    • Update execution is synchronous per message, which preserves a single source of truth for model transitions.
    • Async work is represented as Cmd and re-enters the loop as messages.
    • Rendering is batched to animation frames to avoid repeated DOM work within a burst of updates.

    Host

    pub trait Host :
    Scheduler
    {
    fn flush(Self) -> Unit
    fn cleanup(Self) -> Unit
    fn get_stores(Self) ->
    SlotMap
    [Store]
    }

    BrowserHost

    type BrowserHost

    The rendering pipeline will do the following step:

    1. view(model) -> newView compute new vdom from model

    2. reconcile(oldView, newView) update the mounted render state
    impl Host for BrowserHost

    BrowserHost::BrowserHost

    HydrationHost

    type HydrationHost

    ReactHost

    ★ moobile 扩展:把 rabbita 运行时的渲染后端从 DOM 换成 React。

    与 BrowserHost 的差别只有两处,而且都不是"重写运行时":

    1. request_frame 不再 document.update(vnode),而是把当前 VNode 交给 frame 回调 —— moobile 在那里翻译成 React 元素。
    2. 挂 microtask / frame 的调度方式由外部注入 (React Native 里没有 window,更没有 requestAnimationFrame)。

    命令队列、微任务抽干、handle_message、SlotMap stores、duplix scope 全部照搬 —— 因为它们本来就与 DOM 无关。

    这条正是 DESIGN 的「渲染可以整体外包」:连 rabbita 自己的抽干循环都能留着, 只把最后一跳换掉。接上它之后,on_click=emit(Msg) 这种视图代码就能直接跑了。
    impl Host for ReactHost

    ReactHost::ReactHost

    fn ReactHost::ReactHost(builder : () ->
    Node
    [
    VNode
    ], schedule_task : (() -> Unit) -> Unit, schedule_frame : (() -> Unit) -> Unit, origin? : String) -> ReactHost

    ReactHost::on_frame

    fn ReactHost::on_frame(self : ReactHost, f : (
    VNode
    ) -> Unit) -> Unit

    注册"每次重绘把当前 VNode 交给宿主"的回调。

    翻译要在回调里做:它被 ambient_host.protect 包着, 而 VNode::Thunk 的闭包可能读 duplix 节点 —— 出了作用域再 force 会失效。

    ReactHost::start

    fn ReactHost::start(self : ReactHost) -> Unit

    同步抽干一次命令队列并同步画出首帧。

    为什么需要它:Host::flush 走的是 microtask,而首帧还要再等一个 animation frame —— React 的首渲染会先看到空,然后闪一下。 同步跑一次,element() 立刻就有内容。

    Store

    type Store

    ambient_host

    let ambient_host :
    Ref
    [&Host]

    The dynamically scoped host of the App currently being evaluated. This is intentionally a dynamic variable so stateful and stateless components can share the same API, instead of requiring stateful components to accept an explicit graph as in Bonsai. MoonBit also lacks an equivalent of OxCaml's local, so passing the graph explicitly would introduce that API distinction without letting the type system constrain the graph's lifetime.

    The cost of this choice is internal to Rabbita rather than exposed to users. As frameworks with fine-grained reactivity and implicit tracking, the runtime must carefully establish and restore this context around every graph evaluation. Async boundaries also make this internal bookkeeping substantially more fragile.

    TODO: Report an error when the ambient host is DummyHost.

    react_message_count

    fn react_message_count() -> Int

    被派发到 store 的消息数(即 on_update 实际跑过几次)。

    react_op_count

    fn react_op_count() -> Int