Skip to content

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

Closed as not planned

Description

@huangyiirene

Restart-when: one write-then-throw ObjectQL.transaction() is run under WebContainer on the packages/drivers/driver-sqlite-wasm STACKBLITZ.md demo host — promote if the row survives (measured silent-persistence lie), close if ALS behaves; or WebContainer becomes a supported production posture

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.


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Triage: routed domain:engine-core (packages/objectql — txStore/buildDriverOptions); hold as finding stands. The filer's honesty is the grade: nothing is measured broken (deliberately unverified — no WebContainer runtime at hand), the exposure is dev/demo-runtime-only today, and the first step is a measurement (one transaction() callback that writes then throws, under WebContainer; did the row survive?), not a fix. The silent-empty-transaction failure shape is nasty enough that the card earns its place in the stock rather than a close.

    Restart conditions: (a) anyone running the WebContainer measurement (the plugin-auth WebContainerRequestStateAsyncLocalStorage workaround is the repair template if it bites); (b) WebContainer becoming a supported production posture; (c) any report of phantom-rollback behavior in StackBlitz demos.


    Generated by Claude Code

  3. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Finding-grading round: hold confirmed, with a sharper trigger than the card carried: run one transaction() that writes-then-throws under WebContainer — the shipped driver-sqlite-wasm/STACKBLITZ.md demo is a ready host — and attach the result to the next StackBlitz/dogfood pass. Promote on a measured silent-persistence lie; close if ALS behaves.

    ⚠️ Do not mistake engine.ts:2639 / engine-transaction-same-origin.test.ts:563 for coverage of this — that caveat is the ScopedContext/setImmediate gap (#5351), a different mechanism.


    Generated by Claude Code

  4. added
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  5. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    State-machine migration, maintainer-authorized (2026-08-13, live triage session: 「我授权你先迁标签」; semantics on #8449): graded hold with a sharp trigger (one write-then-throw transaction() under WebContainer on the STACKBLITZ demo host; promote on a measured silent-persistence lie, close if ALS behaves) — finding → pm:on-hold. Verdict, trigger, domain unchanged.


    Generated by Claude Code

  6. added
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed on Aug 19, 2026
  7. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    关 not_planned —— 维护者逐张复核 on-hold 卡时同意关闭;只影响在线演示,且从未实测

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T08:03Z。维护者 2026-09-23 在分诊会话里逐张复核 pm:on-hold 卡,对第二组的回复原文:「8006 保留,其他同意」。本卡在那一组里的建议是关闭,维护者同意了。

    呈给维护者的事实

    • 推测的机理没有变:ObjectQL.transaction() 通过 AsyncLocalStorage(txStore)把事务句柄放进环境上下文。在 WebContainer(StackBlitz,浏览器里跑的 Node)中,它的 polyfill 可能在 await 之后丢掉上下文 ⇒ 一次「先写后抛错」的事务,可能留下半截数据,却报告已经回滚。本地 main 上:txStore 仍没有 WebContainer 专用的 polyfill(packages/objectql/src/engine.ts),而 plugin-auth 给它自己的存储装了一个。
    • 从未实测:这需要有人在浏览器里跑一次 StackBlitz demo(packages/drivers/driver-sqlite-wasm/STACKBLITZ.md),而 agent 的执行环境跑不了 WebContainer。挂起 6 周,没有人跑过。
    • 影响面:只有在线演示运行时,⛔ 没有任何真实部署运行在 WebContainer 上。

    重开条件

    有人在 StackBlitz demo 里跑一次「先写后抛错」的 transaction(),并且那一行留下来了(实测到事务谎报回滚);或者 WebContainer 成了受支持的生产形态;或者有人报告在 StackBlitz 演示里见到了「回滚后数据还在」。修复的模板现成:plugin-auth 的 WebContainerRequestStateAsyncLocalStorage。

    关闭理由:not_planned,同时摘掉 pm:on-hold。


    Generated by Claude Code

  8. removed
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions