[2.0 / next] A keyed Show on latest() of a staged write mounts its child twice when the flush parks (regressed by #4393)

已关闭
#4,417 6 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
领域
frontend, testing

调研方向

Start with the failing matrix rows in packages/signals/tests/boundary-not-born-held-3540.test.ts and the equivalent cases in packages/web/test/loading-on-keyed-boundary-3540.spec.tsx. Read the linked design notes D-2 and D-22 and the reader-seat rebuild plan in #4395 to understand the intended in-place park re-derive. Done means both cases produce mount counts [1, 2, 2] while preserving the stated latest() behavior.

由索引模型根据 Issue 内容生成。

描述

Summary

A keyed <Show when={latest(count)}> whose when reads a write that is staged in the current flush but not yet held mounts its child twice when that flush parks. The pass runs provisionally (reads latest(count) → new value, the keyed Show mounts the child for key 2), the flush parks on a pending async read, and the pass is re-derived once as a verdict reader (D-2). Because the provisional owner was voided before the re-run, the keyed comparison is against the last committed when, so the same key mounts a second child. Mount side effects run twice for one key change. This regressed with #4393 (which was itself correct for #4365) and is currently pinned at the wrong number.

Reproduction

Signals-level (packages/signals/tests/boundary-not-born-held-3540.test.ts, matrix rows static over latest / callback over latest); web-level equivalent in packages/web/test/loading-on-keyed-boundary-3540.spec.tsx (show-static-latest, show-callback-latest):

const [count, setCount] = createSignal(1);
const data = createMemo(async () => { const v = count(); await delay(1000); return v; });
let mounts = 0;
const L = () => { mounts++; return <Loading fallback="Loading A">{data()}</Loading>; };
<Show keyed when={latest(count)}><L /></Show>
<Loading fallback="Loading B">{data()}</Loading>   // B holds the frame on data (A33)
  1. Render; advance 1000ms; flush → mounts === 1.
  2. setCount(2); flush(); await microtask; flush() — the frame parks on data; A shows Loading A early (correct). mounts is 3; expected 2.
  3. Advance 1000ms; flush → 2|2|2; mounts stays at the mid-flight count.

Observed [1, 3, 3], expected [1, 2, 2]. The <L /> factory (and anything a real child would do on mount — effects, refs, subscriptions) runs twice for a single key change.

Why it is a bug

  • D-2 says a provisional pass that read a staging not yet held is re-derived at the park. It says nothing about discarding the pass's output.
  • latest() serves the staging on both runs (D-22, 2026-10-28 paragraph: "Inside a flush, latest() of a staging not yet held is the frame's read like any other"), so the key is identical across the two runs. Same key → one child → one mount.
  • No rule licenses a mount that depends on whether a flush happened to park. It is a timing-dependent double invocation. A remount is only correct when the verdict run yields a different key — an ordinary keyed change.

History

  • #3540 added a !probing arm to provisionalVerdict ("a provisional pass re-derived at the seam ran everything downstream of it twice (#3540 web)"), which produced [1, 2, 2].
  • #4393 removed that arm to fix #4365 — correctly: latest() must not open a transaction for its verdict — and re-pinned both tests to [1, 3, 3] with a "not a bug" comment. The removal was right; the re-pin was not.

Intended fix

An in-place park re-derive: re-run the pass under the same owner, with keyed children compared against the provisional run rather than the last commit. Scheduled for the reader-seat rebuild S1 (#4395). Explicitly not: restoring the !probing arm, the stagedScreen transaction variant, or letting latest() open a transaction.

Interim

The companion PR flips both pins to it.fails asserting the target [1, 2, 2], so they turn green when fixed instead of guarding the wrong number. List under known bugs in the next RC notes.

主要语言
TypeScript
星标
36.2k
派生
1.1k
平均合并
10 小时 10 分钟
30 天内合并 PR
505

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

solidjs/solid 的其他 Issue

查看 solidjs/solid 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。