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

Fermée
#4,643 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
5/5
Temps estimé
Plus d'une semaine
Accessibilité débutants
15/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
javascript, typescript
Domaine
frontend

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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
Langage dominant
TypeScript
Étoiles
36.2k
Forks
1.1k
Merge moyen
9 h 56 min
PR mergées (30 j)
487

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de solidjs/solid

Toutes les issues de solidjs/solid

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.