[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 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
javascript, typescript
領域
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
平均マージ
11時間 5分
マージ済み PR(30日)
475

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

solidjs/solid のほかの issue

solidjs/solid の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。