🥮 mooncakes.io

    #Subscriptions

    The sub package models long-lived external signals that should feed back into your TEA update loop.

    Unlike Cmd, which runs once, a Sub stays installed until your app stops returning it.

    #Basic shape

    In Rabbita, subscriptions are usually returned from the subscriptions callback of create_state.

    ///|
    fn subscriptions(model : Model, emit : Emit[Msg]) -> @sub.Sub {
    if model.running {
    @sub.every(1000, emit(Tick))
    } else {
    @sub.none
    }
    }

    #Building subscriptions

    Use none when nothing should be active:

    ///|
    test "sub none" {
    let _ : Sub = @sub.none
    }

    Use batch to combine multiple subscriptions:

    ///|
    test "sub batch" {
    let _ : Sub = @sub.batch([none, every(1000, @cmd.none)])
    }

    #Emitting messages

    Event-based subscriptions usually work with emit.map(...), just like HTML event handlers.

    ///|
    enum Msg {
    Resized(ViewPort)
    MouseMoved(Mouse)
    }

    ///|
    fn subscriptions(_model : Model, emit : Emit[Msg]) -> @sub.Sub {
    @sub.batch([
    @sub.on_resize(v => emit(Resized(v))),
    @sub.on_mouse_move(m => emit(MouseMoved(m))),
    ])
    }

    RunningSub

    pub(all) struct RunningSub {
    unload : (&
    Scheduler
    ) -> Unit
    update_tagger : (Error) -> Unit
    }

    Scope

    pub(all) enum Scope {
    Local
    Global
    }

    Sub

    type Sub

    A long-lived subscription managed by the Rabbita runtime.

    Use subscriptions to listen to external signals such as timers, window resize, scrolling, keyboard input, visibility changes, or WebSocket events.

    平台可用性(别信"inert"那句话)

    上游原文是 "Browser event sources are inert on native targets"。 在 React Native 上这句不成立,而且两种失效方式差别很大(2026-09-21 实测):

    依赖RN 上表现
    document.*document 未定义抛 ReferenceError
    window.location / .historywindow 存在(global.window = global)但没有这些属性抛 TypeError
    window.innerWidth / scrollY属性是 undefined不抛,静默给 0 ← 最难发现

    所以最该先修的不是"无替代物"那几个,而是"静默给错值"的那两个 (on_resize / on_scroll)。完整的「哪端可用 + 替代物 + 失效形态」表在 tools/cap_platform.mjs,那张表是机器校验的(代码变了表没改 → 门红)。

    走通了的路子:on_visibility_change —— 先问宿主能力(MOBILE_HOST.native), 问不到再回退 DOM(见 cmd/host_native.mbt)。

    SubLoader

    Custom subscriptions loader.

    batch

    fn batch(xs : Array[Sub]) -> Sub

    Combine multiple subscriptions into one.

    If multiple subscriptions use the same internal key, the later one wins.

    custom_sub

    fn custom_sub(key : String, scope : Scope, payload : Error, loader : SubLoader) -> Sub

    Create a custom subscription

    every

    fn every(ms : Int, cmd :
    Cmd
    ) -> Sub

    Repeatedly enqueue cmd every ms milliseconds.

    平台:Web ✅ · RN ✅ —— setInterval 是标准 JS 全局,不是什么 DOM 能力。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    none

    let none : Sub

    A subscription that does nothing.

    on_animation_frame

    fn on_animation_frame(msg :
    Emit
    [Double]) -> Sub

    Subscribe to requestAnimationFrame ticks.

    The handler receives the browser timestamp for each frame while the subscription is active.

    平台:Web ✅ · RN ✅ —— requestAnimationFrame 也是标准 JS 全局。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_key_down

    Subscribe to document keydown events.

    The handler receives a normalized Keyboard value from @common.

    平台:Web ✅ · RN ❌ Web-only —— 移动端没有全局键盘。 RN 上会抛 ReferenceError: document is not defined(会抛,不是静默, 所以它比 on_resize 那类好查)。要键盘/手势请走 RN 的 responder 或手势通道。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_key_up

    Subscribe to document keyup events.

    The handler receives a normalized Keyboard value from @common.

    平台:Web ✅ · RN ❌ Web-only(同 on_key_down,会抛)。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_mouse_move

    Subscribe to document mousemove events.

    The handler receives a normalized Mouse value from @common.

    平台:Web ✅ · RN ❌ Web-only —— 触屏没有"指针移动"这回事 (onHoverIn/Out 只在 Web/桌面有意义)。RN 上会抛(document 不存在)。 拖拽类交互请走 RN 的手势通道,不要复用这条。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_resize

    Subscribe to window resize events.

    The handler receives the latest viewport width and height.

    平台:Web ✅(走 DOM 回退)· RN ✅(走宿主能力 native.geometry → Dimensions)。

    ⚠️ 它原来是静默失效那一类:window 存在(RN 做了 global.window = global), 但 window.innerWidth 是 undefined → 按 Int 接住就是 0×0 且不抛 —— 所以先修的是它(tools/cap_platform.mjs 会把"静默给错值"这类单独报出来)。 载荷经 subscribe_json 严格解:宿主字段名写错会当场报错,不会退化成 0。 ⚠️ 真机未验(visibility 那条验过了,这条只到逻辑层测试)。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_scroll

    Subscribe to window scroll events.

    This reports page-level scrolling on window, not scrolling of an inner element. The handler receives the current scroll offset and document scroll size as a Scroll value.

    平台:Web ✅ · RN ❌ 语义不成立(注意:不是「待接」)。

    Web 上它报的是文档级滚动;RN 没有文档级滚动 —— 滚动发生在每个 ScrollView 内部,事件是那个组件的 onScroll prop。所以它的替代物在组件通道(I 轨道), 不是在宿主能力通道:宿主能力换的是"某个能力的平台实现", 而这里换不掉的是"一个不存在的语义"。

    ⚠️ 老行为是静默给 0(window 存在但 scrollY 是 undefined,不抛)—— 最难发现的一类。现在装载时明确 abort 并给出替代做法,与 on_key_down 那类 "会抛"的行为对齐:宁可在开发期炸一次。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_url_changed

    Subscribe to browser location changes.

    This is an app-scoped subscription. It is only active when returned from the root cell's subscriptions callback; if a non-root cell returns it, the subscription is ignored.

    平台:Web ✅ · RN ✅ 机制已通(宿主驱动)。

    驱动方式是既有的 Scheduler 注入器:sub 把注入器交给宿主 (Scheduler::set_url_changed_injector),宿主在它自己的"URL 变化"时机调 Context::inject_url_changed(ReactHost 已实现)。

    ⚠️ 原来 RN 上装载就抛:老代码无条件又去挂 popstate (@dom.window().to_event_target().add_event_listener(...)), 而 RN 上 window 存在、addEventListener 不存在 → TypeError。 现在按 @cmd.host_has_dom() 分两条路:有 DOM 照旧自己挂 popstate(Web 一字不变), 没有 DOM 就一个 DOM API 都不碰,只把注入器留给宿主。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_url_request

    Subscribe to captured navigation requests from @html.a(...).

    This is an app-scoped subscription. It is only active when returned from the root cell's subscriptions callback; if a non-root cell returns it, the subscription is ignored.

    平台:Web ✅ · RN ✅ 机制已通(宿主驱动,同 on_url_changed)。

    同源判断需要一个"当前地址"当基准:有 DOM 时读地址栏(老行为), 没有 DOM 时问宿主(Context::get_origin —— ReactHost 已实现)。 原来那一句无条件读 @dom.window().current_url()(= window.location.href), 而 RN 上没有 location,注入一发生就会抛。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    on_visibility_change

    fn on_visibility_change(msg :
    Emit
    [Bool]) -> Sub

    Subscribe to document visibility changes.

    The handler receives true when the document is hidden, and false when it becomes visible again.

    平台:Web ✅(走 DOM 回退)· RN ✅(走宿主能力 native.visibility → AppState)。 这是第一条走宿主能力通道的订阅:两端同一份应用代码,真机验过(PLAN.md §3.6 的 N2)。 语义近似处(RN 的 inactive 算作不可见、只在变化时推送不补发当前值)见 npm/moobile-host/native-rn.js 的文件头。

    见 tools/cap_platform.mjs —— 「哪端可用」那张表是机器校验的(漂了就红)。

    Source Files