Skip to content

Commit 79e246e

Browse files
fix: avoid stale Loading boundary flattening
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e4bc11b commit 79e246e

4 files changed

Lines changed: 135 additions & 3 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 Loading boundary child flattening so settled async memo reads converge with `latest()`.

packages/solid-signals/src/boundaries.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
runWithOwner,
1717
setSignal,
1818
signal,
19-
staleValues,
2019
STATUS_ERROR,
2120
STATUS_PENDING,
2221
untrack,
@@ -59,7 +58,7 @@ function createBoundChildren<T>(
5958
cleanup(() => parentQueue.removeChild(owner._queue!));
6059
return runWithOwner(owner, () => {
6160
const c = computed(fn);
62-
return boundaryComputed(() => staleValues(() => flatten(read(c))), mask);
61+
return boundaryComputed(() => flatten(read(c)), mask);
6362
});
6463
}
6564

packages/solid-signals/tests/createLoadingBoundary.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
flatten,
1010
flush,
1111
isPending,
12+
latest,
1213
NotReadyError,
14+
type SourceAccessor,
1315
refresh,
1416
untrack
1517
} from "../src/index.js";
@@ -674,6 +676,75 @@ describe("createLoadingBoundary", () => {
674676
expect(result).toEqual(["Page ", "b", ": ", "value-b"]);
675677
});
676678

679+
it("settled normal and latest reads converge for async sibling memos", async () => {
680+
vi.useFakeTimers();
681+
try {
682+
let result: any;
683+
let directRead!: () => number;
684+
let latestRead!: () => number;
685+
let boundary!: () => any;
686+
const [$og, setOg] = createSignal(553);
687+
688+
createRoot(() => {
689+
const derived1 = createMemo(async () => {
690+
const o = $og() + 1;
691+
await new Promise(resolve => setTimeout(resolve, 1000));
692+
return o;
693+
});
694+
const derived2 = createMemo(async () => {
695+
const o = derived1() + 1;
696+
await new Promise(resolve => setTimeout(resolve, 1000));
697+
return o;
698+
});
699+
directRead = derived2;
700+
latestRead = () => latest(derived2);
701+
boundary = createLoadingBoundary(
702+
() => [
703+
"a. ",
704+
createMemo(() => derived2(), { sync: true }),
705+
" b. ",
706+
createMemo(() => latest(derived2), { sync: true })
707+
],
708+
() => "Loading..."
709+
);
710+
711+
mountLike(boundary, value => {
712+
result = value;
713+
});
714+
});
715+
716+
flush();
717+
expect(result).toBe("Loading...");
718+
719+
await vi.advanceTimersByTimeAsync(1000);
720+
flush();
721+
expect(result).toBe("Loading...");
722+
723+
await vi.advanceTimersByTimeAsync(1000);
724+
await Promise.resolve();
725+
flush();
726+
expect(directRead()).toBe(555);
727+
expect(latestRead()).toBe(555);
728+
expect(boundary()).toEqual(["a. ", 555, " b. ", 555]);
729+
expect(result).toEqual(["a. ", 555, " b. ", 555]);
730+
731+
setOg(x => x + 1);
732+
flush();
733+
expect(result).toEqual(["a. ", 555, " b. ", 555]);
734+
735+
await vi.advanceTimersByTimeAsync(1000);
736+
flush();
737+
expect(result).toEqual(["a. ", 555, " b. ", 555]);
738+
739+
await vi.advanceTimersByTimeAsync(1000);
740+
await Promise.resolve();
741+
flush();
742+
expect(result).toEqual(["a. ", 556, " b. ", 556]);
743+
} finally {
744+
vi.useRealTimers();
745+
}
746+
});
747+
677748
it("shows inline pending when a keyed subtree revalidates an external async source with isPending", async () => {
678749
let result: any;
679750
const [$page, setPage] = createSignal("a");
@@ -773,7 +844,7 @@ describe("createLoadingBoundary", () => {
773844
it("publishes pending from persistent UI while a revealed branch loads the same source", async () => {
774845
let indicator: unknown;
775846
let branch: unknown;
776-
let source!: () => Promise<string> | string | undefined;
847+
let source!: SourceAccessor<Promise<string> | string | undefined>;
777848
const [$show, setShow] = createSignal(false);
778849
let current = deferred<void>();
779850

packages/solid-web/test/loading.spec.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Switch,
1616
Match,
1717
isPending,
18+
latest,
1819
flush
1920
} from "solid-js";
2021
import { render } from "../src/index.js";
@@ -479,6 +480,62 @@ describe("Testing Loading", () => {
479480
localDispose();
480481
});
481482

483+
test("latest preserves sibling async text in Loading fragments", async () => {
484+
const localDiv = document.createElement("div");
485+
let setOg!: (value: number | ((prev: number) => number)) => number;
486+
let readDirect!: () => number;
487+
488+
const localDispose = render(() => {
489+
const [og, _setOg] = createSignal(553);
490+
setOg = _setOg;
491+
const derived1 = createMemo(async () => {
492+
const o = og() + 1;
493+
await new Promise(res => setTimeout(res, 1000));
494+
return o;
495+
});
496+
const derived2 = createMemo(async () => {
497+
const o = derived1() + 1;
498+
await new Promise(res => setTimeout(res, 1000));
499+
return o;
500+
});
501+
readDirect = derived2;
502+
503+
return (
504+
<Loading fallback="Loading...">
505+
a. {derived2()} b. {latest(derived2)}
506+
</Loading>
507+
);
508+
}, localDiv);
509+
510+
flush();
511+
expect(localDiv.textContent).toBe("Loading...");
512+
513+
await vi.advanceTimersByTimeAsync(1000);
514+
flush();
515+
expect(localDiv.textContent).toBe("Loading...");
516+
517+
await vi.advanceTimersByTimeAsync(1000);
518+
await Promise.resolve();
519+
flush();
520+
expect(readDirect()).toBe(555);
521+
expect(localDiv.textContent).toBe("a. 555 b. 555");
522+
523+
setOg(x => x + 1);
524+
flush();
525+
expect(localDiv.textContent).toBe("a. 555 b. 555");
526+
527+
await vi.advanceTimersByTimeAsync(1000);
528+
flush();
529+
expect(localDiv.textContent).toBe("a. 555 b. 555");
530+
531+
await vi.advanceTimersByTimeAsync(1000);
532+
await Promise.resolve();
533+
flush();
534+
expect(localDiv.textContent).toBe("a. 556 b. 556");
535+
536+
localDispose();
537+
});
538+
482539
test("implicit route transition stays held after lazy component is cached", async () => {
483540
let setRoute!: (value: "home" | "profile") => void;
484541
const localDiv = document.createElement("div");

0 commit comments

Comments
 (0)