Skip to content

fix(sqlite): stop holding a savepoint across local write authorization - #284

Draft
marcus-pousette wants to merge 2 commits into
mainfrom
fix/auth-local-write-interleaving
Draft

marcus-pousette wants to merge 2 commits into
mainfrom
fix/auth-local-write-interleaving

Conversation

@marcus-pousette

Copy link
Copy Markdown
Collaborator

Auth-checked local writes held a savepoint across await authorizeLocalOps, as separate runner calls. Anything else on the same connection could land inside it: another crossTab tab, sync appends, or a second auth write. On main:

  • a denied write rolls back writes that already resolved
  • a second concurrent auth write fails with no such savepoint after its op committed
  • reads during authorization see the op before it is authorized

Fix

The op is authorized before anything is written, and no transaction spans the await.

  • treecrdt_local_insert/move/delete/payload take an optional trailing token. With a token, the function writes only if minting again gives the same op with no op applied in between (blake3 of the op JSON and the materialized head_seq). Otherwise it writes nothing and returns the op with a new token and outcome: null. Passing NULL only mints.
  • createTreecrdtSqliteWriter mints, authorizes, then writes if unchanged, re-authorizing at most 4 times. Auth writes from the same replica take turns, and byte params are copied up front.

API change: only the SQL functions change; the adapter and extension must update together. LocalWriteOptions is unchanged, but authorizeLocalOps now sees the tree without the op, as peers and postgres-napi do, and may run more than once per write. A move from outside a scoped subtree is now denied locally, matching peers.

This is the mechanism from the legacy drafts #193 and #195, without proof storage. Storing op auth atomically with the op is still #256: rows the auth session writes during authorization are not tied to the commit.

Tests

These fail on main:

  • wa-sqlite node: the pending op stays hidden; a denied write keeps concurrent writes; concurrent auth writes take turns and both commit
  • browser e2e: a denied write in one crossTab tab keeps another tab's write
  • sqlite-node with a real SQLite auth session: a scoped move is checked at its source

New coverage for the token check (including a late remote op that leaves the op unchanged), the retry cap, and the payload copy.

Auth-checked local writes opened a savepoint, minted the op, awaited
authorizeLocalOps and released it as separate runner calls. Other calls on
the same connection (crossTab tabs, sync appends, other auth writes) ran
inside that savepoint: a denied write rolled them back, readers saw the
unauthorized op, and a second auth write failed after committing.

The local op SQL functions now take an optional authorized token. They mint
the op and write it only if the token still matches the op and the
materialized head; otherwise they return the op unwritten with a new token.
The writer authorizes before writing, lets auth writes of a replica take
turns, and gives up after four authorizations.
Main now opens OPFS databases in WAL mode (#283), so the summary includes journalMode.
@marcus-pousette
marcus-pousette force-pushed the fix/auth-local-write-interleaving branch from c7432c4 to 5c8942a Compare November 2, 2026 11:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant