Skip to content

Commit aace71e

Browse files
fix(signals): report isPending during refresh()/refetch of a resting optimistic source (closes #2799)
A resting `createOptimistic(asyncFn)` (no active override, `_overrideValue === NOT_PENDING`) reported `isPending` as `false` while a `refresh()`-driven refetch was in flight, diverging from `createMemo` and `createOptimisticStore` which both report `true`. The optimistic-capable identity was wrongly muting pendingness even when no override was active. `computePendingState` now excludes only a resting optimistic node from the upstream "value held in transition" check (that held `_pendingValue` belongs to a reverting optimistic write, not a refetch). A resting optimistic node otherwise behaves like a plain async memo, reporting pending via the downstream async-in-flight check. Muting pending remains the job of an active override. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 26f443f commit aace71e

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/signals": patch
3+
---
4+
5+
Fix `isPending` not reporting pending during a `refresh()`/refetch of an async `createOptimistic` accessor (#2799). A resting optimistic node (no active override) now reports pending exactly like a plain async memo while a refetch is in flight with stale data; muting pending remains the job of an active optimistic override only.

‎packages/solid-signals/src/core/core.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,11 +1118,18 @@ function computePendingState(el: Signal<any> | Computed<any>): boolean {
11181118
}
11191119
return true;
11201120
}
1121-
if (el._overrideValue !== undefined && el._overrideValue === NOT_PENDING && !el._parentSource) {
1122-
return false;
1123-
}
1124-
// Upstream: value held in transition (not during initial load)
1125-
if (el._pendingValue !== NOT_PENDING && !(comp._statusFlags & STATUS_UNINITIALIZED)) return true;
1121+
// Upstream: value held in transition (not during initial load). Excluded for a
1122+
// *resting* optimistic node (`_overrideValue === NOT_PENDING`, no active override):
1123+
// its held `_pendingValue` belongs to a reverting optimistic write, not a refetch, so
1124+
// it must not read as pending. Muting pending is the job of an active override; a
1125+
// resting optimistic node otherwise behaves like a plain async memo and reports
1126+
// pending only via the downstream async-in-flight check below (#2799).
1127+
if (
1128+
el._overrideValue !== NOT_PENDING &&
1129+
el._pendingValue !== NOT_PENDING &&
1130+
!(comp._statusFlags & STATUS_UNINITIALIZED)
1131+
)
1132+
return true;
11261133
// Downstream: async in flight with previous value (not initial load)
11271134
// STATUS_UNINITIALIZED is cleared on first successful completion
11281135
return !!(comp._statusFlags & STATUS_PENDING && !(comp._statusFlags & STATUS_UNINITIALIZED));

‎packages/solid-signals/tests/createOptimistic.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,40 @@ describe("createOptimistic", () => {
905905
expect(isPending($data!)).toBe(false);
906906
});
907907

908+
it("isPending reports pending while refresh() refetches an async optimistic accessor (#2799)", async () => {
909+
let resolveFetch: ((v: number[]) => void) | null = null;
910+
const makeFetch = () => new Promise<number[]>(r => (resolveFetch = r));
911+
912+
let $data!: SourceAccessor<number[]>;
913+
createRoot(() => {
914+
const [data] = createOptimistic<number[]>(() => makeFetch());
915+
$data = data;
916+
// Keep the node alive / recomputing on refresh.
917+
createRenderEffect(data, () => {});
918+
});
919+
920+
// Initial load — isPending stays false for the first load (no stale data).
921+
flush();
922+
resolveFetch!([1, 2, 3]);
923+
await Promise.resolve();
924+
flush();
925+
expect($data()).toEqual([1, 2, 3]);
926+
expect(isPending(() => $data())).toBe(false);
927+
928+
// refresh() forces a refetch; the resolved value is still visible while the
929+
// new fetch is in flight, so isPending must report true (same as an async memo).
930+
refresh($data);
931+
flush();
932+
expect(isPending(() => $data())).toBe(true);
933+
934+
// Settling the refetch clears pending.
935+
resolveFetch!([4, 5, 6]);
936+
await Promise.resolve();
937+
flush();
938+
expect($data()).toEqual([4, 5, 6]);
939+
expect(isPending(() => $data())).toBe(false);
940+
});
941+
908942
it("plain optimistic stays true through refresh-of-unrelated-async (issue #2685)", async () => {
909943
// github.com/solidjs/solid/issues/2685 — the optimistic signal itself
910944
// is plain (no async source); the async work comes from refresh()ing

0 commit comments

Comments
 (0)