Skip to content

Commit 03369dd

Browse files
brenelzryansolidcursoragent
authored
fix: track nested deep optimistic store pending state (#2748)
* fix: track nested deep optimistic store pending state * test: cover nested optimistic deep reads Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Ryan Carniato <ryansolid@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f083220 commit 03369dd

3 files changed

Lines changed: 107 additions & 1 deletion

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+
Track pending state for nested deep optimistic store reads.

packages/solid-signals/src/store/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getPropertyDescriptor,
1010
isWrappable,
1111
STORE_OVERRIDE,
12+
STORE_LOOKUP,
1213
STORE_VALUE,
1314
storeLookup,
1415
trackSelf,
@@ -38,7 +39,7 @@ function snapshotImpl<T>(
3839
: target[STORE_VALUE]
3940
);
4041
item = target[STORE_VALUE];
41-
lookup = storeLookup;
42+
lookup = target[STORE_LOOKUP] ?? storeLookup;
4243
} else {
4344
isArray = Array.isArray(item);
4445
map.set(item, item);

packages/solid-signals/tests/store/createOptimisticStore.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,106 @@ describe("createOptimisticStore", () => {
17201720
expect(pendingValues.at(-1)).toBe(false);
17211721
});
17221722

1723+
it("isPending tracks deep nested optimistic store reads before async refresh starts", async () => {
1724+
let state: Refreshable<{ items: { name: string }[] }>;
1725+
let setState: (fn: (s: { items: { name: string }[] }) => void) => void;
1726+
let save!: () => Promise<void>;
1727+
let resolveAction!: () => void;
1728+
const pendingValues: boolean[] = [];
1729+
1730+
createRoot(() => {
1731+
[state, setState] = createOptimisticStore(async () => ({ items: [{ name: "Initial" }] }), {
1732+
items: [{ name: "Initial" }]
1733+
});
1734+
1735+
createRenderEffect(
1736+
() => isPending(() => deep(state)),
1737+
value => {
1738+
pendingValues.push(value);
1739+
}
1740+
);
1741+
1742+
save = action(function* () {
1743+
setState(s => {
1744+
s.items[0].name = "Modified";
1745+
});
1746+
expect(isPending(() => deep(state))).toBe(true);
1747+
yield new Promise<void>(resolve => {
1748+
resolveAction = resolve;
1749+
});
1750+
});
1751+
});
1752+
1753+
flush();
1754+
await Promise.resolve();
1755+
await Promise.resolve();
1756+
flush();
1757+
expect(pendingValues.at(-1)).toBe(false);
1758+
1759+
const actionPromise = save();
1760+
flush();
1761+
1762+
expect(state!.items[0].name).toBe("Modified");
1763+
expect(pendingValues.at(-1)).toBe(true);
1764+
1765+
resolveAction();
1766+
await actionPromise;
1767+
await Promise.resolve();
1768+
flush();
1769+
1770+
expect(pendingValues.at(-1)).toBe(false);
1771+
});
1772+
1773+
it("isPending tracks deep optimistic store reads from a nested proxy", async () => {
1774+
let state: Refreshable<{ items: { name: string }[] }>;
1775+
let setState: (fn: (s: { items: { name: string }[] }) => void) => void;
1776+
let save!: () => Promise<void>;
1777+
let resolveAction!: () => void;
1778+
const pendingValues: boolean[] = [];
1779+
1780+
createRoot(() => {
1781+
[state, setState] = createOptimisticStore(async () => ({ items: [{ name: "Initial" }] }), {
1782+
items: [{ name: "Initial" }]
1783+
});
1784+
1785+
createRenderEffect(
1786+
() => isPending(() => deep(state.items)),
1787+
value => {
1788+
pendingValues.push(value);
1789+
}
1790+
);
1791+
1792+
save = action(function* () {
1793+
setState(s => {
1794+
s.items[0].name = "Modified";
1795+
});
1796+
expect(isPending(() => deep(state.items))).toBe(true);
1797+
yield new Promise<void>(resolve => {
1798+
resolveAction = resolve;
1799+
});
1800+
});
1801+
});
1802+
1803+
flush();
1804+
await Promise.resolve();
1805+
await Promise.resolve();
1806+
flush();
1807+
expect(pendingValues.at(-1)).toBe(false);
1808+
1809+
const actionPromise = save();
1810+
flush();
1811+
1812+
expect(state!.items[0].name).toBe("Modified");
1813+
expect(pendingValues.at(-1)).toBe(true);
1814+
1815+
resolveAction();
1816+
await actionPromise;
1817+
await Promise.resolve();
1818+
flush();
1819+
1820+
expect(pendingValues.at(-1)).toBe(false);
1821+
});
1822+
17231823
it("isPending clears for deep optimistic store reads when fresh projection data lands", async () => {
17241824
let serverCount = 0;
17251825
const fetches: Array<() => void> = [];

0 commit comments

Comments
 (0)