Repository navigation
Description
On next 8180332.
Setup:
- A list comes from an async memo
issues. - A
createOptimisticlog of pending assignments is overlaid on that list by a memo. - The result is shown through
createProjection(..., { key: "id" }). - Each action guesses into the log, awaits the server, then calls
refresh(issues).
Steps: action 1 is accepted, and its refresh(issues) is in flight. Action 2 now guesses into the same log.
Result: action 2's guess never shows. With the projection, it is not shown while action 2 is in flight. It is still not shown after action 1's re-ask lands. The row only changes when the server's own answer arrives. Without the projection (the overlay memo rendered directly), the guess is held instead: it shows only once action 1's re-ask lands.
A guess should show at once and hold until its own action is answered.
import { For, Loading, action, createMemo, createOptimistic, createProjection, createSignal, flush, refresh } from "solid-js";
import { render } from "../src/index.js";
const tick = async () => { for (let i = 0; i < 4; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
for (const proj of [false, true]) test(proj ? "projection" : "memo", async () => {
const asks: { key: string; res: (v: any) => void; rej: (e: any) => void }[] = [];
const ask = (key: string) => new Promise<any>((res, rej) => asks.push({ key, res, rej }));
const settle = async (key: string, f: (a: any) => void) => { const i = asks.findIndex(a => a.key === key); f(asks.splice(i, 1)[0]); await tick(); };
const c = document.body.appendChild(document.createElement("div"));
let assign!: (id: number) => any;
const server = [{ id: 3, a: "-" }, { id: 4, a: "dana" }];
render(() => {
const issues = createMemo(() => ask("issues"));
const [truth] = createSignal<any[]>(() => []);
const [assigning, guess] = createOptimistic<any[]>(() => truth());
const overlay = createMemo(() => issues().map((i: any) => { const g = assigning().filter((x: any) => x.id === i.id).pop(); return g ? { ...i, a: g.a } : i; }));
const shown = proj ? (() => { const p = createProjection(() => overlay() as any, [] as any, { key: "id" } as any); return () => p as any; })() : overlay;
assign = action(function* (id: number) {
guess(xs => [...xs, { id, a: "guest" }]);
yield ask(`assign:${id}`);
refresh(issues);
});
return <Loading fallback={<i>loading</i>}><For each={shown()}>{(it: any) => { const v = () => typeof it === "function" ? it() : it; return <i>[#{v().id} {v().a}]</i>; }}</For></Loading>;
}, c);
await tick();
await settle("issues", a => a.res(server.map(x => ({ ...x }))));
assign(3); await tick();
server[0].a = "guest"; await settle("assign:3", a => a.res({})); // action 1's refresh(issues) is now in flight
assign(4); await tick(); // (4) expected [#4 guest]
await settle("issues", a => a.res(server.map(x => ({ ...x })))); // (5) action 1 done; expected [#4 guest]
server[1].a = "guest"; await settle("assign:4", a => a.res({}));
await settle("issues", a => a.res(server.map(x => ({ ...x })))); // (6)
});Frames, logged per case, labelled:
| step | memo | projection |
|---|---|---|
| (4) action 2 out, action 1's re-ask in flight | [#3 guest][#4 dana] |
[#3 guest][#4 dana] |
| (5) re-ask landed, action 2 still out | [#3 guest][#4 guest] |
[#3 guest][#4 dana] |
| (6) all landed | [#3 guest][#4 guest] |
[#3 guest][#4 guest] |
Expected [#4 guest] from (4) on, in both cases.
Which case is a bug:
- Memo: the hold from (4) to (5) may be the Memoized sum disagrees with its async inputs during overlapping updates #3443 rule ("a write whose async work flows into a held memo is held with it"). If it is meant to apply to an optimistic write too, the guess is silent while it waits.
- Projection: a guess that is never shown while its action is in flight looks like a bug.
Where it came from: this shape is the overlay pattern that AI-written ReactiveScript apps use. It showed up as fuzzer holes in two separate apps in our eval round 10: "row assignee shows dana, server guest" when a user assigned two rows in quick succession.
Activity
test(signals): pin #4629's second guess through a keyed projection
chore: mention #4629 in the changeset
test(signals): pin #4629 frame (4) as a D-26 known deviation
fix(store): a projection leaf written by a lane pass is the lane's (#…
test(signals): #4629 frame (4) now passes in both shapes
fix(signals): seat the held re-ask reader at every lane node's change (…
fix(signals): a second guess shows beside the first action's re-ask (#…
ryansolid commentedon Nov 3, 2026
Fixed on
nextby #4633 (efd7f7d) and #4637 (885b99b); it ships in the next RC.Generated by Claude Code