Patch Changes
-
51ffcb9:
refresh(target)now returns a promise for the target's next QUIESCENT state — the re-ask (and anything that supersedes it) has settled. Accessor targets resolve with the settled value; store targets resolve with the store node passed (nested targets re-ask the whole family but resolve with the node the caller was looking at). A failed re-ask rejects, soyield refresh(x)in an action throws back at the yield point and reverts like any failed step; an ignored promise never surfaces an unhandled rejection, keeping fire-and-forget refresh unchanged. Semantics are quiescence, not flight identity: a superseding refresh folds every waiter onto whatever finally lands. Inside actions, truth landing into the held transaction is staged; the promise settles then (matchingresolve()/until()delivery) and delivers the staged value — never the caller's optimistic override. The re-ask stays verdict-quiet (isPendingunchanged).Implementation is pay-for-use and shares
resolve()/until()'s effect machinery: the waiter is a microtask-delivered, direct-commit, authoritative-read effect over the marked node, deferred one microtask so same-tick refreshes still coalesce into a single re-ask. One new reader bit (CONFIG_FRESH_READ) makes the waiter's read pull a still-dirty source through recompute inline — it then parks on the re-ask's pending window (woken by the settle walk, which runs on every landing including equal-value ones) or serves the sync answer, instead of misreading the pre-re-ask value as settled.updateIfNecessarynow also refuses disposed nodes outright (#2983's bug class), and a disposed or non-derived target resolves immediately with its last value. -
28a1eaf: Clear a node's transition stamp when its pending value commits.
_transition
was only ever cleared for optimistic nodes and in one async settle;
everything else relied onreassignPendingTransition, which the completing
branch runs overbatch._pendingNodes.commitPendingNodesdrains that list
without clearing anything, so a node committed by an earlier drain kept
pointing at a transition that later finished.setSignalre-enters
el._transitionbefore it discovers a write changes nothing, and a loading
boundary rewrites the same flag on every drain pass, so a finished transition
was re-armed forever andflush()never ended: dev threw "Potential Infinite
Loop Detected", production has no counter on that loop and hung. The
re-entered transition also aliases the ambient batch's containers, so
initTransition's adoption pass could push into the array it was iterating
untilRangeError: Invalid array length. -
ca16891: Harden the transaction-stamp lifecycle around #3140 (companion to #3143, which clears
_transitionstamps when pending values commit).initTransitionnow refuses a transaction whose_donechain ends intrue— the belt for dead references that survive outside the commit path (merged forwarding chains, async settles racing completion), sincesetSignalre-opens a node's stamped transaction before the value-equal bail and re-activating a corpse spins the flush drain loop (dev threw the loop guard; production hung). The dev loop guard also now reports what kept the loop alive — transition done-state, queue counts, and the last staged node — instead of only that it happened. -
751f991: Attribution: derive honest
changedfor effect runs. Core executes effects with_equals: false, so every effect recompute reportedchanged: true— making effect waste invisible tocosts()(wastedMswas effectively memo-only, while compiled JSX bindings are effects: the fan-out waste of a naive selected-row implementation measured as zero). The engine now compares the effect's committed compute output against its own frame snapshot; identical output reportschanged: falseand accrues waste. Side-effect-only computes (undefinedoutput) are exempt. -
ed2fb43: Fix
flattenArrayoverwriting itsneedsUnwrapflag with a nested call's result instead of OR-ing it (#3133). UnderdoNotUnwrap, an accessor child (a<For>/<Repeat>/memo) followed at the same level by a fragment containing no functions reset the flag, soflattenreturned a plain array with the raw accessor still inside instead of the resolving wrapper. Every renderer crashed on the raw function: universal hosts received it ininsertNode(as reported), and the DOM renderer threwinsertBefore … parameter 1 is not of type 'Node'— the protective function branch remembered from 1.x dom-expressions does not exist in 2.0. Reported with the fix by @antoinevanwel; also submitted by @nickshiro. -
893b8f9: Fix a tracked
latest(() => isPending(...))probe created during an active new-question flight reportingfalsefor that whole flight (#3166). A latest() shadow created lazily mid-flight is born uninitialized, so the probe's uninitialized suspend-throw dropped it from the verdict collection — swallowed by latest()'s committed-value fallback, the probe cached the wrong verdict until the next flight. The suspend now defers to the parent source's initialization state: an initialized parent means latest() has a value to serve, so the shadow is collected and the probe reports the in-flight truth regardless of creation time. -
2023daa: Fix unrelated async work being captured by a lingering ambient transaction (#3141). Parking is flush-driven, but a transaction opened without any writes — an action whose first statements only await — scheduled nothing, so
activeTransitionand the adopted batch stayed armed across the async gap. The next unrelated work to arrive was adopted into a transaction it had nothing to do with: an optimistic store's authoritative landing would not render until the stranger action settled, an unowned optimistic write rode that transaction instead of reverting at the flush, anddeep()/per-key readers disagreed about the committed value in the meantime.initTransitionnow guarantees a flush, so the ambient window closes in one flush regardless of whether the transaction wrote anything — enforcing the A26 containment ruling. -
3e3676b: The owned-scope write guard's thrown message now names the owning scope (previously only the diagnostics channel carried it); CHEATSHEET no longer claims
untrackexempts owned-scope writes — the guard is owner-based and untrack only stops tracking (#3157) -
09bbe24: Fix a type error in refresh()'s quiescence waiter that broke declaration emit (
pnpm types): the waiter captured inside the effect's own compute is the effect node, so it is typedComputedrather thanOwner, matching whatdispose()takes. Type-only; no runtime change. -
88fa9d6: Fix derived optimistic stores permanently corrupting committed state when the source's draft writes ran while a caller's optimistic override was active (#3108). The source is the truth author: its draft reads — sync body and post-
await/yieldcontinuations alike — now serve the authoritative view instead of composing the caller's tentative overlay. Before, a generator continuation'sstore.pushreadlengththrough an action's optimistic row and landed truth at the wrong index, committing[null, row]. Values, array length, membership, keys, and descriptors all leave the authoritative view together, gated on the same authoritative-write postureensurePBalready used to seed authoritative drafts from committed truth. User setter drafts are unchanged and keep composing on the optimistic view (#2951). Not anuntil()bug, despite the report's shape — the corruption reproduced with a plain yielded promise. -
fa13761: Close superseded async-iterable flights at supersede time (#3122). The iterator close was registered only as an owner cleanup, and a recompute whose disposal rides the zombie-deferred channel drains it at commitPendingNode — which a verdict-held write defers until the SUPERSEDING flight settles, leaving the stale iterator running to completion. Iterator close is the cancellation hook for resource-shaped streams (fibers, sockets, subscriptions), so the flight teardown now also fires at recompute's
_inFlightrelease, keyed to flight identity; the owner cleanup stays as the death backstop. -
90603c5: Fix latest()-mode isPending probes answering differently depending on read order (#3104). Two probe-mode leaks in the verdict layer: latestRead's mid-tick shadow pull recomputed a stale shadow with the probe still live (collecting the parent and flipping the verdict to the held-write answer only when nothing had pulled the shadow earlier in the tick), and the probe's companion-verdict reads ran with an outer latest() window still active, building a shadow of the pending signal itself that later halted dev with the owned-scope write guard when a flush recompute wrote it.
-
a536e29: Truth landings under live optimism now FOLD instead of consuming (#3123, #2719, #3164). While any transaction retains optimism on a store family (or an armed signal), fresh authoritative data — continuation yields, foreign refetches, replacing landings alike — stages into the retaining transaction and reveals atomically when it settles, exactly like a signal landing under an active override. Ordinary readers keep the committed view until that reveal, so overlapping optimistic actions no longer flash each other's pending rows out, partial override coverage can no longer compose override + staged truth into a frame no timeline contains (GabbeV's union tear), and a pending add never ghosts onto the next dataset. Authoritative readers tunnel through the hold:
until()'s predicate andlatest()see the staged truth, and a landing staged under an active override wakes waiting authoritative observers instead of deadlocking. Key-matched rows keep their proxy identity across the fold, and the settle-time revert resyncs structural subscribers (mapArray) with the landed arrangement. This supersedes the interim #3123 retained-setter replay machinery, which is removed. -
4ee9e3b: A pending flight superseded by a synchronous settle now wakes its registered dependents (#3181). When an async source's settle is announced by a signal write in the same synchronous step in which its promise resolves (the cache-backed fetch shape — TanStack Query's adapter), the write recomputes the derive first and the flight lands pre-superseded, so the async landing's settle walk never ran: every dependent that suspended on the flight stayed flagged pending on a source that would never land. A memo over an in-place-reconciling projection was the visible casualty — it recovered to an unchanged value, so nothing re-notified, and readers that suspended through it re-parked on the dead source permanently.
recomputenow captures pending source-hood and runs the settle walk itself when a synchronous settle preempts the landing, the pending twin of the #2949 silent-error-recovery sweep. -
1ece086: until() now entangles the confirming transition with the awaiting action. When a foreign write (another action's landing, a stream echo, a store fold) flips an awaited until() predicate truthy, its staged nodes are stolen into the awaiting transaction and masked from ordinary readers until the joint settle, so the confirmation and the action's own reveal paint in one frame instead of tearing. latest() and authoritative reads still see the staged truth; non-flipping updates on watched sources reveal freely; the confirming carrier keeps its own async reporters so open streams cannot deadlock the awaiting action.
The store fold's held-truth mask and the entanglement mask are unified on one mechanism (CONFIG_HELD_TRUTH): staged confirming truth is bit-marked at its arming site, masked in the read paths, and revealed by a single post-revert wake at the holding transaction's settle — replacing the fold's WeakSet ledger, the GlobalQueue._heldTruthMasked hook, and the entangled-transition recompute pass. The wake running after optimistic reversion also fixes a settle-window frame that composed committed confirming truth with a not-yet-reverted override.
-
0c02d42: Add
until(fn, options?)— the acknowledgment primitive for mutations confirmed on a live data channel (sockets, subscriptions, live queries) rather than by the mutation's own response. Resolves the first time the reactive predicate settles truthy (falsy and pending both mean "not yet");yield until(...)from an action holds the transaction — and its optimistic state — open until the world confirms, with{ timeout }(TimeoutError) and{ signal }rejections throwing back at the yield point so failed holds revert like any failed action.The predicate reads the AUTHORITATIVE view, and the carve-out is exactly one layer deep: the caller's own optimistic overrides (values and structure) are invisible, so a tentative write can never satisfy its own ack — including on the single-primitive shape where the optimistic store is the live-fed store. Everything else reads normally, including uncommitted transition-staged data: truth landing into the open transaction (e.g. a
refresh()the action issued) stages and cannot commit until the hold releases, so refusing staged reads would deadlock the hold on its own data plane. The A17-silent "landing equals the override" paths wake authoritative readers only (CONFIG_AUTHORITATIVE_OBSERVED); the wakeup machinery is hook-installed at firstuntil()call so unused apps tree-shake it.Also fixes
resolve()(anduntil()) delivering stale values when their source settles into a held transaction: promise-delivery effects apply on a microtask (#2930) but their computed value staged with the transition, so the immediate apply read stale mainline state —resolvecould report pre-refresh data anduntilcould deadlock. Such effects now commit their value directly (CONFIG_DIRECT_COMMIT), keeping value and delivery on the same schedule; safe because effects are private leaves (no subscriber reads an effect's value).