Summary
isPending(() => latest(x)) has semantics pinned in packages/solid-signals/tests/createMemo.test.ts
(tests around lines 2001–2530): while async is in flight and latest() falls back to stale, it reads
true; once the newest value is visible and its lane has no unresolved downstream async, it reads false.
Probing the same expression across node shapes shows two deviations from that contract, plus one
unspecified case. Probe used in all tables:
const probe = (fn) =>
`read=${fn()} latest=${latest(fn)} pRead=${isPending(fn)} pLatest=${isPending(() => latest(fn))}`;
| # |
Shape |
Phase |
read |
latest |
pRead |
pLatest |
| A |
plain signal |
write held by action |
stale |
new |
true |
true |
| B |
sync memo over held signal |
during action |
stale |
stale |
false |
false |
| C |
async memo |
source-change fetch in flight |
stale |
stale |
true |
true |
| D |
async memo |
refresh() fetch in flight |
stale |
stale |
true |
true |
| E |
async memo |
resolved while action still open |
new |
new |
true |
true |
| F |
optimistic signal |
override active |
override |
override |
true |
true |
| G |
optimistic computed |
override active |
override |
override |
true |
true |
| H |
derived-store leaf |
write held by action |
stale |
new |
true |
true |
| I |
derived-store leaf |
firewall refetch in flight |
stale |
stale |
true |
false |
Rows A, C, D, F, G, H match the pinned contract. Rows I, E, and B are the findings below.
1. Store leaf during firewall refetch reports pLatest === false (row I vs C/D) — bug?
Same semantic situation as row C/D (fetch in flight, latest showing stale), opposite answer:
const [store] = createStore(async () => api.getState(), {}); // settled: store.value === "v1"
refresh(store);
// while refetch is in flight:
isPending(() => store.value) // true ✓
isPending(() => latest(() => store.value)) // false ✗ — async memo in the same phase reads true
Suspected cause: the leaf's _latestValueComputed pending check consults the leaf's status flags
(computePendingState, core.ts:1098–1102), but a store leaf's pending status lives on its
_firewall — which the shadow computed never consults (unlike plain reads, core.ts:727).
Expected: true while the firewall refetch is in flight, matching the async-memo contract
pinned by "single async - [isPending(() => latest(x)), latest(x)] pairs update atomically"
(createMemo.test.ts:2203, phase-1 assertion [true, 10] at line 2237).
2. [isPending(x), x()] pair atomicity violated when async resolves inside an open action (row E)
Tests 3 and 5 (createMemo.test.ts:2155, 2251) pin the invariant: if isPending(x) is true, x()
must be the stale value (asserted explicitly at lines 2197–2199 and 2321–2327). This holds for plain
transitions but breaks when the resolve happens while an action is still open:
const [x, setX] = createSignal(1);
const m = createMemo(() => fetchData(x())); // settled: "data-1"
const act = action(function* () {
setX(2); // held write → memo refetches with held value
yield gate; // action stays open past the fetch
});
act();
// ... fetch for x=2 resolves while the action is still open; effect observes:
// read = "data-2", isPending(() => m()) = true ← forbidden pair: pending + NEW value
Expected: either the read stays on "data-1" until the transition commits, or pRead drops to
false — but not true alongside the new value.
3. Sync derivations of held sources are invisible to both latest and isPending (row B) — spec question
const [x, setX] = createSignal(1);
const m = createMemo(() => x() * 10);
// inside an action: setX(2) (held). Then:
latest(x) // 2 ✓
isPending(x) // true ✓
latest(() => m()) // 10 — does not tunnel through the cached memo
isPending(() => m()) // false — silently reports "nothing pending"
The cached memo read never re-executes, so the held source is never reached. Mechanically
understandable, but it contradicts the JSDoc for both APIs (latest: "reads the in-flight value of a
signal/computation"; isPending: true when the read "is showing a stale value while newer async
work is pending" — m's visible value is stale-to-be). If this is a deliberate limitation for cost
reasons, it may deserve a documentation note, since latest(page) working while
latest(() => pageLabel()) silently doesn't is an easy trap.
Minor
The test title at createMemo.test.ts:2001 says isPending(() => latest(x)) is false, but its own
in-transition assertion at line 2036 expects [true, 2] — looks like a stale title.
Environment
solid-js / @solidjs/signals 2.0.0-beta.15
- Repros run as vitest tests against
packages/solid-signals (happy to PR them as failing specs)
Related
Summary
isPending(() => latest(x))has semantics pinned inpackages/solid-signals/tests/createMemo.test.ts(tests around lines 2001–2530): while async is in flight and
latest()falls back to stale, it readstrue; once the newest value is visible and its lane has no unresolved downstream async, it readsfalse.Probing the same expression across node shapes shows two deviations from that contract, plus one
unspecified case. Probe used in all tables:
refresh()fetch in flightRows A, C, D, F, G, H match the pinned contract. Rows I, E, and B are the findings below.
1. Store leaf during firewall refetch reports
pLatest === false(row I vs C/D) — bug?Same semantic situation as row C/D (fetch in flight,
latestshowing stale), opposite answer:Suspected cause: the leaf's
_latestValueComputedpending check consults the leaf's status flags(
computePendingState, core.ts:1098–1102), but a store leaf's pending status lives on its_firewall— which the shadow computed never consults (unlike plain reads, core.ts:727).Expected:
truewhile the firewall refetch is in flight, matching the async-memo contractpinned by "single async - [isPending(() => latest(x)), latest(x)] pairs update atomically"
(createMemo.test.ts:2203, phase-1 assertion
[true, 10]at line 2237).2.
[isPending(x), x()]pair atomicity violated when async resolves inside an open action (row E)Tests 3 and 5 (createMemo.test.ts:2155, 2251) pin the invariant: if
isPending(x)istrue,x()must be the stale value (asserted explicitly at lines 2197–2199 and 2321–2327). This holds for plain
transitions but breaks when the resolve happens while an
actionis still open:Expected: either the read stays on
"data-1"until the transition commits, orpReaddrops tofalse— but nottruealongside the new value.3. Sync derivations of held sources are invisible to both
latestandisPending(row B) — spec questionThe cached memo read never re-executes, so the held source is never reached. Mechanically
understandable, but it contradicts the JSDoc for both APIs (
latest: "reads the in-flight value of asignal/computation";
isPending: true when the read "is showing a stale value while newer asyncwork is pending" —
m's visible value is stale-to-be). If this is a deliberate limitation for costreasons, it may deserve a documentation note, since
latest(page)working whilelatest(() => pageLabel())silently doesn't is an easy trap.Minor
The test title at createMemo.test.ts:2001 says
isPending(() => latest(x)) is false, but its ownin-transition assertion at line 2036 expects
[true, 2]— looks like a stale title.Environment
solid-js/@solidjs/signals2.0.0-beta.15packages/solid-signals(happy to PR them as failing specs)Related
latest()issues — different symptoms: initialundefined, first-change miss)isPending()insideLoading > Errored fallbackcauses infinite loop #2790, 2.0.0-beta.15:isPendingdoes not report pending forrefresh()on acreateOptimistic#2799 (priorisPendingedge cases)