Repository navigation
L2: mount triggered by latest() shows content beside the committed value #3851
Description
Summary
Inside an action, a <Show when={latest(x) > 0}> opens at once, which is expected, because latest() is display-ahead. But the new <Loading> it mounts shows content 1 while everything else on screen still shows the committed x = 0. Under the lane rule, a verdict lane's mounts stay mainline, so the mounted content belongs to the screen's world, not the proposal's. This is a torn frame.
Repro (packages/signals/tests, vitest)
import { expect, it } from "vitest";
import {
action,
createLoadingBoundary,
createMemo,
createRenderEffect,
createRoot,
createSignal,
flush,
latest,
untrack
} from "../src/index.js";
const settle = async () => {
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 0));
flush();
}
};
it("a mount triggered by latest() shows its content beside the committed value", async () => {
const [x, setX] = createSignal(0);
let screenX: unknown;
let slot: unknown;
createRoot(() => {
// <p>{x()}</p>
createRenderEffect(x, v => {
screenX = v;
});
// <Show when={latest(x) > 0}>
// <Loading fallback="fallback">content {x()}</Loading>
// </Show>
const when = createMemo(() => latest(x) > 0);
const children = createMemo(() =>
when()
? untrack(() =>
createLoadingBoundary(
() => `content ${x()}`,
() => "fallback"
)
)
: "closed"
);
createRenderEffect(
() => {
const c = children();
return typeof c === "function" ? c() : c;
},
v => {
slot = v;
}
);
});
flush();
expect([screenX, slot]).toEqual([0, "closed"]);
let release!: () => void;
const done = action(function* () {
setX(1);
yield new Promise<void>(r => (release = r));
})();
await settle();
// The action holds x = 1; the screen still shows x = 0.
expect(screenX).toBe(0);
// Actual: "content 1" beside the committed 0.
expect(slot).toBe("fallback");
release();
await done;
await settle();
expect([screenX, slot]).toEqual([1, "content 1"]);
});The repro fails at expect(slot).toBe("fallback") with Received: "content 1". The same thing happens when the content is a memo, createMemo(() => \content ${x()}`)`.
Expected vs actual
| Checkpoint | Expected | Actual |
|---|---|---|
| Before the action | x = 0, slot closed |
x = 0, slot closed |
Action holds x = 1 |
x = 0, slot fallback |
x = 0, slot content 1 |
| Action ends | x = 1, slot content 1 |
x = 1, slot content 1 |
Rules involved
- The lane rule: an optimistic lane sees the screen plus its own guesses, and its mounts' children are the lane's. A verdict lane computes the real outcome, and its mounts stay mainline. Here the
Showcondition is verdict-lane work, and its children are seated in that lane. The newLoading's first pass inherits the lane from the pass that created it, so it reads the proposalx = 1. - A29's boundary scope (2026-10-06): a loading boundary that has not shown content owns its subtree. Content that reads a hold waits behind the boundary's fallback, and it appears at the hold's commit.
- No tearing: content on screen agrees with the screen's value of what it reads.
The suspected site is the first-pass lane rule in recompute (core/core.ts). A first pass takes its creator's lane, which is right for an optimistic lane (ruling A). For a verdict lane, it should stay mainline.
Where it reproduces
next(ba67ddbaa)- Optimistically mounted component appears before its dynamic style is applied #3835's branch (
ae59c7682) - fix(signals): a Loading mounted under a hold shows its fallback (L2 regression of #3540) #3824's head (
3ab71b0dc) proto/3540-boundary-scope(60d2f1ded). Its SPEC draft already lists this shape as open ("a boundary a verdict reader mounts … shows the held derivation now rather than its fallback").
Provenance
Found by the semantic fuzzer's mount-under-hold cohort, verdict family (fuzz/semantic-fuzzer-l2, b2bdf125a, rules MH1 and MH5). It probably shares a root with the read-order mount family (fuzzer cases 827, 416 and 936). There, a latest() read routes a freshly mounted child into the verdict lane, its mount control publishes alone, and the slot stays empty. In both cases a pass picks up verdict-lane membership that the lane rule says stays mainline: through a read in the read-order family, and through its creator here.
Activity
chore(size): size exception for first-pass lane seating (#3835, #3851)
fix(signals): seat a first pass in its creator's lane, never a verdic…
ryansolid commentedon Oct 7, 2026
ethan-huo commentedon Oct 20, 2026
The manually assembled signals pin and public compiled JSX shape still differ on current next. Existing conformance case 056 remains red on both published RC15 and source-built current next
2172a37687e92dd91758476423f08cb4a12bcb0cwith the matching native compiler and system Chrome.During the hold, the outer committed binding stays 0 while the newly mounted Loading displays inner content 1; the expected intermediate state is outer 0 plus pending fallback. Both reach 1 after release, so the failure is the intermediate visible tear rather than non-convergence or a final multiple-text-locator assertion. No Router, async source or type bypass is needed.
Current HEAD raw result, fresh RC15 result. This is the already-recorded compiled Show/Loading reproduction of this semantic family; I am following up here rather than opening a duplicate issue. Could the compiled JSX form be retained alongside the primitive regression pin?
ethan-huo commentedon Oct 20, 2026
Follow-up: the missing shape can now be reproduced entirely at the signals level, without a compiler or browser. The existing pin reads
xdirectly in the boundary's content expression; the failing DOM form creates a childcreateRenderEffect(x, ...)there, with the two condition memos used by Show. The new pin observes[outer 0, content node, inner 1]during the hold instead of[outer 0, pending, undefined].A small core-only candidate carries verdict-mount provenance through intermediate content computations while their first passes execute, restores it in the existing finally, and uses it in the born-held staged-read decision. Descendants remain mainline; the repair does not seat them in the verdict lane or change the compiler. On current next, the fresh-boundary hook also needs the propagated provenance so it does not classify this mount as the held pass's own frame.
HEAD source diff + failing signals pin, qualification and RC15-specific diff, raw receipts.
2172a37687e92dd91758476423f08cb4a12bcb0c: the new signals pin and original public compiled JSX case go red to green; compiled case passes in development and production. Full signals suite with the new pin: 324 files, 5388 passed, 3 expected failures, 2 skips, Node / Vitest 4.1.6.This is a local source proposal for review, not a claim of upstream acceptance or complete SSR/hydration coverage. The original conformance test stays unpatched and red. Could this child-render-effect pin and provenance repair be reviewed alongside the original primitive fix?
ethan-huo commentedon Oct 20, 2026
The uncovered DOM/render-effect path is now tracked separately in #4166, with the minimal JSX repro, signals-only pin, candidate source repair and RC15/next validation. Linking it here for continuity with the original fix; follow-up review can use the open issue.
fix(signals): a verdict-mounted Loading holds a nested render effect'…
fix(signals): an unread pending memo does not hold a verdict-mounted …
fix(signals): a boundary mounted by a pass that joined the hold appea…