Poll Controller (fallback transport)
src/lib/offline/poll.ts is the periodic-polling transport. It was the MVP real-time mechanism (ADR-006, spec 013) and is now the fallback that runs only while the live WebSocket is unavailable (ADR-011); the live path suspends it.
The diagram below shows the controller, its decision helpers, and the shared delta-read path.
flowchart LR
PC["PollController<br/>createPollController()"] --> SP["shouldPoll(online, hidden)"]
PC --> DS["decideSince()<br/>?since cursor"]
PC --> SK["surfaceKey(s)"]
PC --> BS["BASE_SURFACES<br/>lists, groups, me-tasks,<br/>me-steps, me-my-day"]
PC -->|"pollNow / refresh"| PULL["sync.ts pullAndReconcile*"]
PULL --> D1{{"D1 — source of truth"}}
PULL --> REC["sync-core.ts reconcilers"]
REC --> DX[("Dexie cache")]
LC["createLiveConnection()<br/>live.ts"] -->|"setTransport('live')<br/>suspends timer"| PC
PC --> ST["getPollState()<br/>subscribePollState()"]
Key pieces
Section titled “Key pieces”POLL_INTERVAL_MS(10 s) andSTALE_AFTER_MS(= 3× interval) — cadence and staleness window.BASE_SURFACES— the surfaces the controller refreshes (lists, tasks, steps, my-day, memberships).surfaceKey(s)— stable key per surface (cursor bookkeeping).decideSince(...)— compute the?sincecursor for a surface’s delta read.shouldPoll(online, hidden)— gate polling on connectivity + page visibility (no polling when offline or backgrounded).PollController/getPollState()/subscribePollState(listener)— the controller and its observable state for the status UI.
Relationship to live push
Section titled “Relationship to live push”The controller calls the same delta-read reconcilers as the live transport (see Sync-Core Reconcilers), so polling and push share one data path. When LiveConnection is connected, the fast poll is suspended; on disconnect it resumes, and every reconnect runs a catch-up read — preserving convergence and offline-first regardless of socket health (ADR-006 → ADR-011).
The controller’s setTransport() moves it between three cadence modes:
stateDiagram-v2
[*] --> legacy
legacy: "legacy — ~10 s timer (no live transport)"
fallback: "fallback — ~45 s timer (socket down)"
live: "live — timer suspended (LiveConnection drives refresh)"
legacy --> live: "setTransport('live')"
live --> fallback: "setTransport('fallback') on close"
fallback --> live: "setTransport('live') on reconnect"
live --> live: "refresh(surfaces)"
fallback --> fallback: "pollNow every ~45 s"
Related: LiveConnection WS Client (the primary transport), Sync-Core Reconcilers (the shared fold logic).