🥮 mooncakes.io

    XiLaiTL/moobile/vendor/rabbita/cmd does not have a README file

    Settle

    type Settle = (Json) -> Unit

    Context

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(open) trait Context {
    #internal(experimental, "This API is unstable and may change in the future.")
    fn get_origin(Self) -> String
    #internal(experimental, "This API is unstable and may change in the future.")
    fn inject_url_changed(Self, String) -> Cmd
    #internal(experimental, "This API is unstable and may change in the future.")
    fn inject_url_request(Self, String) -> Cmd
    }

    Scheduler

    pub(open) trait Scheduler : Context {
    fn add(Self, Cmd) -> Unit = _
    #internal(experimental, "This API is unstable and may change in the future.")
    fn queue_command(Self, Cmd) -> Unit
    #internal(experimental, "This API is unstable and may change in the future.")
    fn set_url_changed_injector(Self, Injector[String]) -> Unit
    #internal(experimental, "This API is unstable and may change in the future.")
    fn set_url_request_injector(Self, Injector[String]) -> Unit
    }

    HydrateExn

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(all) suberror HydrateExn {
    Unhandled
    Fallback
    Skip
    }

    Unhandled

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(all) suberror Unhandled

    Cmd

    type Cmd

    A deferred command handled by the Rabbita runtime.

    Cmd models side effects in the update loop. Commands are returned from update or embedded in Html event handlers, and are executed later by the runtime.
    impl Debug for Cmd

    EffectKind

    pub(all) enum EffectKind {
    Immediately
    AfterRender
    }

    Emit

    #alias(Dispatch, deprecated="Use Emit[Msg] instead.")
    pub(all) struct Emit[Msg]((Msg) -> Cmd)

    Emit::map

    fn[A, B] Emit::map(self : Emit[A], f : (B) -> A) -> Emit[B]

    Extension

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(all) extenum Extension {
    }

    HostCapability

    #external
    pub type HostCapability

    宿主能力通道(PLAN §3.6 的 N2)。

    它解决的是什么问题

    vendor 里的能力包(sub/ clipboard/ nav/ dialog/)都是 DOM 实现: 它们直接调 @dom.window() / @dom.document()。在 RN 上 document 不存在, window.innerWidth / window.scrollY / window.location 也不存在 —— 于是这些能力在原生端要么静默失效、要么直接抛。

    ⚠️ #cfg(target="js") 解决不了这件事:RN 走的也是 js 目标 (moobile-host 就是 moon build --target js),Web 与 RN 是同一个 target。 所以"这是不是浏览器"必须由宿主自己声明,不能由编译目标推断。

    机制

    宿主在 globalThis.MOBILE_HOST.native 下登记"某个能力在它这个平台上的实现":

    MOBILE_HOST.native = { visibility: { // 订阅一个布尔流;返回退订函数 subscribe(cb) { const sub = AppState.addEventListener("change", s => cb(s !== "active")); return () => sub.remove(); }, }, }

    能力包的用法是先问这里,问不到就回退 DOM:

    match @cmd.host_capability("visibility") {
    Some(cap) => cap.subscribe_bool(...) // 宿主实现(RN)
    None => ...原有的 DOM 实现... // 回退(Web)
    }

    这样做的三条性质:

    1. Web 行为一字不变 —— 宿主不登记就自动走回退,不需要新代码。
    2. RN 上有了真实现的路 —— 只要求宿主登记,不要求库知道 RN 的存在。
    3. 两端都没有时仍然回退(老行为:DOM 抛),由 N5 的"哪端可用"标注负责 说清楚,而不是假装支持。

    为什么放在 cmd/

    Context / Scheduler(宿主契约 trait)就在这个包,而全部能力包都已 import 它 —— 于是加这条通道不需要任何新的包依赖、也不会成环(能力包不能 import 模块根包: 根包已经 import 了它们)。

    为什么不复用 Context

    Context 的现有方法(get_origin 等)是同步取值;能力多数是异步或流式 (剪贴板读写、可见性变化、尺寸变化)。把它塞进 Context 会让每条能力都要求 Context 的每个实现(含 SSR / dummy 宿主)跟着实现一遍。 这条通道是可选注册表:没有登记就是 None,宿主不必为不用的能力写空实现。

    HostCapability::subscribe_bool

    fn HostCapability::subscribe_bool(self : HostCapability, cb : (Bool) -> Unit) -> HostSubscription?

    订阅一个布尔流(能力对象上要有 subscribe(cb) -> unsubscribe)。

    返回 None 表示这个宿主登记了这个能力、但没提供这个形状的方法 —— 调用方应当据此回退,而不是当作"流为空"。

    HostCapability::subscribe_json

    fn HostCapability::subscribe_json(self : HostCapability, cb : (String) -> Unit) -> HostSubscription?

    订阅一个结构化载荷流(能力对象上同样要有 subscribe(cb) -> unsubscribe)。

    为什么是"JSON 字符串"而不是每种载荷一个窄适配器

    载荷形状各不相同:Viewport 是两个整数、Scroll 是四个、URL 变化是一个字符串。 三条路:

    方案代价
    每种形状一个窄适配器(subscribe_vec2 / subscribe_scroll / …)FFI 面按载荷数量线性膨胀,每加一条能力都要动库
    JSON 字符串(本方案)形状错误在编译期看不见 —— 用"解码严格"补(见下)
    不透明 @js.Value 交给能力包自己打字每个能力包都要写一遍 JS 互操作,且要 import @js

    选 JSON 是跟随本仓库已有的约定:sqlite/ 也是"参数与返回值都是 JSON 字符串, 边界上只传字符串,MoonBit 侧用 @json 解,避免把 JS 对象逐个字段打字" (见 npm/moobile-host/capabilities/db.js 顶部)。

    形状错了怎么办:严格解码,不是静默给默认值

    这一条是关键。载荷形状不匹配(宿主写错字段名)如果解成 0,就正好复现了本通道要 消灭的那个毛病 —— 静默给错值。所以调用方必须严格解:字段缺失就当场报错, 而不是取默认值。这与 sqlite/ensure() 的取舍一致:契约不匹配是作者错误, 要立刻看见;而载荷"运行时才知道形状"那是另一回事(那是 design/DESIGN-COMPONENT-LIBRARY.md §5 T1 说的"提取器永不抛错")。

    宿主侧可以给对象也可以给字符串 —— FFI 里统一成字符串,省得每个宿主记一遍规矩。

    HostSubscription

    #external
    pub type HostSubscription

    一次订阅的句柄。持着它才能在 unload 时退订。

    HostSubscription::cancel

    fn HostSubscription::cancel(self : HostSubscription) -> Unit

    退订。幂等:宿主没给 unsubscribe 也不抛(同"提取器永不抛错"那条取舍, 见 design/DESIGN-COMPONENT-LIBRARY.md §5 T1)。

    Injector

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(all) struct Injector[Arg]((Arg) -> Cmd)

    #internal(experimental, "This API is unstable and may change in the future.")
    type Op

    Op::Op

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::Op() -> Op

    Op::on_debug

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::on_debug(self : Op, f : (Extension) ->
    Repr
    raise Unhandled) -> Unit

    Op::on_identify

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::on_identify(self : Op, identify : (Extension) -> String raise Unhandled) -> Unit

    Registers the identity used to store and resume this command's SSR transcript value. Authors must return different keys for commands that may need different transcript values during hydration.

    Op::on_invoke

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::on_invoke(self : Op, f : (Extension, &Context) -> OpCont[Cmd] raise Unhandled) -> Unit

    Op::on_resume

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::on_resume(self : Op, f : (Extension, Json, &Context) -> Cmd raise HydrateExn) -> Unit

    Op::on_settle

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::on_settle(self : Op, f : (Extension, &Context) -> OpCont[(Json, Cmd)] raise Unhandled) -> Unit

    Op::request

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Op::request(self : Op, extension : Extension) -> Cmd

    OpCont

    #internal(experimental, "This API is unstable and may change in the future.")
    pub(all) enum OpCont[T] {
    None
    Ready(T)
    Async(async () -> T)
    AfterLayout(() -> T)
    LegacyEffect(EffectKind, (&Scheduler) -> Unit)
    }

    Runner

    #internal(experimental, "This API is unstable and may change in the future.")
    type Runner

    Runner::identify

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Runner::identify(runner : Runner, extension : Extension) -> String raise Unhandled

    Runner::invoke

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Runner::invoke(runner : Runner, extension : Extension, context : &Context) -> OpCont[Cmd] raise Unhandled

    Runner::resume_

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Runner::resume_(runner : Runner, extension : Extension, value : Json, context : &Context) -> Cmd raise HydrateExn

    Runner::settle

    #internal(experimental, "This API is unstable and may change in the future.")
    fn Runner::settle(runner : Runner, extension : Extension, context : &Context) -> OpCont[(Json, Cmd)] raise Unhandled

    after_render

    let after_render : EffectKind

    attempt

    fn[A, E : Error] attempt(msg : (Result[A, E]) -> Cmd, f : async () -> A raise E) -> Cmd

    Create a command that runs an async function and handles errors.

    Similar to perform, but captures errors and passes Result[A, E] to msg.

    batch

    fn batch(cmds : Array[Cmd]) -> Cmd

    Combine multiple commands into one command.

    custom_cmd

    #alias(raw_effect, deprecated="`raw_effect` is deprecated, use `custom_cmd` instead")
    fn custom_cmd(callback : (&Scheduler) -> Unit, kind? : EffectKind) -> Cmd

    Create a low-level effect command for runtime and FFI integration. Consider use higher-level helpers like perform, delay, @http.post(), and @dialog.show() in app code.

    This function allows you to write wrappers that encapsulate FFI in a command when the functionality is not provided by Rabbita yet.

    Parameters:

    • callback: receives the runtime Scheduler, so it can enqueue follow-up commands (for example, after an async callback).

    • kind: determines when the effect runs.
      • Immediately: run as soon as the runtime receives it.
      • AfterRender: run after DOM patching for the current flush completes.

    Example:

    extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"

    pub fn delay(cmd : Cmd, ms : Int) -> Cmd {
    raw_effect(scheduler => set_timeout(() => scheduler.add(cmd), ms))
    }

    delay

    fn delay(cmd : Cmd, ms : Int) -> Cmd

    effect

    fn effect(f : async () -> Unit noraise) -> Cmd

    Create a command that runs an effectful async function

    host_capability

    fn host_capability(name : String) -> HostCapability?

    问宿主:"这个能力你有平台实现吗?"

    名字是能力名("visibility" / "dimensions" / "clipboard" …), 不是标签名也不是组件名 —— 与 库名:组件名 那套(I 轨道)是两个不同的名字空间: 组件通道解决"渲染一个 UI 组件",本通道解决"取一个平台能力"。

    host_has_dom

    fn host_has_dom() -> Bool

    这个运行时有没有 DOM?

    为什么需要它(与"宿主能力"是两个不同的问题)

    宿主能力回答的是"你有没有这个能力的实现";这个函数回答的是 "浏览器到底在不在"。两者互补,不能互相替代:

    问题谁来答
    host_capability(name)这个能力你有替代实现吗?宿主登记
    host_has_dom()这个运行时是不是浏览器?运行时事实,不需要谁声明

    有一个具体场景只有这条能解:有些 js-only 的订阅即使在 RN 上也没法"换成别的实现", 它只是不该去碰 DOM。典型是 on_url_changed —— 它的宿主机制 (Scheduler::set_url_changed_injector)本来就在,宿主驱动即可; 但老代码无条件去 @dom.window().add_event_listener("popstate", …), 而 RN 上 window 存在、addEventListener 不存在 → 直接抛。 这种情况下"宿主没登记能力"并不等于"没有 DOM",两件事必须分开问。

    判据为什么要查 addEventListener 而不只查 document 是否存在

    RN 把 global.window 指到了 globalThis(global.window = global),所以 window 一定存在、document 一定不存在。但只查 document 还不够稳: 有些非浏览器环境会挂一个残缺的 document 壳。要求 document.addEventListener 是个函数,才是"能真的挂监听"的判据。

    ⚠️ 它是运行时探测,不是编译期判断 —— 因为 #cfg(target="js") 分不开 Web 与 RN (两边同一个 target,见本文件顶部)。

    immediately

    let immediately : EffectKind

    none

    let none : Cmd

    A command that does nothing.

    perform

    fn[A] perform(msg : (A) -> Cmd, f : async () -> A noraise) -> Cmd

    Create a command that runs an async function.

    The async function f is executed, then its result is converted into a new command by msg and scheduled back into the update loop.