# State model

liebstoeckel keeps several distinct pieces of state, each with its own **owner**, **transport**, and **authority**. This page is the map.

![liebstoeckel state handling](https://docs.liebstoeckel.app/diagrams/state-handling.svg)

## Standalone vs. live

On load the engine reads `window.__LIEBSTOECKEL_LIVE__` (a `<script>` the **live server injects**):

- **absent → standalone.** The deck is a plain `.html`. Nav syncs between the presenter window and the audience window over **`BroadcastChannel`** (same machine only). Plugins render their **fallback** (no shared state).
- **present → live.** The deck connected to a live server. Nav + plugin state live in a **shared Yjs document** synced over **WebSocket**, so they cross devices. Each client has a `role` (presenter / viewer) from its URL token.

The *same built `.html`* works both ways. See [Live presenting](https://docs.liebstoeckel.app/guides/live/).

## State inventory

| State | Owner | Transport | Authority | Persistence |
|-------|-------|-----------|-----------|-------------|
| Slide **index / step / total** | `Deck` / `PresenterView` controller | **live:** Yjs `deck` map · **standalone:** `BroadcastChannel` | presenter writes, everyone reads | ephemeral (per session) |
| **Plugin shared state** (e.g. poll `votes`) | the plugin, via `pluginState()` | Yjs `plugin:<id>` map (CRDT), or `plugin:<id>:<instance>` for a named instance | **any** participant writes (incl. read-only viewers); presenter-only actions gated in plugin code | ephemeral (lost on server restart) |
| **Brand** (`data-brand`) | `Deck` / `PresenterView` | per-window React state | local | n/a |
| **Elapsed timer** | `PresenterView` | per-window React state | local | resets per window |
| **Participant id** | client | `sessionStorage` | local | survives reload (1 browser session = 1 participant) |
| **Overlays** (help / blur / overview / QR / jump buffer) | `Deck` | per-window React state | local UI | n/a |

## The two nav controllers (same shape, different transport)

`Deck` and `PresenterView` both pick a controller and call `next()` / `prev()` / `setIndex()` on it:

- **standalone →** `useDeckSync(count)`: `BroadcastChannel` carries `{ index, step, total, startedAt }`. A newly opened window broadcasts `request`; the others reply, so it snaps to the live state.
- **live →** `useLiveDeck(doc, count, canDrive)`: a Yjs `deck` map holds `{ index, step, total }`. **Viewers (`canDrive = false`) can't write**, so they follow; the presenter drives. `next()`/`prev()` read the freshest doc state (no stale-closure on rapid presses) and apply step/slide logic.

In both, **steps** reveal before the slide advances; the **presenter view shows the step counter**, the audience just sees the reveals.

## Plugin state authority

Plugin state is a CRDT, so writes merge and there's no central lock. We assume **non-malicious clients**: the server does basic checks only and drops oversized/garbage frames, but does not police *which* field a client writes. "Presenter-only" semantics (e.g. close-voting) are enforced in the plugin's own UI/logic by checking `role`, not by the transport.

## Resilience

- **Reconnect:** `connectLive` auto-reconnects with capped exponential backoff and re-pushes local state on reopen. A clean network blip recovers without a reload.
- **Half-open detection:** the server sends a keepalive (~25 s) and Bun pings idle sockets; the client runs a watchdog and force-reconnects if no frame arrives within `staleMs`. A silently-dead (mobile-sleep / NAT-timeout) socket recovers instead of hanging.
- **Bad frames:** every `Y.applyUpdate` boundary (client message, server `Hub.recv`) is wrapped, and the server caps inbound frame size. One malformed update can't disrupt the session.
- **Broadcast isolation:** the relay guards each per-peer `send`; a failing peer is dropped rather than starving the broadcast to the others.
- **Late join:** the server sends the full Yjs state to any newcomer, so opening a link mid-session lands you on the current slide with current plugin state.

## See also

- [Live presenting](https://docs.liebstoeckel.app/guides/live/): sessions, tokens, roles, the windows, and controller selection.
- [Relay](https://docs.liebstoeckel.app/guides/relay/): taking a session to the public internet without shipping deck code to the relay.
- [Plugins overview](https://docs.liebstoeckel.app/plugins/overview/) and [State & sync](https://docs.liebstoeckel.app/plugins/state-and-sync/): authoring shared state, fallback, and theming.