[2.0 / next] Optimistic todo list with an "N left" memo loops forever when a toggle's save lands

Cerrado
#4,643 0 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
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
15/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
javascript, typescript
Área
frontend

Línea de trabajo

The reproduction is a vitest spec in packages/web/test/ that drives createOptimisticStore, createMemo, refresh and flush through a fake server. Start by running npx vitest run in packages/web with the spec, then find where the "Potential Infinite Loop Detected" error is thrown in the scheduler. Done means the count=true case settles without that error while count=false still passes. This touches the reactive core, so expect to read it closely before changing anything.

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

Descripción

Describe the bug

The todo list from the 2.0 cheatsheet, with toggle, remove and add actions that each write a guess, yield the save, then call refresh(todos), plus a remaining memo for an "N left" count. Against a slow server:

  1. The list loads.
  2. Toggle 1, toggle 2, delete 2, add a todo.
  3. The delete's save lands, so its refresh of the list goes out.
  4. Add another todo.
  5. The list refetch lands.
  6. Toggle 1's save lands.

At step 6 the flush never settles. The dev build throws Potential Infinite Loop Detected. Kept alive by scheduled work; last staged node: computed, so a production build would presumably spin and freeze the tab. Without the remaining memo, the same sequence settles normally.

A second shape loops the same way: an inbox with a polled createOptimisticStore list, a memo that filters it by read state, and optimistic deletes. The sequence is filter to "read", poll, delete 1, delete 3, poll, the server declines the delete of 1, a list answer lands, filter back to "all". Then the remaining answers loop, with last staged node: signal. That spec is longer and can be attached if useful.

Found by fuzzing random sequences of these actions against a fake server.

Your Example Website or App

The vitest spec below (in packages/web/test/), on next @ 42350195f.

Steps to Reproduce the Bug or Issue
import { render } from "@solidjs/web";
import { action, createMemo, createOptimisticStore, flush, For, Loading, refresh } from "solid-js";
import { expect, it } from "vitest";

const tick = async () => { for (let i = 0; i < 2; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
type Todo = { id: number; done: boolean };

// The cheatsheet's todo list with an "N left" count, against a slow server.
for (const withCount of [true, false])
it(`toggle, toggle, remove, add, add: the toggle's save lands without a loop (count=${withCount})`, async () => {
  let server: Todo[] = [{ id: 1, done: false }, { id: 2, done: false }];
  const out: { key: string; go: () => void }[] = [];
  const call = (key: string, apply?: () => void) =>
    new Promise<any>(resolve => out.push({ key, go: () => { apply?.(); resolve(key === "list" ? server.map(t => ({ ...t })) : true); } }));
  const land = (key: string) => out.splice(out.findIndex(f => f.key === key), 1)[0].go();
  const el = document.createElement("div");
  let toggle!: (id: number) => Promise<unknown>, remove!: (id: number) => Promise<unknown>, add!: (id: number) => Promise<unknown>;
  const dispose = render(() => {
    const [todos, setTodos] = createOptimisticStore<Todo[]>(() => call("list"), []);
    const remaining = createMemo(() => todos.filter(t => !t.done).length);
    toggle = action(function* (id: number) {
      setTodos(s => { const t = s.find(x => x.id === id); if (t) t.done = !t.done; });
      yield call("toggle:" + id, () => { server = server.map(t => t.id === id ? { ...t, done: !t.done } : t); });
      refresh(todos);
    });
    remove = action(function* (id: number) {
      setTodos(s => s.filter(x => x.id !== id));
      yield call("remove:" + id, () => { server = server.filter(t => t.id !== id); });
      refresh(todos);
    });
    add = action(function* (id: number) {
      setTodos(s => { s.push({ id, done: false }); });
      yield call("add:" + id, () => { server = [...server, { id, done: false }]; });
      refresh(todos);
    });
    return (
      <Loading fallback={<p>loading</p>}>
        <ul><For each={todos}>{t => <li>[{t.id}{t.done ? "x" : "o"}]</li>}</For></ul>
        {withCount ? <p>{remaining()} left</p> : null}
      </Loading>
    );
  }, el);
  const step = async (f: () => void) => { f(); flush(); await tick(); };
  let error: string | null = null;
  try {
    await step(() => land("list"));
    await step(() => { toggle(1); });
    await step(() => { toggle(2); });
    await step(() => { remove(2); });
    await step(() => { add(3); });
    await step(() => land("remove:2"));   // the remove's refresh goes out
    await step(() => { add(4); });
    await step(() => land("list"));
    await step(() => land("toggle:1"));   // Potential Infinite Loop Detected (dev)
  } catch (e) { error = (e as Error).message.slice(0, 60); }
  dispose();
  expect(error).toBeNull();
});

npx vitest run in packages/web. count=true fails with Potential Infinite Loop Detected, and count=false passes.

Expected behavior

Each landing settles in a bounded number of passes, with or without a memo over the list.

Platform
  • OS: macOS
  • Browser: n/a (jsdom)
  • Version: solid-js 2.0.0-rc.20, next @ 42350195f
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.