Repository navigation
feat(tx): owned write transactions and browser transactions (#824) - #885
Merged
Merged
Conversation
Language bindings need a write transaction they can hold in an object that outlives a borrow and is used, committed or freed on any thread. The borrowed WriteTransaction borrows the database, is not Send and is tracked by a thread-local flag, so no binding can hold it. Minigraf::begin_write_owned / begin_write_owned_with(TxOptions) return an OwnedWriteTransaction: Send + 'static, keeps the database open, and stages, reads and commits exactly as WriteTransaction does (both now share TxState and the small-transaction Staged buffer). The write lock becomes Mutex<WriteSlot> + Condvar: while an owned transaction holds it, every other writer waits in Inner::lock_write. Its begin thread is recorded per database, and the calls WriteTransaction refuses on its thread are INT-001 there, whichever thread uses the transaction. BrowserDb.beginWrite returns a BrowserWriteTransaction (sync execute, commit() Promise resolved after the IndexedDB flush, rollback). While it is open, BrowserDb's read/write calls and prepared runs are INT-001; a finished transaction is API-018. No new error codes and no format change; INT-001's text now says "write transaction". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2a7QouMzKp3tAWxgVsxRf
…tement (#824) An owned transaction's statement holds the write mutex while it runs. On a thread other than the begin thread, a user function calling back into the database passed the begin-thread guard: a write locked the mutex its own thread held (a permanent deadlock), and a query read outside the transaction. The running thread now sets the reentry flag for the statement's duration, so such calls are INT-001 and the query fails with QRY-014, as on the begin thread. BrowserDb.beginWrite checks for an open transaction before its mutable borrow, so a user function calling it during a transaction's query is refused instead of panicking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2a7QouMzKp3tAWxgVsxRf
This was referenced Nov 11, 2026
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Core part of #824: explicit write transactions that language bindings can hold.
What changes
Minigraf::begin_write_owned()/begin_write_owned_with(TxOptions)→OwnedWriteTransaction:Send + 'static, keeps the database open,execute/commit/rollback, drop rolls back. It may begin on one thread and run, commit, roll back or be dropped on another.WriteTransaction(both shareTxState; small staging isStaged). The borrowed transaction's public API is unchanged (cargo semver-checks: no change required).Mutex<WriteSlot>+Condvar. While an owned transaction holds it, other writers (writes, rules, checkpoints,verify,rebuild_indexes, both begins) wait inInner::lock_write; queries don't take it.INT-001: the calls aWriteTransactionrefuses on its thread are refused on an owned transaction's begin thread (per database), and on the thread running one of its statements, so a user function calling back getsINT-001(QRY-014for the query) instead of deadlocking.BrowserDb.beginWrite()→BrowserWriteTransaction(syncexecute,commit()Promise resolved after the IndexedDB flush, syncrollback; freeing it open rolls back). While open,BrowserDb'sexecute,query,prepare,checkpoint,importGraph, anotherbeginWriteand prepared runs are[INT-001]; a finished transaction is[API-018].docs/superpowers/specs/2026-11-10-824-binding-transactions-design.md.Tests
tests/owned_tx_test.rs(16): equivalence with the borrowed transaction (5 sequences × commit/rollback/drop × small/large × memory/file, + reopen), cross-thread commit/rollback/drop, guards, waiting writers, non-waiting readers, other databases unaffected, begin refusals, user functions off the begin thread.src/db.rs(2): a commit refused at commit time releases the lock; a poisoned lock still ends the transaction.src/browser/tx_tests.rs(12 wasm-bindgen): staging, a differential check against the native transaction, IndexedDB commit/rollback/drop + reopen, failed flush, guards, finished transactions, user functions.Verification: full default suite 2,138 passed / 21 ignored; headless Firefox 80 passed; clippy (all-features, both wasm targets) clean; dist binary 1,268,984 B on rustc 1.99.1 (gate 1,269,760 B).
Binding, template and docs-site PRs follow, pinned to this branch until it merges.
Refs #824
🤖 Generated with Claude Code
https://claude.ai/code/session_01H2a7QouMzKp3tAWxgVsxRf