Skip to content

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()"]
  • POLL_INTERVAL_MS (10 s) and STALE_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 ?since cursor 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.

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).