# 发布/订阅 `pub/*` 是 Deft 的运行时之间消息总线。任何运行时都可以向主题发布;任何运行时都可以订阅。代理在整个运行时层级中共享,因此消息可以在脚本二进制、子运行时和 TUI 应用面板之间透明流动。 发布/订阅是 Deft 中主要的松耦合机制。它始终可用——无论是独立的 `deft` 二进制、ANSI 二进制还是完整的 TUI。 ## 代理 单个 `PubSub` 代理驻留在根运行时上,并与所有子运行时共享。订阅在代理处注册;发布通过它进行分派。 - 同运行时投递:零拷贝——订阅者直接收到原始 Value。 - 跨运行时投递:消息经 `deft/stringify` 序列化,排入目标运行时的线程安全消息队列,并在投递时通过 `deft/parse` 解析回来。 无论哪种方式,处理器看到的都是一致的事件映射形状。 ## `pub/*` 标准库函数 | 函数 | 返回值 | 用途 | |---|---|---| | `[pub/subscribe "topic" "handler-name"]` | 布尔值 | 订阅具名处理器 | | `[pub/publish "topic" data]` | 数字 | 发布;返回到达的订阅者数量 | | `[pub/unsubscribe "topic" "handler-name"]` | 布尔值 | 取消订阅 | | `[pub/topics]` | 列表 | 活动主题列表 | | `[pub/subscribers "topic"]` | 数字 | 订阅者数量 | ## 订阅 处理器是一个具名函数,在投递时通过 `VM.resolveVar` 查找。这种间接方式让代理可以投递给其他运行时中的处理器(在那些运行时中闭包引用将无效),并在模块重新加载后仍然有效。
def on-config {|ev|
    @doc "React to a config change event."
    echo "config: $ev~data~key -> $ev~data~value"
}

pub/subscribe "config" "on-config"
处理器接受一个参数——事件映射(见下文“事件映射形状”)。 ## 发布
# Publish a string
pub/publish "file-open" "/path/to/file.dft"

# Publish a structured value
pub/publish "user-loggedIn" %{ :user $user :at [clock/seconds] }

# Publish to a topic no one subscribed to — silently a no-op
pub/publish "orphan" "data"
数据值可以是任何能通过 `deft/stringify` 往返的 Deft 值(标量、字符串、列表、映射、带标签的映射、关键字)。闭包和原生资源无法序列化。 ## 事件映射形状 处理器收到一个 `%{:type :message :topic <string> :data <value>}` 映射:
def on-event {|ev|
    echo "topic: $ev~topic"
    echo "data:  $ev~data"
    echo "type:  $ev~type"
}
pub/subscribe "file-open" "on-event"
| 字段 | 始终存在? | 值 | |---|---|---| | `:type` | 是 | 始终为 `:message` | | `:topic` | 是 | 主题字符串 | | `:data` | 是 | 发布的值(跨运行时时从字符串解析回来) | ## 主题命名 主题是任意字符串。约定: - 以 `/` 分隔的命名空间用于层级主题:`"ranger/files-deleted"`、`"diff/load"`、`"debug-trace"`。 - 带模块前缀的主题以避免冲突:`"editor/file-saved"`、`"shell/eval-code"`。 - 横切主题不带前缀,用于系统范围事件:`"file-open"`、`"app-focus"`、`"config"`。 当只关心某个子集时,在处理器中按前缀过滤:
def on-pubsub {|ev|
    if [starts-with? $ev~topic "ranger/"] {
        echo "ranger event: $ev~topic"
    |
    }
}
pub/subscribe "ranger/selection-changed" "on-pubsub"
pub/subscribe "ranger/cwd-changed"       "on-pubsub"
pub/subscribe "ranger/files-deleted"     "on-pubsub"
或者如果偏好直接分派,也可以为每个主题单独订阅一个处理器。 ## 内置主题 平台定义了几个带约定载荷的主题: | 主题 | 发布者 | 载荷 | |---|---|---| | `"config"` | `config/set`、`config/unset` | `%{:key :value :old}` | | `"runtime/eval-result"` | `runtime/eval-async` | `%{:id :value :error}` | | `"dialog-result-<id>"` | `dialog/*`(TUI) | 编码后的对话框结果字符串 | | `"app-focus"` | TUI 工作区 | 获得焦点的应用:`"<面板 id>:<应用名>"` | | `"file-open"` | 编辑器 / 文件面板 / git / chat | 文件路径字符串 | TUI 应用会发布数十个额外主题——真实示例见 `packages/editor/src/editor.dft`、`packages/files/src/files.dft` 等源码。 ## 取消订阅
pub/unsubscribe "config" "on-config"
pub/unsubscribe "ranger/files-deleted" "on-pubsub"
运行时被销毁时,其订阅会自动移除——短生命周期子运行时无需显式清理。 ## 自省
[pub/topics]                                  # => list of active topic strings
[pub/subscribers "config"]                    # => number
对于调试“为什么我的处理器没有触发?”这类场景很有用。 ## 示例 ### 响应式配置
def on-config {|ev|
    if [starts-with? $ev~data~key "log/"] {
        echo "log config changed: $ev~data~key -> $ev~data~value"
        [reload-logger]
    |
    }
}
pub/subscribe "config" "on-config"

# Somewhere else in the system:
config/set "log/max-lines" 1000              # triggers on-config
### 跨运行时工作进程分派
# Parent runtime
runtime/create "worker"
runtime/spawn "worker" "src/worker.dft"

def on-work-done {|ev|
    @doc "Handle work-completion events from the worker."
    echo "worker finished: $ev~data~job-id"
}
pub/subscribe "work-done" "on-work-done"
# src/worker.dft (runs in "worker" runtime)
def do-job {|job-id|
    # ... process ...
    pub/publish "work-done" %{ :job-id $job-id :result $r }
}

pub/subscribe "work-request" "do-job"
### 真实示例:编辑器 file-open
def on-pubsub {|ev|
    match $ev~topic {
        "file-open"       => [cmd-open-file $ev~data]
        "app-focus"       => [on-focus-change $ev~data]
        "debug-attach-ready" => [attach-debugger $ev~data]
        _                 => nil
    }
}

pub/subscribe "file-open"          "on-pubsub"
pub/subscribe "app-focus"          "on-pubsub"
pub/subscribe "debug-attach-ready" "on-pubsub"
## 与 `rpc` 的对比 | 方面 | `pub/*` | `rpc` | |---|---|---| | 耦合 | 松散(广播,任意订阅者) | 紧密(直接调用具名函数) | | 方向 | 一对多 | 一对一 | | 宿主 | 全部(`deft`、`retra`、`retro`) | 所有宿主 | | 返回值 | 无(即发即忘) | 解析为返回值的 Future | | 类型安全 | 无(数据即值) | 部分(`@rpc` 装饰器 + 规格映射) | 当需要解耦和扇出时使用 `pub/*`;当需要带类型的返回值时使用 `rpc`(见 RPC)。 ## 快速参考 | 形式 | 用途 | |---|---| | `[pub/subscribe "topic" "handler-name"]` | 订阅具名处理器 | | `[pub/publish "topic" data]` | 发布值 | | `[pub/unsubscribe "topic" "handler-name"]` | 移除订阅 | | `[pub/topics]` | 列出活动主题 | | `[pub/subscribers "topic"]` | 统计订阅者 | | 处理器签名 | `{|ev| ... }`,其中 `ev` 为 `%{:type :message :topic ... :data ...}` |