Skip to content

objectql: transaction()'s ambient txStore has no WebContainer AsyncLocalStorage polyfill, so rollback silently covers nothing there #7880

Description

@huangyiirene

Observation-class finding, noticed while implementing #7724 (PR #7879). Not a defect anyone hits in a normal deployment — filing it because the failure mode is the silent kind.

What

ObjectQL.transaction() (ADR-0034) publishes its driver handle into an AsyncLocalStorage-backed ambient store (txStore), and buildDriverOptions reads it as the safety net so internal queries and nested writes join the open transaction without being threaded explicitly:

const tx = execCtx?.transaction !== undefined
? execCtx.transaction
: this.txStore.getStore()?.transaction;

packages/plugins/plugin-auth/src/auth-manager.ts already documents, and works around, the fact that this does not hold everywhere:

WebContainer reports itself as Node.js but runs inside a browser. Several Node APIs are polyfilled with significant behavioural differences — most notably node:async_hooks.AsyncLocalStorage, whose run() does NOT propagate the store across await boundaries the way Node's native implementation does.

That file installs WebContainerRequestStateAsyncLocalStorage to repair better-auth's request-state ALS. txStore has no equivalent.

Why it is worth recording

If AsyncLocalStorage.run() does not survive await in that runtime, then inside a transaction() callback every ambient lookup returns undefined, so the writes execute outside the transaction — and then commit or roll back an empty transaction. The caller is told the unit of work rolled back; the rows are still there. That is the shape AGENTS.md singles out: the system looks normal while something it claims about persistence is not true. Unlike the missing-beginTransaction case, there is no warning — warnTransactionUnsupported does not fire, because the driver does support transactions.

Every transaction() caller inherits this equally (CRM convertLead, publishPackageDrafts, batchData's atomic gate, and now #7724's subject-erasure wrapper). It is not specific to any of them.

Not verified, deliberately

I did not reproduce this in a WebContainer — I have no such runtime here, and I did not want to assert a measurement I had not made. What is verified is only that (a) the repo states this ALS caveat in its own words, and (b) txStore has no polyfill while better-auth's request-state store does. Whether the gap actually bites depends on WebContainer's current polyfill, which may have improved.

So the first step is measurement, not a fix: run one transaction() whose callback writes and then throws, under WebContainer, and check whether the row survived.

No pm:queue

Dormant for real deployments — WebContainer is a StackBlitz dev/demo runtime. Filed unassigned for triage rather than sat on, per the filing-discipline rule that severity judged at filing time is unreliable in both directions.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions