# Pubsub
`pub/*` is Deft's inter-runtime message bus. Any runtime can publish
to a topic; any runtime can subscribe. The broker is shared across
the runtime hierarchy, so messages flow between the scripting binary,
child runtimes, and TUI app panels transparently.
Pubsub is the primary loose-coupling mechanism in Deft. It's always
available — in the standalone `deft` binary, the ANSI binary, and
the full TUI.
## The Broker
A single `PubSub` broker lives on the root runtime and is shared
with all children. Subscriptions are registered with the broker;
publishes dispatch through it.
- **Same-runtime delivery**: zero-copy — the subscriber receives the
original Value directly.
- **Cross-runtime delivery**: the message is serialised via
`deft/stringify`, queued on the target runtime's thread-safe
message queue, and parsed back via `deft/parse` on delivery.
Either way, the handler sees a consistent event map shape.
## `pub/*` Stdlib functions
| Function | Returns | Purpose |
|---|---|---|
| `[pub/subscribe "topic" "handler-name"]` | bool | Subscribe a named handler |
| `[pub/publish "topic" data]` | number | Publish; returns subscriber count delivered to |
| `[pub/unsubscribe "topic" "handler-name"]` | bool | Unsubscribe |
| `[pub/topics]` | list | List of active topics |
| `[pub/subscribers "topic"]` | number | Subscriber count |
## Subscribing
The handler is a **named function** looked up at delivery time via
`VM.resolveVar`. This indirection lets the broker deliver to handlers
in other runtimes (where a closure reference would be invalid) and
survive module reloads.
def on-config {|ev| @doc "React to a config change event." echo "config: $ev~data~key -> $ev~data~value" } pub/subscribe "config" "on-config"
The handler takes one argument — the event map (see "Event Map
Shape" below).
## Publishing
# 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"
The data value can be any Deft value that round-trips through
`deft/stringify` (scalars, strings, lists, maps, tagged maps,
keywords). Closures and native resources don't serialise.
## Event Map Shape
The handler receives a `%{:type :message :topic :data }`
map:
def on-event {|ev| echo "topic: $ev~topic" echo "data: $ev~data" echo "type: $ev~type" } pub/subscribe "file-open" "on-event"
| Field | Always present? | Value |
|---|---|---|
| `:type` | yes | Always `:message` |
| `:topic` | yes | Topic string |
| `:data` | yes | The published value (parsed back from string for cross-runtime) |
## Topic Naming
Topics are arbitrary strings. Conventions:
- **`/`-separated namespaces** for hierarchical topics:
`"ranger/files-deleted"`, `"diff/load"`, `"debug-trace"`.
- **Module-prefixed topics** to avoid collisions:
`"editor/file-saved"`, `"shell/eval-code"`.
- **Cross-cutting topics** without a prefix for system-wide events:
`"file-open"`, `"panel-focus"`, `"config"`.
Filter on prefix in the handler when you only care about a subset:
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"
Or subscribe to one topic per handler if you prefer direct dispatch.
## Built-in Topics
The platform defines a few topics with conventional payloads:
| Topic | Published by | Payload |
|---|---|---|
| `"config"` | `config/set`, `config/unset` | `%{:key :value :old}` |
| `"runtime/eval-result"` | `runtime/eval-async` | `%{:id :value :error}` |
| `"dialog-result-"` | `dialog/*` (TUI) | Encoded dialog result string |
| `"panel-focus"` | TUI workspace | Focused panel id |
| `"file-open"` | Editor / files panel / git / chat | File path string |
TUI apps publish dozens of additional topics — see the source of
`packages/editor/src/editor.dft`, `packages/files/src/files.dft`,
etc. for real-world examples.
## Unsubscribing
pub/unsubscribe "config" "on-config" pub/unsubscribe "ranger/files-deleted" "on-pubsub"
A runtime's subscriptions are automatically removed when the runtime
is destroyed — no explicit cleanup needed for short-lived children.
## Introspection
[pub/topics] # => list of active topic strings [pub/subscribers "config"] # => number
Useful for debugging "why isn't my handler firing?" scenarios.
## Examples
### Reactive configuration
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
### Cross-runtime worker dispatch
# 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"
### Real-world: editor file-open
def on-pubsub {|ev| match $ev~topic { "file-open" => [cmd-open-file $ev~data] "panel-focus" => [on-focus-change $ev~data] "debug-attach-ready" => [attach-debugger $ev~data] _ => nil } } pub/subscribe "file-open" "on-pubsub" pub/subscribe "panel-focus" "on-pubsub" pub/subscribe "debug-attach-ready" "on-pubsub"
## Compared To `rpc`
| Aspect | `pub/*` | `rpc` |
|---|---|---|
| Coupling | Loose (broadcast, any subscribers) | Tight (direct call to named fn) |
| Direction | One-to-many | One-to-one |
| Hosts | All (`deft`, `retra`, `retro`) | All hosts |
| Return value | None (fire-and-forget) | Future resolving to return value |
| Type safety | None (data is a value) | Some (`@rpc` decorator + spec map) |
Use `pub/*` when you want decoupling and fan-out; use `rpc` when
you need a typed return value (see [RPC](rpc)).
## Quick Reference
| Form | Purpose |
|---|---|
| `[pub/subscribe "topic" "handler-name"]` | Subscribe a named handler |
| `[pub/publish "topic" data]` | Publish a value |
| `[pub/unsubscribe "topic" "handler-name"]` | Remove a subscription |
| `[pub/topics]` | List active topics |
| `[pub/subscribers "topic"]` | Count subscribers |
| Handler signature | `{|ev| ... }` where `ev` is `%{:type :message :topic ... :data ...}` |