Read from the UI
The UI never writes. It reads through its own read-only connection and reads
again whenever a source says something changed. Every database is opened in
WAL mode with synchronous=NORMAL, so a read never queues behind a source’s
commit.

In the chart a source commits 10,000-row passes back to back for three
seconds while the UI reads a page of 50 rows and the sync row every
millisecond. With pito-sync the UI got 2,305 reads, the slowest 2.84 ms. The
same code on a plain connection with SQLite’s defaults (rollback journal,
synchronous=FULL) got 88 reads, the slowest stuck behind the writer for
1.23 s.
Open a reader
Section titled “Open a reader”let Some(ui) = pito_sync::reader(&spec)? else { return Ok("not built yet".into());};reader(&spec) returns Ok(None) until a source has built the file, and
while the version stored in the file differs from the Spec’s, which happens
for a moment after an upgrade, before the source’s thread rebuilds it. It
returns Err only when the file exists and can’t be read.
Re-read on each Changed
Section titled “Re-read on each Changed”spawn and spawn_checked take the sending half of a channel. The UI owns
the receiving half and reads again on every Changed(name):
for Changed(name) in changes { println!("{name:<9} {}", show(&spec, &name)?);}A desktop or terminal app does the same in its event loop, with try_recv
on the channel so the loop never blocks.
Show “syncing…”
Section titled “Show “syncing…””A pass sends Changed when it starts and again when it ends. While it runs,
the source’s sync row has started_at later than finished_at:
let sync = pito_sync::sync(&ui, name) .map_err(|e| e.to_string())? .unwrap_or_default();if sync.started_at > sync.finished_at { return Ok("syncing…".into());}sync(&conn, source) returns None for a source that has never run;
unwrap_or_default() turns that into an empty row.
Read your own tables
Section titled “Read your own tables”The rest is plain SQL on your own tables through the same connection:
let (posts, newest): (i64, Option<String>) = ui .query_row( "SELECT count(*), (SELECT title FROM posts ORDER BY id DESC LIMIT 1) FROM posts", [], |row| Ok((row.get(0)?, row.get(1)?)), ) .map_err(|e| e.to_string())?;With the failure check added, this is the show
function of the feed example, and these are the lines it prints:
feed syncing…feed 6 posts, newest "One file per app"feed syncing…feed unreachable: the server did not answer (6 posts kept)