# Global Slots Plan — Deterministic-key broadcast state with explicit opt-in ## Overview Global slots let the server broadcast shared state (e.g. a live score) to all connected clients. They use a **deterministic key** (`"global:" + type_name`) so every client references the same slot. The existing slot machinery (signing, SSE push, stale-`as_at` auto-refresh) handles everything else — no new transport, no new channel protocol. Global slots are **explicit opt-in**: each entrypoint/action that needs a global calls `.set_global(type)` (current value) and/or `.subscribe_global(type)` (live SSE updates). Pages that don't opt in never receive the global. ## Design ### Deterministic key A global slot's key is `"global:" + type_name` (e.g. `"global:score"`). This is set as the slot's `id` at creation, so all existing methods (`get_slot`, `sign_slot`, `update`, `clear_slot`) work with it unchanged. ### Registration (startup) ```vala statum.global("score", new ScorePublic { home = 0, away = 0 }); ``` Creates the slot with the deterministic key and the initial model (mapped via `GObjectMapping`). Scope is `PAGE` (the server always has the authoritative copy; the client doesn't need to persist it). ### Explicit opt-in (entrypoints / actions) ```vala // Entrypoint: include the current value + subscribe for live updates return directives() .set_global("score") .subscribe_global("score") .set_typed("counter", Scope.PAGE, counter); // Action: update via update_held (client holds it from the entrypoint) return directives().update_held("score", s => s.home += 1); ``` `update_held` resolves the held slot's key (the deterministic `"global:score"`), mutates, signs, and pushes to **all** subscribed channels. The requesting client gets a `set` in the response; other clients get the SSE push. Clients that miss the push refresh on their next request via the stale-`as_at` auto-set. ### Background updates (no client request) ```vala // From a timer, webhook, or any non-request context yield state_service.update_global_typed("score", s => s.home += 1); ``` Pushes via SSE to all subscribers. Stale clients refresh on next request. ### What reuses existing machinery | Mechanism | How it works for globals | |---|---| | Signing | `sign_slot("global:score")` — same as any slot | | SSE push | `update("global:score", state)` → `push_set` to all channels subscribed to that key | | Stale `as_at` auto-set | HeldSlotResolver compares the client's `as_at` with `last_touched`; stale → auto `set` appended | | Subscribe/unsubscribe | `subscribe` directive with key `"global:score"`; cleanup on page close as normal | --- ## Tasks ### V-1: `StateService` global slot support — `src/StateService.vala` - `new_global_slot(string type_name, Scope scope, State state)` — creates a slot with `id = "global:" + type_name`, caches it. If the key already exists (e.g. on restart), updates the current state. - `update_global(string type_name, State state)` async — delegates to existing `update("global:" + type_name, state)` (sign + push). - `update_global_typed(string type_name, owned UpdateMutator mutator)` async — typed read-mutate-write (maps via `GObjectMapping`, preserves private). - `clear_global(string type_name)` async — delegates to `clear_slot`. ### V-2: DirectiveBuilder global methods — `src/DirectiveBuilder.vala` - `.set_global(string type_name)` — derives `"global:" + type_name`, calls `state_service.sign_slot(key)`, emits `set`. Signs synchronously at call time (sign_slot is sync). - `.subscribe_global(string type_name)` — emits `subscribe` with the derived key. ### V-3: StatumConfigurator.global() — `src/Statum.vala` - `global(string type_name, TPublic initial)` — injects `StateService`, maps the initial model via `GObjectMapping.to_properties`, calls `state_service.new_global_slot(type_name, Scope.PAGE, state)`. ### V-4: Example — `example/` - Add a global "announcement" slot to the example (simple string) to demonstrate the flow: registered at startup, `set_global` + `subscribe_global` in the home entrypoint, updated by a background timer. - No new page needed; the home page binds `stm-text="announcement.text"`. ### D-1: Documentation - **`model.md`**: add a "Global slots" section describing the deterministic-key model, explicit opt-in via `set_global` / `subscribe_global`, background updates via `update_global_typed`, and the SSE/stale-refresh fan-out. - **`getting-started.md`**: add `set_global` / `subscribe_global` / `update_global_typed` to the DirectiveBuilder and StateService tables; add a "Global state" quick-start example. ### T-1: Runtime test - Register a global at startup. - Verify the entrypoint (with `set_global` + `subscribe_global`) returns the global's `set` + `subscribe`. - Verify `update_held("global_type", ...)` in an action pushes the update (the requesting client gets `set`; a second client's stale `as_at` gets an auto-`set` on its next request).