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

Chiusa
#4,643 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
15/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
javascript, typescript
Ambito
frontend

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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
Lingua principale
TypeScript
Stelle
36.2k
Fork
1.1k
Merge medio
10h 10m
PR unite (30g)
505

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.