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

Cerrado
#4,417 6 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
48/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
javascript, typescript
Área
frontend, testing

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
TypeScript
Estrellas
36.2k
Forks
1.1k
Merge medio
10 h 10 min
PR fusionados (30 d)
505

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de solidjs/solid

Todos los issues de solidjs/solid

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.