L2: mount triggered by latest() shows content beside the committed value
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- frontend
Research direction
Start with the provided failing vitest repro under packages/signals/tests, run it to confirm slot reads "content 1" while screenX stays 0. Then read the first-pass lane rule in recompute (packages/signals/src/core/core.ts) alongside the SPEC's lane rule and A29 boundary-scope note to see why a pass created inside a verdict lane inherits that lane instead of staying mainline. Done means the repro passes (slot equals "fallback" during the hold) without breaking the existing lane, loading-boundary and optimistic-lane tests — expect to consult the SPEC draft for proto/3540-boundary-scope before touching semantics.
Written by the indexing model from the issue text.
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)- #3835's branch (
ae59c7682) - #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.
- Dominant language
- TypeScript
- Stars
- 36.1k
- Forks
- 1.1k
- Avg merge
- 11h 42m
- Merged PRs (30d)
- 326
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from solidjs/solid
-
[2.0 next, regressed after rc.13] Async-generator action times out on its authoritative live echoOpen
Difficulty 3/5 1-2 days Newbie friendliness 55/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 Half a day Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 64/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
Maintainers usually reply within 1 day
Similar issues
-
[bug] diagnostics.dumpBody:Buffer 形态请求(透传 lane)跳过 dumps/ 落盘,仅留 raw/-unknown-Possibly taken @ranxianglei claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
ranxianglei/billion-context#2421 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
platformatic/mcp#216 ·
Maintainers usually reply within 1 day
-
pending triage
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
nuxt/test-utils#1842 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
MoonshotAI/kimi-code#4146 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
farbenmeer/tapi#531 ·