SSE Channels & State Scopes¶
Reference for the host's Server-Sent-Events (SSE) channel and the state-scope token system that drives selective UI updates.
Source of truth.
racelink/web/sse.pyandracelink/domain/state_scope.py. This document is the readable summary distilled from../host/ARCHITECTURE.md§"UI Scope Matrix".
Channel¶
A single SSE endpoint at:
The client subscribes once on page load. The host pushes events as they happen — there is no client-driven polling.
Event types¶
event: refresh
data: {"what": ["devices", "groups"]}
event: task
data: {"name": "fwupdate", "stage": "UPLOAD_FW", "index": 3, "total": 8, "addr": "AABBCC"}
| Event | Payload | Purpose |
|---|---|---|
refresh |
{"what": [...]} — list of refresh tokens |
Tell the JS to reload some part of its model from the REST API. |
task |
{"name", "stage", "message", "index", "total", "addr", ...} |
Long-running task progress (Discover, Status poll, OTA, presets download). The frontend's updateTask dispatches by name. |
The two event types are independent — a long-running task can
update its task event many times without firing any refresh
event, and vice versa.
Refresh tokens (refresh.what payload)¶
The list inside refresh.what tells the frontend which API
calls to re-issue. Tokens are derived from state-scope tokens via
racelink/domain/state_scope.sse_what_from_scopes:
| Refresh token | Triggered by state-scope token | JS handler action |
|---|---|---|
groups |
GROUPS, DEVICE_MEMBERSHIP, FULL |
loadGroups() — refetch group list |
devices |
DEVICES, DEVICE_MEMBERSHIP, DEVICE_SPECIALS, FULL |
loadDevices() — refetch device list |
presets |
PRESETS |
Refresh preset dropdowns |
The NONE scope produces an empty list — no tokens fire,
suppressing any visible refresh.
State-scope tokens¶
racelink/domain/state_scope.py defines the token set. Every
save_to_db(scopes={...}) call carries one or more of these:
| Token | When to use |
|---|---|
FULL |
Initial load (load_from_db) or migration boot — rebuild everything. |
NONE |
Pure persistence, no visible change (e.g. "Save Configuration" button just flushes the combined key). |
DEVICES |
A device record changed but the device did not move between groups (rename, specials struct rebuild). |
DEVICE_MEMBERSHIP |
A device moved to a different group — affects group counts and any list embedded per group. |
DEVICE_SPECIALS |
A special config byte was written on a single device (startblock slot, etc.). No cross-UI effect on RH panels. |
GROUPS |
Groups added / renamed / removed — group-list-backed dropdowns must refresh. |
PRESETS |
WLED presets file or RL preset store reloaded — preset-list-backed selects must refresh. |
Two consumers — same scope token¶
The state-scope tokens drive two independent UI layers:
- The browser WebUI. Routed via the
refreshSSE event; tokens map throughsse_what_from_scopesto refresh tokens (table above). - The RotorHazard plugin. Routed via the
on_persistence_changedcallback intoRotorHazardUIAdapter.apply_scoped_update. The plugin's UI elements are bootstrapped once and then selectively re-registered per scope token. See../host/ARCHITECTURE.md§"UI Scope Matrix" for the full per-element table.
Both consumers receive the same scope token set on every state change. The dual fan-out is what makes the Host ↔ RotorHazard plugin layout's UI-update story consistent.
Rule of thumb for new call sites¶
When you call save_to_db(args, scopes=...), pick the narrowest
token set describing what actually changed. If you genuinely don't
know, pass {FULL} — but prefer to refactor so you do know.
The regression tests pin the scope mapping:
tests/test_state_scope.py(host) — pins token → SSE refresh token mapping.tests/test_ui_scope_routing.py(plugin) — pins token → RotorHazard panel-element refresh.
An accidental FULL regression in either codebase fails CI.
Concurrency¶
SSEBridge.broadcast is the fan-out. It snapshots the registered
clients under _clients_lock (a gevent.lock.Semaphore when
running under gevent, threading.Lock otherwise) and then puts
into each client's queue outside the lock. The audit's A4 fix
established this snapshot-then-fan-out pattern so a slow client
queue cannot starve other broadcasters or new SSE registrations.
If you need to broadcast from a non-request thread (e.g. the
gateway reader thread), call SSEBridge.broadcast directly — it
is thread-safe.
See also¶
../host/ARCHITECTURE.md§"UI Scope Matrix" — full per-element matrix.../host/docs/UI_CONVENTIONS.md— button vocabulary and toast / confirm conventions.WEB_API.md— the REST endpoints thatloadGroups()/loadDevices()/ preset refresh actually call.