[2.0 / next] Optimistic todo list with an "N left" memo loops forever when a toggle's save lands
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:
- The list loads.
- Toggle 1, toggle 2, delete 2, add a todo.
- The delete's save lands, so its refresh of the list goes out.
- Add another todo.
- The list refetch lands.
- 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
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de solidjs/solid
-
Difficulté 4/5 3-5 jours Accessibilité débutants 55/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 3-5 jours Accessibilité débutants 45/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 3-5 jours Accessibilité débutants 35/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 22/100
Les mainteneurs répondent en général sous 1 jour
-
What gets passed to the `when` prop of a `Show` still gets executed twice on SSRPeut-être pris @brenelz l’a pris il y a 5 jours. Ouverte1.x
Difficulté 3/5 1-2 jours Accessibilité débutants 52/100
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de solidjs/solid
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 66/100
Les mainteneurs répondent en général sous 2 jours
-
reproduced
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
anomalyco/opencode#62052 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
strapi/design-system#2084 ·
Les mainteneurs répondent en général sous 3 jours
-
Report: &TV InternationalOuvertecheck:passed streams:remove
Difficulté 1/5 Moins d'une heure Accessibilité débutants 67/100
Les mainteneurs répondent en général sous 1 jour
-
OAuth documentation for GET /api/v2/users/me/ appears inconsistent with required global read scopeOuverte
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
Les mainteneurs répondent en général sous 1 jour