Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/projection-staged-error-reader-link-4517.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

A reader of a derived store whose derive carries a staged error (D-41) now links to the derive, as a reader of a pending or errored one does, so it re-runs when the derive's source answers. An `<Errored>` mounted in the change that errored its content over a keyed `createProjection` showed its fallback for good; it now shows the answer.
6 changes: 4 additions & 2 deletions packages/signals/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
CONFIG_MANUAL_WRITE,
CONFIG_OVERRIDE,
CONFIG_PROMOTED,
CONFIG_STAGED_ERROR,
CONFIG_VERDICT,
EFFECT_RENDER,
NOT_PENDING,
Expand Down Expand Up @@ -1844,7 +1845,8 @@ function pullFamily(target: StoreTarget): void {
// reader of a memo would — linked, so it re-runs at the landing (the
// next settled pass pulls without linking and the stale link trims), a
// verdict reader registered, a tracked pass suspended.
if (fw._statusFlags & (STATUS_PENDING | STATUS_ERROR)) {
// A staged error (D-41) throws at the pull, before any leaf would link the reader.
if (fw._statusFlags & (STATUS_PENDING | STATUS_ERROR) || fw._config & CONFIG_STAGED_ERROR) {
// A render effect outside the flight's own flush is the frame, not a
// derivation: it keeps what it shows and learns of the landing from the
// leaves the landing changes (unchanged leaves say nothing — it is not a
Expand All @@ -1858,7 +1860,7 @@ function pullFamily(target: StoreTarget): void {
if (
c !== null &&
c._type === EFFECT_RENDER &&
!(fw._statusFlags & STATUS_ERROR) &&
fw._statusFlags & STATUS_PENDING &&
fw._config & CONFIG_HELD &&
flushTransaction !== txOf(fw) &&
!linkedTo(c, fw)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { expect, it } from "vitest";
import {
createErrorBoundary,
createLoadingBoundary,
createMemo,
createProjection,
createRenderEffect,
createRoot,
createSignal,
flush,
mapArray,
untrack
} from "../src/index.js";

const tick = async () => {
for (let i = 0; i < 4; i++) {
await new Promise(r => setTimeout(r, 0));
flush();
}
};

type Row = { id: number; name: string };

for (const keyed of [true, false])
it(`an answer replaces the error a boundary was born with (projection: ${keyed}) (#4517)`, async () => {
const asks: { key: string; res: (v: Row[]) => void }[] = [];
const ask = (key: string) => new Promise<Row[]>(res => asks.push({ key, res }));
const land = async (key: string, v: Row[]) => {
for (const a of asks.filter(a => a.key === key)) {
asks.splice(asks.indexOf(a), 1);
a.res(v);
}
await tick();
};
const [q, setQ] = createSignal("");
let screen: unknown;
const dispose = createRoot(d => {
const src = createMemo(() => {
const s = q();
if (s === "") return [] as Row[];
if (s.length < 2) throw new Error("too short");
return ask(`search:${s}`);
});
const results: () => Row[] = keyed
? (() => {
const p = createProjection(() => src(), [] as Row[], { key: "id" });
return () => p;
})()
: src;
const show = createMemo(() => q() !== "");
const out = createMemo(() => {
if (!show()) return undefined;
return untrack(() =>
createErrorBoundary(
() =>
createLoadingBoundary(
() => mapArray(results, r => r.name),
() => "loading"
),
e => `E:${(e() as Error).message}`
)
);
});
const resolve = (v: unknown): unknown =>
typeof v === "function" ? resolve(v()) : Array.isArray(v) ? v.join("") : v;
createRenderEffect(
() => resolve(out()),
v => void (screen = v)
);
return d;
});
await tick();
expect(screen).toBe(undefined);
setQ("a");
await tick();
expect(screen).toBe("E:too short");
setQ("ap");
await tick();
expect(screen).toBe("E:too short");
await land("search:ap", [
{ id: 1, name: "apple" },
{ id: 2, name: "apricot" }
]);
expect(screen).toBe("appleapricot");
dispose();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*/
import { expect, test } from "vitest";
import { render } from "../src/index.js";
import {
createMemo,
createProjection,
createSignal,
Errored,
flush,
For,
Loading,
Show
} from "solid-js";

type Row = { id: number; name: string };

const tick = async () => {
for (let i = 0; i < 4; i++) {
await new Promise(r => setTimeout(r, 0));
flush();
}
};

test.each([true, false])(
"an answer replaces the error a boundary was born with (projection: %s)",
async projection => {
const asks: { key: string; res: (v: Row[]) => void }[] = [];
const ask = (key: string) => new Promise<Row[]>(res => asks.push({ key, res }));
const land = async (key: string, v: Row[]) => {
for (const a of asks.filter(a => a.key === key)) {
asks.splice(asks.indexOf(a), 1);
a.res(v);
}
await tick();
};
const container = document.createElement("div");
const [q, setQ] = createSignal("");
const dispose = render(() => {
const src = createMemo(() => {
const s = q();
if (s === "") return [] as Row[];
if (s.length < 2) throw new Error("too short");
return ask(`search:${s}`);
});
const results: () => Row[] = projection
? (() => {
const p = createProjection(() => src(), [] as Row[], { key: "id" });
return () => p;
})()
: src;
return (
<Show when={q() !== ""}>
<Errored fallback={(e: () => any) => <p>{e().message}</p>}>
<Loading fallback={<p>loading</p>}>
<For each={results()}>{r => <i>{r.name}</i>}</For>
</Loading>
</Errored>
</Show>
);
}, container);
await tick();
expect(container.textContent).toBe("");
setQ("a");
await tick();
expect(container.textContent).toBe("too short");
setQ("ap");
await tick();
expect(container.textContent).toBe("too short");
await land("search:ap", [
{ id: 1, name: "apple" },
{ id: 2, name: "apricot" }
]);
expect(container.textContent).toBe("appleapricot");
dispose();
}
);
Loading