Close the ambient transaction window when a transaction opens without writes (#3141) - #3145
Conversation
🦋 Changeset detectedLatest commit: 0d3e2d1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report for CI Build 33370625606Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 71.652%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 18.34%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | merge |
225.1 µs | 329.2 µs | -31.63% |
| ❌ | merge |
265.2 µs | 366.5 µs | -27.65% |
| ⚡ | reconcile: deep tree, 10 of ~12k paths subscribed |
3.8 ms | 3.4 ms | +10.11% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/ambient-transaction-capture (0d3e2d1) with next (28a1eaf)
Footnotes
-
132 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
… writes (#3141) Parking is flush-driven, but a transaction opened with no writes before its first suspension — an action whose first statements only await — scheduled nothing, so activeTransition and the adopted batch stayed armed across the async gap and captured the next unrelated work to arrive: an optimistic store's landing on a still-live lane was held until the stranger action settled, an unowned optimistic write rode that transaction instead of reverting, and deep()/per-key readers split-brained meanwhile. initTransition now guarantees a flush, enforcing the A26 containment ruling: the ambient window is one flush, period. Co-authored-by: Cursor <cursoragent@cursor.com>
… thread A source write entangled with downstream async that rejects must still commit — the error boundary handling the rejection is the transition resolving, not it failing. This pin came out of the router navigation investigation and was never landed; tests only, no changeset. Co-authored-by: Cursor <cursoragent@cursor.com>
7b61904 to
0d3e2d1
Compare
Summary
Fixes #3141.
actionwhose first statements only await — scheduled nothing.activeTransitionand the adopted batch stayed armed across the async gap, and the next unrelated work to arrive was adopted into a transaction it had nothing to do with. In 2.0.0-rc.4 | optimistic store, read in an effect, changes the visible rendering. and has inconsistent value when logged and when rendered. #3141's playground: the optimistic store's authoritative landing (on a still-live lane, since the generator keeps running past the yield) froze until the stranger 10s action settled; an unowned optimistic push rode that transaction instead of reverting at the flush; anddeep()/per-key readers split-brained — the DOM showed[1,2,3,1111](stale base + override) while the deep observer reported[3,2,1,1111].initTransitionends by scheduling a flush. The ambient window closes in one flush regardless of whether the transaction wrote anything. This is enforcement of the existing A26 containment ruling (SPEC-ASYNC-SEMANTICS, 2026-07-17, 2.0.0-beta.19: action(): writes after an internalawaitescape the transaction and commit mid-flight #2913), which explicitly rejected holdingactiveTransitionopen across await windows because "unrelated ambient writes… would be captured into the action's transaction and held until it settles" — pre-fix, that rejected behavior happened anyway through this gap. A26's accepted consequence (pre-await_transitionstamps rejoining) is untouched.optimistic-ambient-capture.test.ts) built on the two exact ingredients the capture needs — a bare-macrotask action open, and a store flight that stays pending past its yield (fails pre-fix on the captured landing, passes post-fix); a faithful web/jsdom port of the playground with real scaled timers and order-based assertions; and an observer-invariance suite. Spec + internals docs updated with the enforcement note.Follow-ups filed separately: the store's truth-flight owning its own transaction, and the
deep()-vs-per-key committed-read seam.Test plan
optimistic-ambient-capture.test.tsfails pre-fix (landing captured), passes post-fixMade with Cursor