# RPC (`@rpc` + `rpc`) For direct, type-safe calls between runtimes, Deft uses **`@rpc`** to expose a function and **`rpc`** to invoke it. Unlike `pub/*` (broadcast, fire-and-forget), RPC is one-to-one and returns a future you can `await`. RPC is a runtime-level peer of `pub/*`: it lives on the `Runtime` (alongside the pub/sub broker and the config registry) and is available in **every host** — the TUI, the standalone `deft` scripting binary, cluster workers, and named child runtimes spawned via `runtime/spawn`. ## The Model 1. A runtime marks functions with `@rpc true` (plus optional `@doc`). 2. When a namespace containing `@rpc` closures is loaded (`tui/run`, AppThread launch, or a standalone file load), the runtime scans it and registers the methods in its **RpcRegistry**. 3. Any other runtime invokes a registered method via `rpc`, which returns a Future resolving to the method's return value. 4. The caller resolves the future with `await`.
 # Producer: packages/editor/src/editor.dft
def cmd-open-file {|path^string|
    @rpc true
    @doc "Open a file in this editor panel."
    [load-file $path]
} 
 # Consumer: packages/git/src/git.dft
await [rpc "editor" "cmd-open-file" $full-path] 
## Identity: the runtime name `[rpc target method ...]` addresses the target by **runtime name** — the `id` the runtime was created with (`RuntimeConfig.id`). In the TUI, each AppThread's runtime is named after its app (the auto-numbered app name, which is also the panel tag), so the same string you would pass to `tui/find-panel` is the RPC target:
 [rpc "editor" "cmd-open-file" $path]    # the "editor" runtime
[rpc "diff"  "load-diff" $l $r]         # the "diff" runtime 
Runtime names are unique across live runtimes, so `(runtime-name, method-name)` uniquely identifies a method even with multiple instances of the same app ("editor", "editor2", ...). For ad-hoc runtimes, the name is whatever was passed to `runtime/spawn` or `createChild`. ## Exposing a Method (`@rpc`) Add `@rpc true` to any function in your app's namespace. The runtime picks it up automatically after load.
 def load-diff {|left-path right-path|
    @rpc true
    @doc "Load a side-by-side diff into this panel."
    @params %{ :left-path "string" :right-path "string" }
    # ... load and display the diff ...
    :ok
} 
The runtime extracts: - The method name (from the `def` name). - The description (from `@doc`). - The parameter names and type hints (from the closure signature). These go into the RpcRegistry as metadata that callers can introspect via `rpc/methods`. ## Calling a Method (`rpc`) `[rpc runtime-name method-name args...]` returns a Future. `await` it for the return value, or ignore it for fire-and-forget.
 # Await the result
set opened [await [rpc "editor" "cmd-open-file" $path]]

# Fire-and-forget
rpc "editor" "cmd-goto-line" 42 
The method name is a string — typos fail at call time with a rejected future ("method not registered for that runtime"). Parameter type mismatches fail at the target runtime with a runtime type error in the caller's future. ## Listing Available Methods `[rpc/methods]` lists all registered methods across the shared registry. `[rpc/methods runtime-name]` lists methods exposed by one runtime.
 [rpc/methods]
# => @{ %{:runtime "editor" :name "cmd-open-file" :description "Open a file..."} ... }

[rpc/methods "editor"]
# => @{ @{:name "cmd-open-file" ...} @{:name "cmd-save" ...} } 
This is what powers RPC discovery UIs (e.g. the Alt+X command palette of available cross-runtime actions). ## The RpcRegistry The registry lives at the **runtime layer** (`src/host/runtime/rpc_registry.zig`), mirroring the pub/sub broker. Each Runtime owns one instance, and child runtimes share the root's via `shared_rpc_registry` so every panel and script sees one global method table. Entries are keyed by **owning runtime name + method name**. When a runtime shuts down (an AppThread exits, a child is destroyed), its entries are evicted via `unregisterByRuntime` so the registry never holds a stale `*Runtime`. Limits: `MAX_METHODS = 256`, `MAX_PARAMS = 8`. ## Return Values and Futures `rpc` allocates a Future on the caller's heap. The target runtime runs the method on its own driver thread (an AppThread's appWorker, or the runtime's loop thread); the result is shipped back via a thread-safe queue, parsed from `deft/stringify`, and used to resolve the Future.
 try {
    set result [await [rpc "editor" "cmd-get-buffer-text"]]
    echo "got back $($result(len)) chars"
} catch e {
    echo "RPC failed: $e~message"
} 
If the target runtime throws, the Future rejects with the error value. ## Patterns ### Editor hand-off Editor, git, find, files, and log apps all use RPC to ask the editor to open files. Because the panel tag equals the runtime name, no `tui/find-panel` indirection is needed — address the editor directly:
 rpc "editor" "cmd-open-file" $path 
See `packages/git/src/git.dft`, `packages/files/src/files.dft`, `packages/find/src/find.dft`, `packages/log/src/log.dft` for production usage. ### Bidirectional coordination Diff and editor apps both expose RPC methods and call each other:
 # Diff runtime exposes
def load-diff {|left right| @rpc true ... }

# Editor runtime calls
rpc "diff" "load-diff" $left-path $right-path 
### Defensive calling Wrap calls in `try` if the target runtime might not exist:
 try {
    [rpc "editor" "cmd-goto-line" ($ln - 1)]
} catch _e { nil } 
### Standalone / multi-runtime RPC works the same outside the TUI. A script that spawns a named child runtime can call its `@rpc` methods directly:
 def child [runtime/spawn "worker" "apps/worker/worker.dft"]
set r [await [rpc "worker" "process" $job]] 
## Compared to `pub/*` | Aspect | `rpc` | `pub/*` | |---|---|---| | Coupling | Tight — caller knows target runtime + method | Loose — broadcast, any subscribers | | Direction | One-to-one | One-to-many | | Hosts | All hosts | All hosts | | Return value | Future resolving to value | None (fire-and-forget) | | Type safety | Some (validated against `@rpc` + closure sig) | None | | Discoverable at runtime | Yes (`rpc/methods`) | By convention only | Use RPC for tight runtime-to-runtime coordination (editor hand-off, diff↔editor, parent↔worker). Use pub/sub for broadcast events (focus changes, config updates, file system changes). ## Limitations - **String method names.** Typos fail at runtime, not compile time. - **Max 8 parameters per RPC method** (registry limit). - **Max 256 methods per registry.** - **No streaming.** For incremental data, use a series of `pub/publish` calls or `deftask` with `@stream`. ## Quick Reference | Form | Purpose | |---|---| | `@rpc true` (on a def) | Expose a method | | `[rpc runtime-name method-name args...]` | Invoke a method (returns Future) | | `[rpc/methods]` | List all registered methods | | `[rpc/methods runtime-name]` | List methods for one runtime | | `[await [rpc ...]]` | Block for the result | | `[try { rpc ... } catch e { ... }]` | Defensive calling |