# Working together
Plugins share state and talk through four channels on the account, and a
UI plugin works with the daemon half of itself, its *companion*. This page
says which to use, and how a UI plugin that runs in several viewers at once
stays correct.
The worked example is a pair: [`together_ui.rs`](../examples/together_ui.rs)
and [`together_companion.rs`](../examples/together_companion.rs)
(`standard-plugin new <id> --kind companion` scaffolds it).
## Four channels
| Values | `values()` | Yes, durable, per account | `Event::ValueChanged` after `values().watch(prefix)` | State: the latest build status, settings a plugin learned, anything a viewer opened later must see |
| Live | `live()` | No, latest value only | `Event::Live` after `live().subscribe(prefix)`; `Event::LiveDeleted` when the publisher withdraws a key (`live().delete(key)`) | Fast-changing readings where only the newest matters: a progress figure, a sensor reading. At most 16 KiB each: publish one key per item (a build, a pane) and only the items that changed, rather than one snapshot of everything. Withdraw a key that no longer applies (a pane that closed) rather than publishing `null` |
| Events | `events()` | No | `Event::Plugin` after `events().on(pattern)` | Something happened: "the status changed", "a build started". Listeners that are not running miss it |
| Calls | `calls()` | No | The companion's `DaemonPlugin::call` | Asking the companion to do something, and getting an answer |
Keys, live keys and event names are namespaced by plugin id: a plugin uses
`<id>.*` freely; `global.*`, another plugin's namespace and `system.*` need
grants ([grants](grants.md)). Payloads are JSON; the SDK serializes typed
values with `serde`.
A common shape combines two: the writer stores the state as a value *and*
emits an event saying it changed; readers read the value when they start
and re-read it on the event. A viewer opened later still sees the state,
and nobody polls.
### Writes before the account connection opens
A plugin can start before its account connection opens (a daemon
plugin does after every daemon start), and writes made then fail. Treat a
failed `values().set` or `live().publish` as not written: remember the
last payload that succeeded, and publish again on later events (an
account change, the interest, the next reading). Make the publish
idempotent: it builds the payload, compares it with the last one written,
and writes only when they differ. `issue-counts`'s daemon half does this
([agent guide](agent-guide.md#2-writes-fail-until-the-account-connection-opens)).
## Companions
A companion is a daemon plugin (manifest kind `companion`) with the same id
as a UI plugin. The UI half runs in every viewer and shows things; the
companion runs in `standardd` and does things:
- It holds the grants a UI plugin should not: `process.exec:`, file access,
panes, HTTP.
- It does work once. The UI half asks (grant `call:<id>`); the companion
answers in `DaemonPlugin::call`, and starts longer work as a task
(`cx.spawn`) that runs when the host next drives it. The UI half asks in
one of three ways:
| SDK | Waits | Answer |
| --- | --- | --- |
| `calls().send(method, request, target)` | No | None: the outcome arrives another way (a value, a live message) |
| `calls().call_async(method, request, target)` | No | `Event::CallResult { id, result }` with the returned `CallId`, after 10 s at most (`call_async_timeout` for another bound, 30 s at most) |
| `calls().call(method, request, target)` | Yes, 10 s at most (`call_json_timeout`, 30 s at most) | The return value; a timeout is `Error::Unavailable("call_timeout")` |
Prefer `send` and `call_async` in a UI plugin: `event` and `frame` keep
their budgets and the plugin keeps painting while the companion works.
`call` blocks the plugin's thread (never the viewer's), and in a browser
without JSPI it answers `unavailable`; `send` and `call_async` work in
every browser. At most 32 sends and async calls of one plugin are in
flight at once; past that they answer `Error::RateLimited`.
- It publishes results as values and events the UI halves read.
- It reads outside sources only while a viewer shows the UI
(`Context::interest()`, [daemon plugins](daemon-plugins.md#reading-only-while-someone-looks)).
The UI half never polls it: no timer, heartbeat or repeated `send` to
keep it reading. A call goes out for what the user did; everything else
arrives as values, live messages and events.
`Target` picks which daemon answers or hears:
| `Target::Singleton` | The plugin's singleton daemon instance: one per account, for work that must happen once |
| `Target::Machine(id)` | The daemon on one machine (work that belongs to that machine's files or panes) |
| `Target::Daemons` | Every daemon (fleet-wide events) |
| `Target::Viewers` | Every viewer (a companion announcing a change to the UI halves) |
| `Target::All` | Every listener |
A daemon knows its own machine (`Context::machine_id()`), so a companion
that publishes per machine keys its values and live messages by it
(`<id>.status.<machine>`) and the UI half matches them against
`account::state().machines`. A viewer knows its own machine too
(`view::machine_id()`, none in a browser) and its instance
(`view::instance_id()`, one per running viewer).
A daemon plugin enabled on several machines runs one instance per machine
(the fleet); the singleton is the one instance the account elects for
account-wide work, on the machine that holds its lease
([daemon plugins](daemon-plugins.md)).
## Running in several viewers
The same account can be open in several viewers at once: a laptop, a
desktop, a browser tab. Each runs its own instance of every UI plugin, so
a UI plugin must be **side-effect free**: painting, reading, and listening
are fine; anything that changes the account or the world must not simply
run in every instance.
Three tools keep it correct:
1. **`view::is_driving()`** is true in the viewer the user is controlling.
User-initiated effects (a click that starts a build) run only there;
`Event::Driving` reports changes.
2. **`claims().claim(key, ttl_ms)`** (or `claims().once(key, ttl_ms, effect)`)
makes an effect exactly-once across instances: only the instance whose
claim succeeds performs it. Use it even behind `is_driving()`, since
two viewers can briefly both believe they drive. Keys live in the
plugin's own namespace; no grant.
3. **A companion** for everything else: effects that are not user-initiated
(reacting to an event, a schedule) belong in a daemon, which runs once.
The **double-play** rule is the check for getting this wrong: a UI plugin
that listens to shared events and holds a grant that acts on the account
(`fetch:`, `values.write:`, `events.emit:`, `call:`) would perform the
action once per viewer per event. `standard-plugin check` warns about the
combination ([grants](grants.md#double-play)).
The account keeps a claim for seven days, first writer wins, whatever
`ttl_ms` asks; a later claim of the same key from the viewer machine that
holds it renews it (`true`). `release` changes nothing on the account yet.