diff --git a/.changeset/fresh-loading-fallback-in-flush.md b/.changeset/fresh-loading-fallback-in-flush.md
new file mode 100644
index 000000000..ef486c3c1
--- /dev/null
+++ b/.changeset/fresh-loading-fallback-in-flush.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/signals": patch
+---
+
+A fresh `Loading` mounted over a held value shows its fallback in a flush too (#3540 under L2): a first pass a loading boundary that has not shown content catches is the boundary's, not the tick's, so the mount publishes and the content reveals at the commit. Content bound by a render effect under the boundary is collected, so the boundary no longer reveals empty content.
diff --git a/documentation/solid-2.0/05-async-data.md b/documentation/solid-2.0/05-async-data.md
index 63c67488c..a4e983b46 100644
--- a/documentation/solid-2.0/05-async-data.md
+++ b/documentation/solid-2.0/05-async-data.md
@@ -47,6 +47,8 @@ Importantly, `Loading` is intended to cover **branch readiness**: it handles a s
Nested `Loading` boundaries can be used to avoid blocking large subtrees and to control where loading UI appears.
+A `Loading` that has not shown content yet owns what is under it. Content there that reads a value a still-running change holds (a write inside an `action`, data another change is loading) is not part of that change: the boundary shows its fallback now, and the content appears when the change commits. The change does not wait for it. The exception is a boundary mounted by the change itself, for example a `` where `x` is the held write: it appears with that change's commit, with no fallback. While the fallback shows, `isPending()` of the boundary is `false`, as for any first load.
+
#### `Loading` `on` prop: dependencies that show the fallback again
By default, once a `Loading` boundary has rendered content, it keeps that content visible during revalidation: like every reader of a pending value, it holds the write that made it pending until the data lands. The `on` prop is a **dependency list**: a tracked expression whose value is irrelevant — what matters is what it reads. Whenever anything it reads changes, the boundary stops waiting on its current content: if something under it is pending, it shows its fallback again until the new content is ready; if nothing is pending, nothing happens.
@@ -81,6 +83,8 @@ The shell keeps showing A until `product(2)` lands; the spinner arrives with B,
One shape shows no fallback at all: when the data the boundary is waiting on is also read outside it (a sibling `` over the same `comments(id)`, an `isPending` on it in the header), or the write's `action` stays open until the data lands. The frame waits on that read, so by the time it commits the content is ready and the fallback was never needed. The first of these is structural — no ordering of the flights can show that fallback — and in development the `LOADING_ON_OUTSIDE_HOLD` diagnostic names the source; the fix is to move the outside read under the boundary so one hold owns the data. The second is a race the fallback may still win (an action that ends first shows it with the commit), and is not reported: during an `action`, show the wait with `isPending()` or an optimistic value, which is what a hold's stale content is for. The old content is on screen and valid the whole time; a `Loading` fallback says it is not.
+_Note (2026-10-06):_ how a re-armed boundary treats data an earlier change still holds is deferred to a separate change; until then a re-arm behaves as described here.
+
It is possible to show the fallback beside the still-held frame anyway: a display-ahead read in `on` — `latest(id)`, `isPending()`, an optimistic signal — says the change is already on screen, so the fallback lands there too:
```
diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md
index 00bbd1f2e..e7708a763 100644
--- a/packages/signals/docs/RULES-INDEX.md
+++ b/packages/signals/docs/RULES-INDEX.md
@@ -45,66 +45,66 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:311` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… |
-| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:319` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. |
-| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:327` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). |
-| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:335` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. |
-| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:343` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". |
-| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:351` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. |
-| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:157` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… |
-| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:165` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… |
-| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:173` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… |
-| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:181` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. |
-| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:99` | — | latest-isPending-consistency.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… |
-| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:189` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… |
-| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:197` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … |
-| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:205` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… |
-| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:241` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … |
-| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:213` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… |
-| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:67` | async.ts×3 constants.ts×1 lanes.ts×6 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
-| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:77` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
-| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:141` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… |
-| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:400` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… |
-| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:407` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… |
-| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:221` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … |
-| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:231` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… |
-| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:149` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… |
-| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:301` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… |
-| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:107` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
-| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:293` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
-| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:91` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
-| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:115` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×9 effect.ts×1 scheduler.ts×3 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
-| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:249` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
-| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:123` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
-| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:131` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
-| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:265` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… |
-| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:275` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… |
+| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:313` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… |
+| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:321` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. |
+| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:329` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). |
+| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:337` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. |
+| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:345` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". |
+| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:353` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. |
+| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:159` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… |
+| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… |
+| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:175` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… |
+| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:183` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. |
+| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… |
+| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:191` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… |
+| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … |
+| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:207` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… |
+| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … |
+| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:215` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… |
+| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:69` | async.ts×3 constants.ts×1 lanes.ts×6 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
+| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:79` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
+| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:143` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… |
+| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:402` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… |
+| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:409` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… |
+| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:223` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … |
+| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:233` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… |
+| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:151` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… |
+| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:303` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… |
+| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:109` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
+| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:295` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
+| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:93` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
+| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×10 effect.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
+| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:251` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
+| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:125` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
+| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:133` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
+| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:267` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… |
+| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:277` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… |
## V — fixed violations
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:470` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported |
-| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:480` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was |
-| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:486` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, |
-| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:493` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it |
-| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:505` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, |
+| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:472` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported |
+| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:482` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was |
+| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:488` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, |
+| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:495` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it |
+| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:507` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, |
## B — tier B
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:197` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). |
-| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:205` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). |
-| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:241` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). |
-| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:77` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). |
-| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:213` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). |
+| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). |
+| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:207` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). |
+| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:243` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). |
+| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:79` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). |
+| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:215` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). |
## C — tier C
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:141` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). |
-| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:436` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A |
-| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:446` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** |
-| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:67` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). |
+| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:143` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). |
+| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:438` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A |
+| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:448` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** |
+| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:69` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). |
## INV — invariants
| id | status | defined | cited in src | cited in tests | statement (at definition) |
diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
index e3b714992..60dd7cd7a 100644
--- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
+++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
@@ -55,6 +55,8 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t
- **A first load derived from a hold lands into it** (#3800): a first load born pending in a frame that read a hold lands into that hold if the hold is still live, else as its own commit; the hold commits without it. **Pending** — `fix/create-time-holds` (`91e474506`); [#3800](https://github.com/solidjs/solid/issues/3800) is open.
- **A first pass under a loading boundary that has not shown content is the boundary's, not the tick's** — in a flush as outside one (A29's boundary exemption, #3540). The out-of-flush form is on `next`; the in-flush form (a fresh `Loading` over a held value shows its fallback in a flush) is **pending** — `fix/create-time-holds` (`ca71e5d9d`).
+**A boundary that has not shown content owns its subtree — A29's boundary scope, fresh mounts (2026-10-06, maintainer ruling; amendment — #3540's L2 regression; the bullet above and A15 are unchanged outside such a boundary).** The ruling, in substance, for a boundary that has not shown content: content under it belongs to that boundary, not to any hold it reads, whichever computation does the reading (a creation pass, a re-running mount effect, the boundary's own render). While that content waits for a hold or its own first loads, it is pending: the boundary shows its fallback, and no hold waits for it. Whether the fallback is seen depends on whether the boundary's own mount is held: a boundary mounted as part of a hold (`` with `x` held, or nested in held content) appears at that hold's commit. Outside such a boundary, A15 and the direction rule are unchanged: committed work that reads a hold joins it, never-committed work waits on every hold it read, and if no boundary would catch it, it stays hidden with the hold. The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. **Mechanism:** a first pass (`STATUS_UNINITIALIZED`) that reads a hold asks the boundaries up its chain (`GlobalQueue._fresh`: `catchStatus` with a pending status — the same walk as any pending read, so revealed boundaries it passes record it as a reader until the commit). When a loading boundary that has not shown content catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. `isPending` of the boundary's value is false while its fallback shows, as for a boundary showing its fallback over a first load; `latest()` of content under it is A11's, unchanged (the derivation staged in the hold). The `direction-rule-probe` pins are unchanged. **Known gaps** (pinned `it.fails`, the same on next): a boundary mounted by a pass that itself joined the hold (a memo created in the mount that reads the hold) shows its fallback now rather than appearing at the hold's commit; a boundary mounted by a flight's hold, top-level or nested, shows its fallback for a frame at the commit. **Re-arm (2026-10-06, deferred):** the ruling's `on` part — content under a boundary that an `on` change has re-armed, committed content included, belongs to that boundary — is deferred to a separate change pending its ruling; until then a re-arm behaves as on next, and its shapes are pinned `it.fails` with the ruled display. Open: a boundary a verdict reader mounts (` 0}>` over an action's write) is created by verdict-lane work, and its first passes are routed into that lane (`recompute`'s first-pass lane rule), whose world is the proposal: it shows the held derivation now rather than its fallback (mount-under-hold `verdict` family; the same on pre-L2 and next). The fix belongs to the first-pass lane rule, with #3835's. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush and from mainline: through a memo, directly, bound; nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; catchers at any depth — unrevealed, revealed, plain, and re-armed in flight; boundaries mounted by the hold; no catcher; `isPending` and `latest` on each; a read made pending before the flush joins its hold, and the #1078, #476/#1674 fuzzer findings; the re-arm shapes, pinned `it.fails`); `tests/loading-on-frame-following-3540.test.ts` (3b, 3c: re-arm over an earlier change's flight or staged write, pinned `it.fails` but for the inside-the-action control); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
+
**Not yet one-way (recorded, not ruled).** The frame of a tick is still one transaction (the tick ruling above stands), so two shapes keep the hold waiting on first loads it never needed:
- **Same tick:** mounts created outside a flush in one tick — two mounts over the held value, or a mount and a sync reader — share the tick's frame. The frame joins the hold, and the hold waits on the mounts' first loads.
diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts
index 97ca08c6f..631f60627 100644
--- a/packages/signals/src/boundaries.ts
+++ b/packages/signals/src/boundaries.ts
@@ -203,7 +203,7 @@ export function redraw(b: Boundary): void {
/** GlobalQueue._catch: status from a frame reader, nearest boundary first.
* A loading boundary on the way records a pending reader whether or not it
* catches it (its `on` may collect it later). */
-function catchStatus(node: Computed, flags: number, error: unknown): boolean {
+function catchStatus(node: Computed, flags: number, error?: unknown): boolean {
if (flags === 0) {
// A status cleared. Judged by the node's status now, against each
// boundary's own rule (`unsettled`) — the error path clears pending
@@ -596,25 +596,23 @@ function createBoundary(
}
if (isCollecting(b)) {
// A29's boundary exemption (#3540): a boundary MOUNTED over a held
- // value (its first pass; the tree born held) shows its fallback now
- // and the content at the commit — entering the transaction would
- // make the output itself born held, and nothing would show until
- // the commit. (The seam keeps a held reader until it is committed.)
- // A boundary with a committed value reads a held tree and enters:
- // the outside sees its committed value until the landing, which
- // reveals the content — a fallback staged earlier is replaced ahead
- // of the commit and never shown.
+ // value (its first pass; a first pass under it that read a hold was
+ // collected, `joinPass` — the tree's own, or a render effect's that
+ // binds the content) shows its fallback now and the content at the
+ // commit — entering the transaction would make the output itself
+ // born held, and nothing would show until the commit. The seam
+ // keeps a held reader until it is committed, then re-derives this
+ // pass. A boundary with a committed value reads a held tree and
+ // enters: the outside sees its committed value until the landing,
+ // which reveals the content — a fallback staged earlier is replaced
+ // ahead of the commit and never shown.
const self = getOwner() as Computed;
if (
- tree._config & CONFIG_HELD &&
+ b._readers.size !== 0 &&
self._statusFlags & STATUS_UNINITIALIZED &&
!(self._config & CONFIG_HELD)
- ) {
- // The seam re-derives this pass once; by then the fallback is
- // the committed value, and the next pass enters.
- collecting.add(b);
+ )
return fallback(b);
- }
// Readers under it still unready: the fallback, the tree untouched.
// The seam re-derives this pass when they settle.
if (prune(b, true) !== 0) return fallback(b);
@@ -776,6 +774,8 @@ export function createErrorBoundary(
// Installed at module evaluation — present exactly when something imports a
// boundary. An app without one pays the three null checks and nothing else.
GlobalQueue._catch = catchStatus;
+GlobalQueue._fresh = node =>
+ node._statusFlags & STATUS_UNINITIALIZED && catchStatus(node, STATUS_PENDING);
GlobalQueue._hidden = hidden;
GlobalQueue._boundarySeam = boundarySeam;
GlobalQueue._heldRun = heldRun;
diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts
index 2ee50e388..a14dfcd34 100644
--- a/packages/signals/src/core/core.ts
+++ b/packages/signals/src/core/core.ts
@@ -943,10 +943,18 @@ export function ext(el: { _x: NodeExtension | null }): NodeExtension {
* reader is a render effect. A render effect is the frame, not a derivation
* (rule 3): in that transaction's own flush, or born into it (uninitialized,
* A29), it reads the staged value and holds nothing of its own; otherwise it
- * reads the committed value instead (`frameRead`). */
+ * reads the committed value instead (`frameRead`). A node born into the
+ * future has no committed value: every reader joins it. A first pass is
+ * something not ready under a loading boundary that has not shown content:
+ * the boundary collects it and shows its fallback, and the pass is the
+ * boundary's, not the tick's (A29's boundary exemption, #3540). */
function joinPass(c: Computed, el: Signal | Computed): void {
c._flags |= REACTIVE_JOINED;
- if ((c as any)._type !== EFFECT_RENDER) joinPassTx(txOf(el));
+ if (
+ (el as Computed)._statusFlags & STATUS_UNINITIALIZED ||
+ (c as any)._type !== EFFECT_RENDER
+ )
+ joinPassTx(txOf(el), GlobalQueue._fresh?.(c));
}
/** A15's stale reader (shared-hole and reveal corollaries): a render effect
@@ -1653,11 +1661,9 @@ export function read(el: Signal | Computed): T {
!(el._config & CONFIG_OVERRIDE) &&
!(c._config & CONFIG_CHILDREN_FORBIDDEN)
) {
- if (owner._statusFlags & STATUS_UNINITIALIZED) {
- (c as Computed)._flags |= REACTIVE_JOINED;
- joinPassTx(txOf(el));
- } else if (frameRead(c as Computed, el)) committed = true;
- else joinPass(c as Computed, el);
+ if (owner._statusFlags & STATUS_UNINITIALIZED || !frameRead(c as Computed, el))
+ joinPass(c as Computed, el);
+ else committed = true;
}
}
// Lanes: a lane's node (after the pull — the node is current). NOT_PENDING
diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts
index f47e334de..b43c474e2 100644
--- a/packages/signals/src/core/scheduler.ts
+++ b/packages/signals/src/core/scheduler.ts
@@ -173,11 +173,15 @@ export let flushTransaction: Transaction | null = null;
* into it, and nothing else in the tick is — a write made after the mount
* is a mainline write, a render effect mounted beside it a stale reader.
* Inside a flush, or in a tick that already has its transaction (an
- * action's body), the frame joins instead (`flushTransaction`). Cleared by
- * the flush the join schedules. */
+ * action's body), the frame joins instead (`flushTransaction`) — except a
+ * first pass a loading boundary that has not shown content catches (`own`,
+ * A29's boundary exemption, #3540): it is the boundary's, not the tick's,
+ * and a flush that has joined nothing keeps it pass-scoped as outside one.
+ * Cleared by the flush the join schedules, and at the end of a flush that
+ * set it. */
export let passTx: Transaction | null = null;
-export function joinPassTx(t: Transaction): void {
- if (globalQueue._running || flushTransaction !== null) return joinFuture(t);
+export function joinPassTx(t: Transaction, own?: unknown): void {
+ if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
if (passTx === null) passTx = resolveTx(t);
else merge(resolveTx(t), passTx);
schedule();
@@ -658,13 +662,15 @@ export class GlobalQueue implements IQueue {
// Boundaries (boundaries.ts): the display consumers between an observer
// and the root. `_catch` — status from a frame reader, nearest boundary
// first (true: caught, the root never hears of it; a clear — flags 0 —
- // settles the reader there); `_hidden` — a frame reader behind a fallback
- // is not on screen and holds nothing; `_boundarySeam` — the seam's sweep
- // (readers gone or settled without a pass reveal; an `on` re-arm
- // resolves).
+ // settles the reader there); `_fresh` — a pass that read a hold (true: a
+ // first pass a loading boundary that has not shown content caught,
+ // `joinPass`); `_hidden` — a frame reader behind a fallback is not on
+ // screen and holds nothing; `_boundarySeam` — the seam's sweep (readers
+ // gone or settled without a pass reveal; an `on` re-arm resolves).
declare static _catch:
| ((node: Computed, flags: number, error: unknown) => boolean)
| undefined;
+ declare static _fresh: ((node: Computed) => unknown) | undefined;
declare static _hidden: ((r: Computed) => boolean) | undefined;
declare static _boundarySeam: (() => void) | undefined;
// `_heldRun` — a queued run under a fallback-showing boundary waits for
@@ -710,6 +716,7 @@ export class GlobalQueue implements IQueue {
if (__DEV__) DEV.hooks.onUpdate?.();
} finally {
this._running = false;
+ passTx = null;
}
}
/** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
diff --git a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
new file mode 100644
index 000000000..4a27cbad2
--- /dev/null
+++ b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
@@ -0,0 +1,1349 @@
+/**
+ * #3540 under L2: a fresh Loading mounted under a hold shows its fallback
+ * (A29's boundary exemption; the fresh-mount part of the 2026-10-06
+ * boundary-scope ruling).
+ *
+ * A first pass that reads a hold asks the boundaries up its chain
+ * (`GlobalQueue._fresh`, the real `catchStatus` walk); when a loading
+ * boundary that has not shown content catches it, the pass is the
+ * boundary's, not the tick's: the boundary shows its fallback, the mount
+ * publishes, and the content appears at the hold's commit. Outside such a
+ * boundary, A15 and the direction rule are unchanged.
+ *
+ * Re-arm (`on`) under the boundary scope is deferred to a separate change,
+ * pending its ruling: those shapes keep next's behavior and are pinned
+ * `it.fails` with the ruled display.
+ *
+ * Each case is traced on the pre-L2 core (41fdf9696) too; the notes say
+ * where it differs and the ruling that explains it. Sources: `held` — the
+ * content reads `x`, written in an action that is still running; `flight`
+ * — the content reads an async memo over `x`, refetching, which a revealed
+ * Loading elsewhere holds. Logs within a step are sorted; `o`/`i`/`f`: the
+ * readers recorded on the outer, inner and fresh boundaries; `pending o`:
+ * `isPending` of that boundary's value.
+ */
+import { describe, expect, it } from "vitest";
+import {
+ action,
+ createLoadingBoundary,
+ createMemo,
+ createRenderEffect,
+ createRoot,
+ createSignal,
+ flush,
+ getOwner,
+ isPending,
+ latest,
+ untrack
+} from "../src/index.js";
+
+const tick = async () => {
+ for (let i = 0; i < 8; i++) await Promise.resolve();
+ flush();
+};
+
+/** `{fn()}`, created untracked
+ * as createComponent does. */
+const Loading = (fn: () => T, fallback: string, on?: () => unknown) =>
+ untrack(() => createLoadingBoundary(fn, () => fallback, on ? { on } : undefined));
+
+/** The readers recorded on the boundary `owner` is under (internal). */
+function readersOf(owner: object | undefined): number | undefined {
+ if (!owner) return undefined;
+ const context = (owner as { _context: Record })._context;
+ for (const key of Object.getOwnPropertySymbols(context)) {
+ const b = context[key] as { _readers?: Set } | null;
+ if (b?._readers) return b._readers.size;
+ }
+ throw new Error("no boundary");
+}
+
+type Source = "held" | "flight";
+type Content = "memo" | "direct" | "bound";
+
+/** `x`, and what the content reads (see the header). */
+function world(source: Source, log: string[]) {
+ const [x, setX] = createSignal(0);
+ const pending: (() => void)[] = [];
+ let data: (() => number) | undefined;
+ if (source === "flight") {
+ data = createMemo(async () => {
+ const v = x();
+ await new Promise(r => pending.push(r));
+ return v;
+ });
+ const holder = Loading(() => `holder ${data!()}`, "holder fallback");
+ createRenderEffect(holder, v => {
+ log.push(v);
+ });
+ } else
+ createRenderEffect(x, v => {
+ log.push(`x ${v}`);
+ });
+ const settle = async () => {
+ while (pending.length) pending.shift()!();
+ await tick();
+ };
+ /** Writes `x` and holds it; resolves to the release. */
+ const begin = () => {
+ if (source === "flight") {
+ setX(1);
+ flush();
+ return settle;
+ }
+ let release!: () => void;
+ action(function* () {
+ setX(1);
+ yield new Promise(r => (release = r));
+ })();
+ flush();
+ return async () => {
+ release();
+ await tick();
+ };
+ };
+ return { read: () => (data ? data() : x()), settle, begin };
+}
+
+/** The content over `read`: through a memo, directly, or bound by a render
+ * effect (`
{m()}
` — the tree itself reads nothing). */
+function content(kind: Content, read: () => number, log: string[], tag = "content") {
+ return () => {
+ if (kind === "direct") return `${tag} ${read()}`;
+ const m = createMemo(() => `${tag} ${read()}`);
+ if (kind === "memo") return m();
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return "p";
+ };
+}
+
+type State = { log: string[]; readers: [string, number | undefined][]; pending: string[] };
+const format = (s: State) =>
+ [
+ [...s.log].sort().join(" · "),
+ ...(s.readers.length ? [s.readers.map(([k, n]) => k + n).join(" ")] : []),
+ ...(s.pending.length ? [`pending ${s.pending.join("")}`] : [])
+ ].join(" | ");
+
+/** Steps: the mounting flush, the hold's release, the source settling (and,
+ * for `gate`, the gate resolving). */
+async function trace(
+ steps: (() => Promise | void)[],
+ log: string[],
+ readers: () => [string, number | undefined][] = () => [],
+ pending: () => string[] = () => []
+) {
+ const out: string[] = [];
+ let seen = 0;
+ for (const step of steps) {
+ await step();
+ out.push(format({ log: log.slice(seen), readers: readers(), pending: pending() }));
+ seen = log.length;
+ }
+ return out;
+}
+
+describe("#3540: a fresh Loading mounted over a held value", () => {
+ // A held / flight, in a flush (a Show opening) — pre-L2 shows the
+ // committed value (`open true · view content 0`) and updates at the
+ // commit; L2 never serves content the committed value of a held node
+ // (A29, it would tear): the boundary owns it, so it is pending and the
+ // fallback shows now (the #3540 ruling). `next` holds the whole mount
+ // (`open true` waits for the commit): the regression this fixes. From
+ // mainline: as pre-L2 and next — the bound content too (`
{m()}
`:
+ // the binding is pending under the boundary; `next` showed `p` with no
+ // text until the commit).
+ const expected: Record = {
+ "held memo flush": ["open true · view fallback", "view content 1 · x 1", ""],
+ "held memo mainline": ["view fallback", "view content 1 · x 1", ""],
+ "held direct flush": ["open true · view fallback", "view content 1 · x 1", ""],
+ "held direct mainline": ["view fallback", "view content 1 · x 1", ""],
+ "held bound flush": ["open true · view fallback", "text content 1 · view p · x 1", ""],
+ "held bound mainline": ["view fallback", "text content 1 · view p · x 1", ""],
+ "flight memo flush": ["open true · view fallback", "holder 1 · view content 1", ""],
+ "flight memo mainline": ["view fallback", "holder 1 · view content 1", ""],
+ "flight direct flush": ["open true · view fallback", "holder 1 · view content 1", ""],
+ "flight direct mainline": ["view fallback", "holder 1 · view content 1", ""],
+ "flight bound flush": ["open true · view fallback", "holder 1 · text content 1 · view p", ""],
+ "flight bound mainline": ["view fallback", "holder 1 · text content 1 · view p", ""]
+ };
+ for (const source of ["held", "flight"] as Source[])
+ for (const kind of ["memo", "direct", "bound"] as Content[])
+ for (const where of ["flush", "mainline"])
+ it(`${source}, content ${kind}, mounted from ${where === "flush" ? "a flush" : "mainline"}`, async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let w!: ReturnType;
+ const mount = () => {
+ const view = Loading(content(kind, w.read, log), "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ };
+ createRoot(() => {
+ w = world(source, log);
+ if (where === "flush")
+ createRenderEffect(
+ () => (open() ? (mount(), true) : false),
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ await w.settle();
+ const end = w.begin();
+ log.length = 0;
+ const steps = [
+ () => {
+ if (where === "flush") setOpen(true);
+ else createRoot(mount);
+ flush();
+ },
+ end,
+ w.settle
+ ];
+ expect(await trace(steps, log)).toEqual(expected[`${source} ${kind} ${where}`]);
+ });
+
+ for (const source of ["held", "flight"] as Source[])
+ it(`${source}, nested under an outer Loading that has shown content: the inner fallback now, the outer keeps its content`, async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let w!: ReturnType;
+ createRoot(() => {
+ w = world(source, log);
+ const outer = Loading(() => {
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const view = Loading(content("memo", w.read, log), "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ return "outer";
+ }, "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ await w.settle();
+ const end = w.begin();
+ log.length = 0;
+ const steps = [() => (setOpen(true), flush()), end, w.settle];
+ expect(await trace(steps, log)).toEqual([
+ "open true · view fallback",
+ source === "held" ? "view content 1 · x 1" : "holder 1 · view content 1",
+ ""
+ ]);
+ });
+
+ it("a derivation outside the boundary in the same flush still holds the tick (membership is the tick's)", async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ createRoot(() => {
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const label = createMemo(() => `label ${x()}`);
+ createRenderEffect(label, v => {
+ log.push(v);
+ });
+ const view = Loading(content("direct", x, log), "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ let release!: () => void;
+ action(function* () {
+ setX(1);
+ yield new Promise(r => (release = r));
+ })();
+ flush();
+ log.length = 0;
+
+ setOpen(true);
+ flush();
+ expect(log).not.toContain("open true");
+ expect(log.filter(l => l.startsWith("label"))).toEqual([]);
+
+ release();
+ await tick();
+ expect(log).toContain("open true");
+ expect(log).toContain("label 1");
+ expect(log.at(-1)).toBe("view content 1");
+ });
+
+ // The label, created in the mount, reads the hold and joins it, so the
+ // mount is part of the hold: the boundary mounted with it appears at the
+ // commit, its fallback never seen (as a boundary under a held Show).
+ // Not fixed here: the boundary's first pass still runs its fallback now
+ // (`view fallback`), as on next.
+ it.fails(
+ "a boundary mounted by a pass that joined the hold appears at its commit, no fallback",
+ async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ createRoot(() => {
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const label = createMemo(() => `label ${x()}`);
+ createRenderEffect(label, v => {
+ log.push(v);
+ });
+ const view = Loading(() => `content ${x()}`, "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ let release!: () => void;
+ action(function* () {
+ setX(1);
+ yield new Promise(r => (release = r));
+ })();
+ flush();
+ log.length = 0;
+
+ setOpen(true);
+ flush();
+ expect(log).toEqual([]);
+
+ release();
+ await tick();
+ expect([...log].sort()).toEqual(["label 1", "open true", "view content 1"]);
+ }
+ );
+
+ it("the boundary's hold stays its own: a mount over another hold after the flush reveals at that hold's release", async () => {
+ const [x, setX] = createSignal(0);
+ const [y, setY] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ const view = (read: () => number, into: string[]) => {
+ const v = Loading(() => `content ${read()}`, "fallback");
+ createRenderEffect(v, s => {
+ into.push(`view ${s}`);
+ });
+ };
+ createRoot(() => {
+ createRenderEffect(
+ () => (open() ? (view(x, log), true) : false),
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ const hold = (write: () => void) => {
+ let release!: () => void;
+ action(function* () {
+ write();
+ yield new Promise(r => (release = r));
+ })();
+ flush();
+ return () => release();
+ };
+ const releaseX = hold(() => setX(1));
+ const releaseY = hold(() => setY(1));
+ log.length = 0;
+
+ setOpen(true);
+ flush();
+ expect(log).toEqual(["view fallback", "open true"]);
+
+ // Mounted from mainline, outside a flush, over y's hold only.
+ const other: string[] = [];
+ createRoot(() => view(y, other));
+ flush();
+ expect(other).toEqual(["view fallback"]);
+
+ releaseY();
+ await tick();
+ expect(other).toEqual(["view fallback", "view content 1"]);
+ expect(log).toEqual(["view fallback", "open true"]);
+
+ releaseX();
+ await tick();
+ expect(log.at(-1)).toBe("view content 1");
+ });
+});
+
+describe("#3540: the catcher is whoever `catchStatus` would catch at — at any depth, unchanged", () => {
+ /**
+ * A Show mounts a fresh computation in the flush, under `inner` (a
+ * revealed Loading), inside `outer`:
+ * - `unrevealed`: outer has not shown content (a gate sibling is loading);
+ * - `rearmed outer`: outer revealed, its `on` reads the Show's signal;
+ * - `revealed`: outer revealed, plain.
+ * Or under one revealed Loading only: `rearmed` (its `on` reads the
+ * Show's signal) or `plain`. Steps: the mount, the release, the source
+ * settling, the gate.
+ */
+ type Shape = "unrevealed" | "rearmed outer" | "revealed" | "rearmed" | "plain";
+ // The fresh pass's own catch walk (`_fresh`) records it on the revealed
+ // boundaries it passes, as `catchStatus` records any pending read, and
+ // its pending status is caught again by the normal path: one reader more
+ // than next on each boundary passed until the commit, none after it.
+ const expected: Record = {
+ // The ancestor catches, as pre-L2 (which also runs the binding behind
+ // the fallback: `text fresh 0` — L2 holds a run behind a fallback,
+ // 2026-10-02). Held or in flight alike: the content is pending, and the
+ // boundaries on the way record it.
+ "unrevealed held memo": [
+ " | o2 i1",
+ "x 1 | o1 i0",
+ " | o1 i0",
+ "gate gate · open fresh 1 · outer outer"
+ ],
+ "unrevealed held bound": [
+ " | o2 i1",
+ "x 1 | o1 i0",
+ " | o1 i0",
+ "gate gate · open p · outer outer · text fresh 1"
+ ],
+ "unrevealed flight memo": [
+ " | o3 i2",
+ "holder 1 | o1 i0",
+ " | o1 i0",
+ "gate gate · open fresh 1 · outer outer"
+ ],
+ "unrevealed flight bound": [
+ " | o3 i2",
+ "holder 1 | o1 i0",
+ " | o1 i0",
+ "gate gate · open p · outer outer · text fresh 1"
+ ],
+ // A re-armed ancestor: the content is pending and its fallback shows
+ // now — nothing else holds the re-arming frame (#3575). In flight, as
+ // pre-L2. Held (the ruled display, pinned `it.fails`: re-arm under the
+ // boundary scope is deferred): pre-L2 shows the committed value
+ // (`open fresh 0`, a tear under L2's A29); `next` holds the whole mount
+ // for the commit.
+ "rearmed outer held memo": [
+ "outer outer fallback | o1 i1",
+ "open fresh 1 · outer outer · x 1 | o0 i0",
+ " | o0 i0",
+ ""
+ ],
+ "rearmed outer held bound": [
+ "outer outer fallback | o1 i1",
+ "open p · outer outer · text fresh 1 · x 1 | o0 i0",
+ " | o0 i0",
+ ""
+ ],
+ "rearmed outer flight memo": [
+ "outer outer fallback | o2 i2",
+ "holder 1 · open fresh 1 · outer outer | o0 i0",
+ " | o0 i0",
+ ""
+ ],
+ "rearmed outer flight bound": [
+ "outer outer fallback | o2 i2",
+ "holder 1 · open p · outer outer · text fresh 1 | o0 i0",
+ " | o0 i0",
+ ""
+ ],
+ // The nearest boundary re-armed by the mounting flush: as the re-armed
+ // ancestor.
+ "rearmed held memo": [
+ "inner inner fallback | i1",
+ "inner inner · open fresh 1 · x 1 | i0",
+ " | i0",
+ ""
+ ],
+ "rearmed held bound": [
+ "inner inner fallback | i1",
+ "inner inner · open p · text fresh 1 · x 1 | i0",
+ " | i0",
+ ""
+ ],
+ "rearmed flight memo": [
+ "inner inner fallback | i2",
+ "holder 1 · inner inner · open fresh 1 | i0",
+ " | i0",
+ ""
+ ],
+ "rearmed flight bound": [
+ "inner inner fallback | i2",
+ "holder 1 · inner inner · open p · text fresh 1 | i0",
+ " | i0",
+ ""
+ ],
+ // No boundary would catch it: the mount joins the transaction and waits
+ // for its commit, as on next. Pre-L2 shows the committed value now; L2
+ // joins the hold (A29).
+ "plain held memo": [" | i1", "open fresh 1 · x 1 | i0", " | i0", ""],
+ "plain held bound": [" | i1", "open p · text fresh 1 · x 1 | i0", " | i0", ""],
+ "plain flight memo": [" | i2", "holder 1 · open fresh 1 | i0", " | i0", ""],
+ "plain flight bound": [" | i2", "holder 1 · open p · text fresh 1 | i0", " | i0", ""],
+ "revealed held memo": [" | o1 i1", "open fresh 1 · x 1 | o0 i0", " | o0 i0", ""],
+ "revealed held bound": [" | o1 i1", "open p · text fresh 1 · x 1 | o0 i0", " | o0 i0", ""],
+ "revealed flight memo": [" | o2 i2", "holder 1 · open fresh 1 | o0 i0", " | o0 i0", ""],
+ "revealed flight bound": [
+ " | o2 i2",
+ "holder 1 · open p · text fresh 1 | o0 i0",
+ " | o0 i0",
+ ""
+ ]
+ };
+ for (const shape of ["unrevealed", "rearmed outer", "revealed", "rearmed", "plain"] as Shape[])
+ for (const source of ["held", "flight"] as Source[])
+ for (const kind of ["memo", "bound"] as Content[])
+ // Re-arm over a held write: deferred to the re-arm change, pending its ruling.
+ (source === "held" && shape.startsWith("rearmed") ? it.fails : it)(
+ `${shape}, ${source}, content ${kind}`,
+ async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let resolveGate: (() => void) | undefined;
+ let w!: ReturnType;
+ let outerOwner: object | undefined;
+ let innerOwner!: object;
+ let outer: (() => unknown) | undefined;
+ let inner!: () => unknown;
+ const innerFn = () => {
+ innerOwner = getOwner()!;
+ createRenderEffect(
+ () => (open() ? content(kind, w.read, log, "fresh")() : false),
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ return "inner";
+ };
+ const showInner = (on?: () => unknown) => {
+ inner = Loading(innerFn, "inner fallback", on);
+ createRenderEffect(inner, v => {
+ log.push(`inner ${v}`);
+ });
+ };
+ createRoot(() => {
+ w = world(source, log);
+ if (shape === "rearmed" || shape === "plain")
+ return showInner(shape === "rearmed" ? open : undefined);
+ outer = Loading(
+ () => {
+ outerOwner = getOwner()!;
+ showInner();
+ if (shape === "unrevealed") {
+ const gate = createMemo(
+ () => new Promise(r => (resolveGate = () => r("gate")))
+ );
+ createRenderEffect(gate, v => {
+ log.push(`gate ${v}`);
+ });
+ }
+ return "outer";
+ },
+ "outer fallback",
+ shape === "rearmed outer" ? open : undefined
+ );
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ await w.settle();
+ const end = w.begin();
+ log.length = 0;
+ const steps = [
+ () => (setOpen(true), flush()),
+ end,
+ w.settle,
+ async () => {
+ resolveGate?.();
+ await tick();
+ }
+ ];
+ const readers = (): [string, number | undefined][] =>
+ outer
+ ? [
+ ["o", readersOf(outerOwner)],
+ ["i", readersOf(innerOwner)]
+ ]
+ : [["i", readersOf(innerOwner)]];
+ const pending = () => [
+ ...(outer && isPending(() => outer!()) ? ["o"] : []),
+ ...(isPending(() => inner()) ? ["i"] : [])
+ ];
+ const out = await trace(steps, log, readers, pending);
+ // The gate step reports the log only.
+ out[3] = out[3].split(" | ")[0];
+ expect(out).toEqual(expected[`${shape} ${source} ${kind}`]);
+ }
+ );
+
+ /**
+ * A revealed outer Loading; a Show under it mounts, in the flush, a fresh
+ * computation with a slow first load of its own, under an inner Loading
+ * that has not shown content — or directly under the outer one. Steps:
+ * the mount, the release, the slow load, the source settling.
+ */
+ for (const source of ["held", "flight"] as Source[])
+ for (const withInner of [true, false])
+ it(`outer revealed, ${withInner ? "inner unrevealed" : "no inner"}, ${source}: isPending and what the commit waits for`, async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let resolveSlow: (() => void) | undefined;
+ let outerOwner!: object;
+ let freshOwner: object | undefined;
+ let outer!: () => unknown;
+ let w!: ReturnType;
+ createRoot(() => {
+ w = world(source, log);
+ outer = Loading(() => {
+ outerOwner = getOwner()!;
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const body = () => {
+ freshOwner = getOwner()!;
+ const m = createMemo(async () => {
+ const v = w.read();
+ await new Promise(r => (resolveSlow = r));
+ return `fresh ${v}`;
+ });
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return "p";
+ };
+ if (!withInner) return body();
+ const view = Loading(body, "inner fallback");
+ createRenderEffect(view, v => {
+ log.push(`inner ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ return "outer";
+ }, "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ await w.settle();
+ const end = w.begin();
+ log.length = 0;
+ const slow = async () => {
+ resolveSlow?.();
+ await tick();
+ };
+ const steps = [() => (setOpen(true), flush()), end, slow, w.settle];
+ const out = await trace(
+ steps,
+ log,
+ () => [
+ ["o", readersOf(outerOwner)],
+ ["f", readersOf(freshOwner)]
+ ],
+ () => (isPending(() => outer()) ? ["o"] : [])
+ );
+ const x1 = source === "held" ? "x 1" : "holder 1";
+ // Inner unrevealed: it catches; the outer records nothing, is never
+ // pending, and the commit does not wait for the slow load — as
+ // pre-L2 (`next` holds the Show's `open true` for the commit). No
+ // inner: the mount joins the transaction and the commit waits for
+ // the slow load, as on next; pre-L2 commits `x` first (held: L2's
+ // A29 — the mount is the hold's). (Reader counts: see `expected`
+ // above — the fresh pass's catch walk records it once more.)
+ expect(out).toEqual(
+ withInner
+ ? [
+ "inner inner fallback · open true | o0 f2",
+ `${x1} | o0 f2`,
+ "inner p · text fresh 1 | o0 f0",
+ " | o0 f0"
+ ]
+ : [
+ " | o2 f2",
+ " | o2 f2",
+ [x1, "open p", "text fresh 1"].sort().join(" · ") + " | o0 f0",
+ " | o0 f0"
+ ]
+ );
+ });
+});
+
+describe("#3540: the boundary scope — committed content, held mounts, no catcher, verdicts", () => {
+ const verdict = (fn: () => unknown, how: "pending" | "latest") => {
+ try {
+ return String(how === "pending" ? isPending(fn) : latest(fn));
+ } catch (e) {
+ return `throws ${(e as Error).constructor.name}`;
+ }
+ };
+ /** Steps: the change, the hold's commit, the source settling — each the
+ * log since the last (sorted) and `state()`. */
+ async function steps(
+ log: string[],
+ w: ReturnType,
+ change: () => void,
+ state: () => Record
+ ) {
+ const end = w.begin();
+ log.length = 0;
+ const out: string[] = [];
+ let seen = 0;
+ const take = () => {
+ const s = Object.entries(state())
+ .map(([k, v]) => `${k}=${v}`)
+ .join(" ");
+ out.push(`${[...log.slice(seen)].sort().join(" · ")} | ${s}`);
+ seen = log.length;
+ };
+ change();
+ flush();
+ take();
+ await end();
+ take();
+ await w.settle();
+ take();
+ return out;
+ }
+
+ // A fresh boundary mounted over the hold (memo content). While its
+ // fallback shows, its value is not pending (nothing stale is shown).
+ // `latest` of the content: held, the derivation staged in the hold
+ // (`content 1`, A11, as next serves it); in flight, the content has no
+ // value yet and throws, as for any first load. Pre-L2 serves the
+ // committed or staged value (`content 0` in a flush, a tear under A29;
+ // `content 1` from mainline); `next` reports the boundary pending while
+ // it holds the mount. (Readers: the content's catch walk records it once
+ // more than the normal path, until the commit.)
+ const observe = (source: Source, x1: string) => [
+ `view fallback | readers=2 view=false m=false latest=${source === "held" ? "content 1" : "throws NotReadyError"}`,
+ `${[x1, "view content 1"].sort().join(" · ")} | readers=0 view=false m=false latest=content 1`,
+ " | readers=0 view=false m=false latest=content 1"
+ ];
+ for (const source of ["held", "flight"] as Source[])
+ for (const where of ["flush", "mainline"])
+ it(`${source}, a fresh boundary from ${where}: isPending and latest`, async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let w!: ReturnType;
+ let m: (() => string) | undefined;
+ let view: (() => unknown) | undefined;
+ let owner: object | undefined;
+ const mount = () => {
+ view = Loading(() => {
+ owner = getOwner()!;
+ m = createMemo(() => `content ${w.read()}`);
+ return m();
+ }, "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ };
+ createRoot(() => {
+ w = world(source, log);
+ if (where === "flush")
+ createRenderEffect(
+ () => (open() ? (mount(), true) : false),
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ await w.settle();
+ const out = await steps(
+ log,
+ w,
+ () => (where === "flush" ? setOpen(true) : createRoot(mount)),
+ () => ({
+ readers: readersOf(owner),
+ view: verdict(() => view!(), "pending"),
+ m: verdict(() => m!(), "pending"),
+ latest: verdict(() => m!(), "latest")
+ })
+ );
+ const expected = observe(source, source === "held" ? "x 1" : "holder 1");
+ if (where === "flush")
+ expected[0] = expected[0].replace("view fallback", "open true · view fallback");
+ expect(out).toEqual(expected);
+ });
+
+ // Committed content under a revealed boundary, re-run by a plain write
+ // to `key` while the hold is up. With `on: key` the boundary is re-armed
+ // and owns its subtree: the content that reads the hold waits behind the
+ // fallback, and the hold does not wait for it (the ruled display, pinned
+ // `it.fails`: re-arm under the boundary scope is deferred). Pre-L2 and
+ // `next` show no fallback: the content joins the hold (`next` reports the
+ // boundary pending). Without `on`: A15, the content joins the hold — as
+ // pre-L2 and next. (`memo`:
+ // the boundary's own render reads `m`, so its re-run creates a fresh
+ // memo — no value until it loads; `bound`: the binding re-runs over the
+ // committed memo, pending with its committed value.)
+ const committed: Record = {
+ "held rearmed memo": [
+ "view fallback | readers=1 view=false m=false latest=throws NotReadyError",
+ "view c1 1 · x 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "held rearmed bound": [
+ "view fallback | readers=1 view=false m=true latest=c0 0",
+ "text c1 1 · view p · x 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "held plain memo": [
+ // The boundary's render creates a fresh memo; its catch walk records it.
+ " | readers=1 view=true m=false latest=c1 1",
+ "view c1 1 · x 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "held plain bound": [
+ " | readers=0 view=false m=true latest=c1 1",
+ "text c1 1 · x 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "flight rearmed memo": [
+ "view fallback | readers=1 view=false m=true latest=c0 0",
+ "holder 1 · view c1 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "flight rearmed bound": [
+ "view fallback | readers=1 view=false m=true latest=c0 0",
+ "holder 1 · text c1 1 · view p | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "flight plain memo": [
+ " | readers=1 view=true m=true latest=c0 0",
+ "holder 1 · view c1 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ],
+ "flight plain bound": [
+ " | readers=1 view=false m=true latest=c0 0",
+ "holder 1 · text c1 1 | readers=0 view=false m=false latest=c1 1",
+ " | readers=0 view=false m=false latest=c1 1"
+ ]
+ };
+ for (const source of ["held", "flight"] as Source[])
+ for (const withOn of [true, false])
+ for (const kind of ["memo", "bound"] as Content[])
+ // Re-arm over committed content: deferred to the re-arm change, pending its ruling.
+ (withOn ? it.fails : it)(
+ `${source}, committed content ${withOn ? "under a re-armed boundary" : "under a plain boundary"}, ${kind}`,
+ async () => {
+ const log: string[] = [];
+ const [key, setKey] = createSignal(0);
+ let w!: ReturnType;
+ let m!: () => string;
+ let view!: () => unknown;
+ let owner: object | undefined;
+ createRoot(() => {
+ w = world(source, log);
+ view = Loading(
+ () => {
+ owner = getOwner()!;
+ m = createMemo(() => `c${key()} ${w.read()}`);
+ if (kind === "memo") return m();
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return "p";
+ },
+ "fallback",
+ withOn ? key : undefined
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ });
+ flush();
+ await w.settle();
+ const out = await steps(
+ log,
+ w,
+ () => setKey(1),
+ () => ({
+ readers: readersOf(owner),
+ view: verdict(() => view(), "pending"),
+ m: verdict(() => m(), "pending"),
+ latest: verdict(() => m(), "latest")
+ })
+ );
+ expect(out).toEqual(committed[`${source} ${withOn ? "rearmed" : "plain"} ${kind}`]);
+ }
+ );
+
+ // A boundary mounted as part of the hold — ``'s memo
+ // creates it, at top level or in a revealed boundary: it appears at the
+ // hold's commit, its fallback never seen — as pre-L2. (In flight `next`
+ // flashes the fallback at the commit; not fixed here, pinned `it.fails`.)
+ for (const source of ["held", "flight"] as Source[])
+ for (const nested of [false, true])
+ (source === "flight" ? it.fails : it)(
+ `${source}, a boundary mounted by the hold${nested ? ", nested" : ""}: no fallback, it appears at the commit`,
+ async () => {
+ const log: string[] = [];
+ let w!: ReturnType;
+ let view: (() => unknown) | undefined;
+ let shown!: () => unknown;
+ createRoot(() => {
+ w = world(source, log);
+ const body = () => {
+ shown = createMemo(() => {
+ if (w.read() < 1) return null;
+ return (view = Loading(() => `content ${w.read()}`, "fallback"));
+ });
+ createRenderEffect(
+ () => {
+ const v = shown() as (() => unknown) | null;
+ return v ? v() : "none";
+ },
+ v => {
+ log.push(`view ${v}`);
+ }
+ );
+ return "outer";
+ };
+ if (!nested) return body();
+ const outer = Loading(body, "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ await w.settle();
+ const out = await steps(
+ log,
+ w,
+ () => {},
+ () => ({
+ view: view ? verdict(() => view!(), "pending") : "-",
+ shown: verdict(() => shown(), "pending")
+ })
+ );
+ const v0 = source === "held" ? "false" : "-";
+ expect(out).toEqual([
+ ` | view=${v0} shown=true`,
+ `${source === "held" ? "view content 1 · x 1" : "holder 1 · view content 1"} | view=false shown=false`,
+ " | view=false shown=false"
+ ]);
+ }
+ );
+
+ // No boundary would catch it: a fresh mount reading the hold joins it
+ // and waits for its commit (A15), as on next. Pre-L2 shows the committed
+ // value in a flush (`text fresh 0`).
+ for (const source of ["held", "flight"] as Source[])
+ for (const where of ["flush", "mainline"])
+ it(`${source}, no catcher, from ${where}: the mount joins the hold`, async () => {
+ const log: string[] = [];
+ const [open, setOpen] = createSignal(false);
+ let w!: ReturnType;
+ let m: (() => string) | undefined;
+ const mount = () => {
+ m = createMemo(() => `fresh ${w.read()}`);
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ };
+ createRoot(() => {
+ w = world(source, log);
+ if (where === "flush")
+ createRenderEffect(
+ () => (open() ? (mount(), true) : false),
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ await w.settle();
+ const out = await steps(
+ log,
+ w,
+ () => (where === "flush" ? setOpen(true) : createRoot(mount)),
+ () => ({
+ m: verdict(() => m!(), "pending"),
+ latest: verdict(() => m!(), "latest")
+ })
+ );
+ const first = source === "held" ? "fresh 1" : "throws NotReadyError";
+ const landed = [
+ source === "held" ? "x 1" : "holder 1",
+ ...(where === "flush" ? ["open true"] : []),
+ "text fresh 1"
+ ].sort();
+ expect(out).toEqual([
+ ` | m=false latest=${first}`,
+ `${landed.join(" · ")} | m=false latest=fresh 1`,
+ " | m=false latest=fresh 1"
+ ]);
+ });
+});
+
+describe("#3540: the boundary scope — semantic fuzzer findings (rev 19, seed 91501)", () => {
+ const drain = async () => {
+ for (let i = 0; i < 10; i++) await Promise.resolve();
+ };
+
+ // branch-boundaries #870, reduced. An `on` re-arm leaves the tree held by
+ // an earlier hold, the content it staged there stale: the output's read
+ // of it is a read of content the boundary owns, so the tree leaves the
+ // transaction and re-derives (pending under the boundary) rather than
+ // being recorded as a settled reader — which redrew the boundary forever.
+ // Re-arm under the boundary scope is deferred, pending its ruling: next's
+ // display here is the held content, no fallback.
+ it.fails("a held tree under a re-armed boundary re-derives — no redraw loop", async () => {
+ const [s1, setS1] = createSignal(0);
+ const [s2, setS2] = createSignal(0);
+ const [visible, setShow] = createSignal(true);
+ const flights: (() => void)[] = [];
+ const log: unknown[] = [];
+ createRoot(() => {
+ const node = createMemo(async () => {
+ const v = s2();
+ await new Promise(r => flights.push(r));
+ return v;
+ });
+ const view = Loading(() => (visible() ? `${s1()} ${s2()}` : "hidden"), "loading", s1);
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ createRenderEffect(node, v => {
+ log.push(`node ${v}`);
+ });
+ });
+ flush();
+ while (flights.length) flights.shift()!();
+ await drain();
+ flush();
+ log.length = 0;
+ setS1(1);
+ flush();
+ setS2(1);
+ setShow(false);
+ flush();
+ setS1(0);
+ flush();
+ expect(log).toEqual(["view 1 0", "view loading"]);
+ setS2(0);
+ flush();
+ await drain();
+ expect(log).toEqual(["view 1 0", "view loading"]);
+ });
+
+ // boundaries #1078, reduced. An `on` re-arm over content staged in a
+ // hold, then a hide, in a flush that an async memo of that hold (read
+ // outside) joins to it: the content staged before the re-arm must not be
+ // published at the hold's landing, after the hide.
+ it("hidden after a re-arm over a held mount: the boundary does not publish stale content", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `content ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setShow(false));
+ await w.settle();
+ expect(w.log).toEqual(["0:hidden"]);
+ });
+});
+
+/** The #1078 world: `n1`, async over `s1`, is read by a render effect
+ * outside the boundaries while `visible`. Mounted with `s1` = 1 committed
+ * and `setShow(true)` held by `n1`'s flight: the boundaries' content
+ * (`content 1`) is staged in that hold, the screen still `hidden`. `s1` is
+ * every boundary's `on`; writing it re-arms them, and re-asks `n1` — the
+ * flush joins the hold after the boundaries' content has read it. `log`:
+ * what each boundary shows, from the hold on. */
+function seamWorld(boundaries: (s1: () => number, visible: () => boolean) => (() => unknown)[]) {
+ const drain = async () => {
+ for (let i = 0; i < 10; i++) await Promise.resolve();
+ };
+ const [s1, setS1] = createSignal(0);
+ const [visible, setShow] = createSignal(false);
+ const flights: (() => void)[] = [];
+ const log: string[] = [];
+ createRoot(() => {
+ const n0 = createMemo(() => s1());
+ const n1 = createMemo(async () => {
+ const v = n0();
+ await new Promise(r => flights.push(r));
+ return v;
+ });
+ boundaries(s1, visible).forEach((view, i) =>
+ createRenderEffect(view, v => {
+ log.push(`${i}:${v}`);
+ })
+ );
+ createRenderEffect(
+ () => (visible() ? n1() : "hidden"),
+ () => {}
+ );
+ });
+ const turn = async (fn: () => void) => {
+ fn();
+ flush();
+ await drain();
+ flush();
+ };
+ const settle = async () => {
+ for (let i = 0; i < 4; i++) {
+ while (flights.length) flights.shift()!();
+ await turn(() => {});
+ }
+ };
+ const mount = async () => {
+ await turn(() => {});
+ await settle();
+ await turn(() => setS1(1));
+ await turn(() => setShow(true));
+ log.length = 0;
+ };
+ return { setS1, setShow, log, turn, settle, mount };
+}
+
+// A re-arm whose content reads a hold, in a flush that then joins that
+// hold: the re-armed content enters the hold and appears at its commit, no
+// fallback committed with the hold — as pre-L2 and next.
+describe("#3540: a read made pending before the flush joins its hold", () => {
+ it("one boundary: the content appears at the hold's commit, no fallback", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `content ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ expect(w.log).toEqual([]);
+ await w.settle();
+ expect(w.log).toEqual(["0:content 0"]);
+ });
+
+ it("two boundaries re-armed by the same change: neither shows its fallback", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `a ${s1()}` : "hidden"), "loading", s1),
+ Loading(() => (visible() ? `b ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ expect(w.log).toEqual([]);
+ await w.settle();
+ expect(w.log.sort()).toEqual(["0:a 0", "1:b 0"]);
+ });
+
+ it("two boundaries, then a hide: both land hidden", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `a ${s1()}` : "hidden"), "loading", s1),
+ Loading(() => (visible() ? `b ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setShow(false));
+ await w.settle();
+ expect(w.log.sort()).toEqual(["0:hidden", "1:hidden"]);
+ });
+
+ it("nested: neither the inner nor the outer fallback shows", async () => {
+ const w = seamWorld((s1, visible) => {
+ const outer = Loading(
+ () => {
+ const inner = Loading(() => (visible() ? `in ${s1()}` : "hidden"), "inner", s1);
+ return () => `out[${inner()}]`;
+ },
+ "outer",
+ s1
+ );
+ return [outer];
+ });
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ expect(w.log).toEqual([]);
+ await w.settle();
+ expect(w.log).toEqual(["0:out[in 0]"]);
+ });
+
+ it("nested, then a hide: lands hidden", async () => {
+ const w = seamWorld((s1, visible) => {
+ const outer = Loading(
+ () => {
+ const inner = Loading(() => (visible() ? `in ${s1()}` : "hidden"), "inner", s1);
+ return () => `out[${inner()}]`;
+ },
+ "outer",
+ s1
+ );
+ return [outer];
+ });
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setShow(false));
+ await w.settle();
+ expect(w.log).toEqual(["0:out[hidden]"]);
+ });
+
+ it("re-armed twice under the hold: the last content appears at the commit", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `content ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setS1(2));
+ expect(w.log).toEqual([]);
+ await w.settle();
+ expect(w.log).toEqual(["0:content 2"]);
+ });
+
+ // boundaries #476/#1674 (rev 19, seed 91501), reduced. `show` is held by
+ // `n1`'s flight, which the boundary's content forwards; `s = 0` re-arms
+ // the boundary and starts `n2`, which a reader outside reads. The hold
+ // must not land at once — `show` true beside content derived from `show`
+ // false — with the fallback a frame later.
+ it("a swap parked with another change takes the hold its content read with it", async () => {
+ const drain = async () => {
+ for (let i = 0; i < 10; i++) await Promise.resolve();
+ };
+ const [s, setS] = createSignal(0);
+ const [visible, setShow] = createSignal(false);
+ const manual: (() => void)[] = [];
+ const frame: Record = {};
+ createRoot(() => {
+ const n0 = createMemo(() => s());
+ const n1 = createMemo(() => {
+ const v = n0();
+ return new Promise(r => manual.push(() => r(v)));
+ });
+ const n2 = createMemo(() => Promise.resolve(s()));
+ createRenderEffect(visible, v => {
+ frame.show = v;
+ });
+ const view = Loading(() => (visible() ? `content ${n1()}` : "hidden"), "loading", s);
+ createRenderEffect(view, v => {
+ frame.view = v;
+ });
+ createRenderEffect(
+ () => `outside ${n2()}`,
+ v => {
+ frame.outside = v;
+ }
+ );
+ });
+ const turn = async (fn: () => void) => {
+ fn();
+ flush();
+ const after = { ...frame };
+ await drain();
+ flush();
+ return [after, { ...frame }];
+ };
+ await turn(() => {});
+ manual.shift()!();
+ await turn(() => {});
+ await turn(() => setS(1));
+ await turn(() => setShow(true));
+ expect(frame).toEqual({ show: false, view: "hidden", outside: "outside 1" });
+ expect(await turn(() => setS(0))).toEqual([
+ { show: false, view: "hidden", outside: "outside 1" },
+ { show: true, view: "loading", outside: "outside 0" }
+ ]);
+ while (manual.length) manual.shift()!();
+ await turn(() => {});
+ expect(frame).toEqual({ show: true, view: "content 0", outside: "outside 0" });
+ });
+
+ // boundaries #1674 (rev 19, seed 91501), reduced again. `show` is held
+ // only by the content under the boundary; `s = 0` re-arms it. A reader
+ // outside goes pending on `n2`, which `s = 0` started. Here, as on next,
+ // the re-arm's content joins the hold, so the hold lands with the change
+ // and nothing tears. Under the deferred re-arm scope the re-armed
+ // boundary would release the hold at once, and the reader re-derived at
+ // that landing would keep showing "hidden" beside the landed `show` —
+ // A15's landing tear, the same as boundaries #75's reduction without any
+ // boundary (which fails on next too).
+ it("a re-arm over held content, beside a reader pending on the change: no tear", async () => {
+ const drain = async () => {
+ for (let i = 0; i < 10; i++) await Promise.resolve();
+ };
+ const [s, setS] = createSignal(0);
+ const [visible, setShow] = createSignal(true);
+ const manual: (() => void)[] = [];
+ const frames: Record[] = [];
+ const frame: Record = {};
+ createRoot(() => {
+ const n0 = createMemo(() => s());
+ const n1 = createMemo(() => {
+ const v = n0();
+ return new Promise(r => manual.push(() => r(v)));
+ });
+ const n2 = createMemo(() => Promise.resolve(n0()));
+ createRenderEffect(visible, v => {
+ frame.show = v;
+ });
+ createRenderEffect(
+ () => (visible() ? `outside ${s()} ${n2()}` : "hidden"),
+ v => {
+ frame.outside = v;
+ }
+ );
+ const view = Loading(() => (visible() ? `content ${n1()}` : "hidden"), "loading", s);
+ createRenderEffect(view, v => {
+ frame.view = v;
+ });
+ });
+ const turn = async (fn: () => void) => {
+ fn();
+ flush();
+ frames.push({ ...frame });
+ await drain();
+ flush();
+ frames.push({ ...frame });
+ };
+ await turn(() => {});
+ manual.shift()!();
+ await turn(() => {});
+ await turn(() => {
+ setShow(false);
+ setS(1);
+ });
+ await turn(() => setShow(true));
+ await turn(() => setS(0));
+ while (manual.length) {
+ manual.shift()!();
+ await turn(() => {});
+ }
+ expect(frames.filter(f => f.show === true && f.outside === "hidden")).toEqual([]);
+ });
+
+ it("re-armed twice, then a hide: lands hidden", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `content ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setS1(2));
+ await w.turn(() => w.setShow(false));
+ await w.settle();
+ expect(w.log).toEqual(["0:hidden"]);
+ });
+});
diff --git a/packages/signals/tests/loading-on-frame-following-3540.test.ts b/packages/signals/tests/loading-on-frame-following-3540.test.ts
index 5d9815117..e190332a5 100644
--- a/packages/signals/tests/loading-on-frame-following-3540.test.ts
+++ b/packages/signals/tests/loading-on-frame-following-3540.test.ts
@@ -374,6 +374,154 @@ describe("3. an outside hold on the SAME source: the frame waits, the fallback c
});
});
+// The same source read outside, but its flight comes from an earlier write
+// the `on` change is not part of: nothing holds the change's frame, the
+// re-armed boundary owns its content (A29's boundary scope, 2026-10-06), so
+// the fallback shows now — and nothing is reported. Re-arm under the
+// boundary scope is deferred to a separate change, pending its ruling.
+describe("3b. an outside reader of a flight the `on` change did not start: the fallback shows now, not reported", () => {
+ for (const outside of ["effect", "loading"])
+ test.fails(
+ `outside ${outside === "effect" ? "render effect" : "revealed Loading"}`,
+ async () => {
+ const d = captureWarnings();
+ const [x, setX] = createSignal(0);
+ const [key, setKey] = createSignal(0);
+ const pending: (() => void)[] = [];
+ const log: string[] = [];
+ let dispose!: () => void;
+ createRoot(dispose_ => {
+ dispose = dispose_;
+ const data = createMemo(
+ async () => {
+ const v = x();
+ await new Promise(r => pending.push(r));
+ return v;
+ },
+ { name: "data" }
+ );
+ const read =
+ outside === "effect"
+ ? data
+ : untrack(() =>
+ createLoadingBoundary(
+ () => `holder ${data()}`,
+ () => "holder fallback"
+ )
+ );
+ createRenderEffect(read, v => {
+ log.push(`outside ${v}`);
+ });
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => `c${key()} ${data()}`,
+ () => "fallback",
+ { on: key }
+ )
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ });
+ flush();
+ const settle = async () => {
+ while (pending.length) pending.shift()!();
+ for (let i = 0; i < 8; i++) await microtask();
+ flush();
+ };
+ await settle();
+ log.length = 0;
+ setX(1);
+ flush();
+ expect(log).toEqual([]);
+ setKey(1);
+ flush();
+ expect(log).toEqual(["view fallback"]);
+ await settle();
+ expect(log.slice(1).sort()).toEqual([
+ outside === "effect" ? "outside 1" : "outside holder 1",
+ "view c1 1"
+ ]);
+ expect(d.codes()).toEqual([]);
+ d.stop();
+ dispose();
+ }
+ );
+});
+
+// Content an earlier action holds with no flight (a write it staged): nothing
+// under the boundary is pending, but the re-armed boundary owns its content —
+// it leaves the hold, the fallback shows now, and the content reveals at the
+// action's commit. A re-arm inside the action is the action's frame: no
+// fallback, the content lands with the commit. The re-arm after the action
+// is pinned: re-arm under the boundary scope is deferred, pending its ruling.
+describe("3c. content an earlier action holds by a staged write: the re-arm shows the fallback now", () => {
+ for (const content of ["direct", "memo", "bound"] as const)
+ for (const inside of [false, true])
+ (inside ? test : test.fails)(
+ `${content} content, re-armed ${inside ? "inside" : "after"} the action`,
+ async () => {
+ const d = captureWarnings();
+ const [x, setX] = createSignal(0);
+ const [key, setKey] = createSignal(0);
+ const log: string[] = [];
+ let done!: () => void;
+ let dispose!: () => void;
+ createRoot(dispose_ => {
+ dispose = dispose_;
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => {
+ if (content === "direct") return () => `c ${x()}`;
+ const m = createMemo(() => `c ${x()}`);
+ if (content === "memo") return m;
+ createRenderEffect(m, v => {
+ log.push(`bind ${v}`);
+ });
+ return "
"] : [`view c ${v}`];
+ expect(log).toEqual(shows(0));
+ log.length = 0;
+ action(function* () {
+ setX(1);
+ if (inside) setKey(1);
+ yield new Promise(r => (done = r));
+ })();
+ flush();
+ expect(log).toEqual([]);
+ if (!inside) {
+ setKey(1);
+ flush();
+ expect(log).toEqual(["view fallback"]);
+ }
+ done();
+ for (let i = 0; i < 8; i++) await microtask();
+ flush();
+ expect(log).toEqual(
+ inside
+ ? content === "bound"
+ ? ["bind c 1"]
+ : shows(1)
+ : ["view fallback", ...shows(1)]
+ );
+ expect(d.codes()).toEqual([]);
+ d.stop();
+ dispose();
+ }
+ );
+});
+
describe("4. `on: () => latest(id)`: the display-ahead read shows the fallback now, beside the held frame", () => {
for (const write of ["plain", "action"] as Write[]) {
test(`${write} write, shell lands first: [A] → [A + spinner] → [B + spinner] → [B + comments]`, async () => {
diff --git a/packages/solid/CHEATSHEET.md b/packages/solid/CHEATSHEET.md
index 33e04daa3..bb6ee2567 100644
--- a/packages/solid/CHEATSHEET.md
+++ b/packages/solid/CHEATSHEET.md
@@ -655,7 +655,7 @@ If your training data is 1.x, these are the corrections. **Read this before gene
- **Stores: setters take a draft callback** — mutate the draft in place by default. Returning a new value is shallow (array index-replace, object top-level diff); reach for it for filter/remove. Keyed reconcile is a _projection-fn_ feature, not a setter feature.
- **`undefined` is a real value in `merge`** — it overrides rather than "skip this key".
- **Async lives in computations** — return a Promise/AsyncIterable from `createMemo`/`createStore(fn)`/`createProjection`. Pending reads participate in ``.
-- **`Loading` covers unresolved branches** — once content has rendered, revalidation keeps it visible. Use `isPending(() => x())` for in-flight-change indicators or render guards; it reads `x` and participates in Loading like that read. `` is a dependency list, not a key: its value is never compared; a change to anything it reads makes the boundary stop waiting on its current content and show the fallback if something under it is pending (nothing otherwise). The fallback _follows the frame_: it lands with the change that caused it — immediately when nothing else holds that frame, together with the rest of the new page during a held navigation (never a spinner beside a page the change has not reached). `on={[a(), b()]}` for several. If the same data is also read outside the boundary, the frame waits on it and the fallback can never be seen — DEV warns `LOADING_ON_OUTSIDE_HOLD`; fix the structure (move the outside read under the boundary so one hold owns the data). A frame held past the content's landing by something else (the write's action, other pending data) shows no fallback either — a race, not reported; show the wait with `isPending()`. A display-ahead read in `on` (`on={latest(id)}`) shows the fallback _now_, beside the still-held frame — a capability, not the recommended shape. A fallback that names what is loading should read `latest(id)`, since `id()` is still the committed value while the new one loads.
+- **`Loading` covers unresolved branches** — once content has rendered, revalidation keeps it visible. Use `isPending(() => x())` for in-flight-change indicators or render guards; it reads `x` and participates in Loading like that read. `` is a dependency list, not a key: its value is never compared; a change to anything it reads makes the boundary stop waiting on its current content and show the fallback if something under it is pending (nothing otherwise). The fallback _follows the frame_: it lands with the change that caused it — immediately when nothing else holds that frame, together with the rest of the new page during a held navigation (never a spinner beside a page the change has not reached). `on={[a(), b()]}` for several. If the same data is also read outside the boundary, the frame waits on it and the fallback can never be seen — DEV warns `LOADING_ON_OUTSIDE_HOLD`; fix the structure (move the outside read under the boundary so one hold owns the data). A frame held past the content's landing by something else (the write's action, other pending data) shows no fallback either — a race, not reported; show the wait with `isPending()`. A display-ahead read in `on` (`on={latest(id)}`) shows the fallback _now_, beside the still-held frame — a capability, not the recommended shape. A fallback that names what is loading should read `latest(id)`, since `id()` is still the committed value while the new one loads. (2026-10-06: how a re-armed boundary treats data an earlier change still holds is deferred to a separate change; the above stands until then.)
- **`isPending` ≠ 1.x `.loading`** — it fires while a value _change_ is in flight (an input changed, or `affects()` declared one), not for every fetch. A bare `refresh()`/poll re-asks the same question and is silent. For a reload that should read as pending: `affects(x); refresh(x)`. For a "saving…" affordance: a co-written optimistic flag.
- **No `Suspense.Provider` or single error path** — async errors flow to `` (or effect `error`); no inline `resource.error` branching.
- **`createRoot` is owned by parent by default** — disposed when parent disposes. To detach: `runWithOwner(null, fn)`.
diff --git a/packages/web/test/loading-fallback-in-flush-3540.spec.tsx b/packages/web/test/loading-fallback-in-flush-3540.spec.tsx
new file mode 100644
index 000000000..21e199a71
--- /dev/null
+++ b/packages/web/test/loading-fallback-in-flush-3540.spec.tsx
@@ -0,0 +1,119 @@
+/**
+ * @jsxImportSource @solidjs/web
+ * @vitest-environment jsdom
+ */
+import { describe, expect, test } from "vitest";
+import { Loading, Show, action, createMemo, createSignal, flush } from "solid-js";
+import { render } from "../src/index.js";
+
+const tick = async () => {
+ for (let i = 0; i < 6; i++) await Promise.resolve();
+ flush();
+};
+
+/**
+ * #3540, in a flush: a `` that flips mainline to mount a fresh
+ * `` over a held value shows `open` and the fallback now; the
+ * content reveals with the hold's commit. A derivation outside the boundary
+ * in the same mount still makes the tick join the hold (membership is the
+ * tick's).
+ */
+describe("a fresh Loading mounted by a flush over a held value (#3540)", () => {
+ function setup(outside: boolean) {
+ const container = document.createElement("div");
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ function Content() {
+ const m = createMemo(() => `content ${x()}`);
+ return