# 发布/订阅
`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 ...}` |