Turso Transaction Mode Guard
libSQL's client.transaction() defaults to deferred and client.batch() does too, which means the safe-looking one-liner is on the mode that loses writes. A deferred transaction takes no write lock until its first write, so a read-modify-write reads a snapshot, another connection commits underneath it, and the upgrade fails with SQLITE_BUSY_SNAPSHOT (extended code 517) that no busy timeout can rescue, because waiting cannot make a stale snapshot fresh: only restarting the transaction can. This runs the closure in write mode (BEGIN IMMEDIATE, the lock taken before the first read) and retries the whole closure rather than the failed statement, since a statement-level retry carries values read from a snapshot that has already been invalidated. The BEGIN itself sits inside the retry, because with BEGIN IMMEDIATE the lock acquisition is the single most likely place to see SQLITE_BUSY and a loop that starts after it leaves its commonest failure unretried. A classifier separates stale-snapshot (restart), lock-busy (wait and retry with full jitter) and fatal (never retry), which are indistinguishable without the extended code since all three arrive as SQLITE_BUSY or worse. Ships with a demo that races eight real connections against three seats on a real local WAL database and prints the real SQLite error codes. Pinned to @libsql/[email protected].
npx shadcn@latest add https://ui.aryank.space/r/turso-transaction-mode-guard.jsonInstalls from ui.aryank.space. To add it by hand, copy the files in Files below, or register the @compronents namespace via the docs.
Both transactions begin deferred, so neither holds a write lock while it reads. Both read 3 seats remaining. The first commits, and the second's upgrade fails with SQLITE_BUSY_SNAPSHOT: its snapshot went stale and no busy timeout can wait that away. The booking is lost while the seat sits unsold, and the customer sees an error on a flight that has room.
@libsql/[email protected]