Skip to content

test(transactions): demonstrate ambient tx loss across await in mutate()#1522

Open
marius-se wants to merge 1 commit into
TanStack:mainfrom
autarcgmbh:test/mutate-async-ambient-tx-loss
Open

test(transactions): demonstrate ambient tx loss across await in mutate()#1522
marius-se wants to merge 1 commit into
TanStack:mainfrom
autarcgmbh:test/mutate-async-ambient-tx-loss

Conversation

@marius-se
Copy link
Copy Markdown

@marius-se marius-se commented May 11, 2026

🎯 Changes

Adds a test that demonstrates a mismatch between Transaction#mutate's documented contract and its implementation for async callbacks.

The contract

The docstring on Transaction#mutate (packages/db/src/transactions.ts:248-251) states:

If the callback returns a Promise, the transaction context will remain active until the promise settles, allowing optimistic writes after await boundaries.

What actually happens

mutate() invokes the callback synchronously and pops the transaction off the ambient stack in a finally block (packages/db/src/transactions.ts:287-298):

mutate(callback: () => void): Transaction<T> {
  // ...
  registerTransaction(this)
  try { callback() }                  // not awaited
  finally { unregisterTransaction(this) }
  // ...
}

When the callback is async, callback() returns its Promise at the first await, the finally fires immediately, and unregisterTransaction(this) removes the transaction from transactionStack before any post-await code runs. Collection operations after the await see no ambient transaction and either throw MissingInsertHandlerError (no onInsert) or create a brand-new direct transaction via the collection's handler.

Additional signals that the async path is unsupported today:

  • The callback parameter is typed () => void, not () => void | Promise<void>.
  • No existing test in packages/db/tests/ exercises an async mutate() callback. The async tests in transactions.test.ts cover mutationFn and commit(), not the mutate() callback.

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

The docstring on `Transaction#mutate` (packages/db/src/transactions.ts:248-251)
claims:

  "If the callback returns a Promise, the transaction context will remain
   active until the promise settles, allowing optimistic writes after
   `await` boundaries."

The implementation does not honor that contract: `mutate()` invokes the
callback synchronously and pops the transaction off the ambient stack in
a `finally` block (packages/db/src/transactions.ts:294-298), so any
collection operation executed after an `await` inside an async callback
runs without an ambient transaction.

This test asserts the documented behavior and fails today with
`expected length 2, got 1` — only the pre-`await` insert lands on the
transaction; the post-`await` insert is orphaned into a new direct
transaction via the collection's `onInsert` handler instead.

This commit only adds the failing test. CI will go red intentionally
until either the implementation is fixed to match the docstring or the
docstring is updated to match the implementation.
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