You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[2.0 / next] A memo's own isPending(heldX) answers false when the memo is pulled inside latest(), true when a plain read wakes it #4468
This is the latestActive half of #4429. #4465 fixes the probing half and deliberately leaves this one open.
Setup:
An action holds a write to x.
Memo m = createMemo(() => \${sa()}:${isPending(x)}`)asksisPending` of that held write in its own pass.
A render effect reads m after sa changes.
What the effect sees depends on how it reads m:
read
result
latest(() => m()) (pulled dirty inside the window)
"1:false"
m(); latest(m) (a plain read wakes it first)
"1:true"
Expected:"1:true" both ways. A memo's value is one shared slot, and its pass runs under its own posture, never its puller's (A31). So the value must not depend on who woke it (D-25).
Cause:verdictValue pulls the dirty memo with the dispatch closed, but leaves latestActive set. The isPending call inside the memo's pass therefore runs as if inside the outer latest(). There heldNotFinal answers "final", which gives false.
Repro (place under packages/signals/tests/):
import{afterEach,beforeEach,expect,test,vi}from"vitest";import{action,createMemo,createRenderEffect,createRoot,createSignal,flush,isPending,latest}from"../src/index.js";beforeEach(()=>vi.useFakeTimers());afterEach(()=>vi.useRealTimers());constsleep=(ms: number)=>newPromise<void>(r=>setTimeout(r,ms));for(constformof["pulled","plainFirst"]asconst)test(`memo's own isPending(held x) is the same whoever wakes it (${form})`,async()=>{const[x,setX]=createSignal(0);const[s,setS]=createSignal(0);const[on,setOn]=createSignal(false);constseen: unknown[]=[];createRoot(()=>{createRenderEffect(x,()=>{});constsa=createMemo(()=>s());constm=createMemo(()=>`${sa()}:${isPending(x)}`);createRenderEffect(()=>(on() ? (form==="pulled" ? latest(()=>m()) : (m(),latest(m))) : "off"),v=>voidseen.push(v));});flush();action(function*(){setX(1);yieldsleep(100);})();flush();setS(1);setOn(true);flush();expect(seen[seen.length-1]).toBe("1:true");awaitvi.advanceTimersByTimeAsync(150);flush();});
A fix is ready locally. It clears latestActive around the pulled pass and restores it afterwards, the same way #4465 handles probing. It costs +13 B minified on the isPending bundles. It opens as a PR once #4465 lands, because both touch the same lines.
Activity
fix(signals): an isPending() in a memo pulled inside latest() answers…
ryansolid commentedon Oct 30, 2026
Fixed on
nextby #4475 (edd3c44); it ships in the next RC.Generated by Claude Code