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

Đã đóng
#4,417 6 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
48/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
javascript, typescript
Lĩnh vực
frontend, testing

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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.

Ngôn ngữ chính
TypeScript
Star
36.2k
Fork
1.1k
Merge trung bình
10 giờ 10 phút
Pull request đã merge (30 ngày)
505

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của solidjs/solid

Tất cả issue của solidjs/solid

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.