Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Each part of a page can say it failed, in its own place: part() (lib/partial.ts, replacing f1687c9a's soft()) reads a part beside what a page is about and keeps a failure (a service that did not answer, or answered with a 5xx) as { failed: true, what } instead of throwing, so the page can tell a part that failed from one with nothing to show and put Unavailable beside it, the way a form puts an error beside its field. A refusal or a 404 is still an answer. orElse() is for a part that can quietly do without. Unavailable has a compact one-line form for a sidebar card or a count's place. Checked: apps/web tsc -b; node --test app/lib/partial.test.ts.

G1t-Session:
ses_01m4zzpc96ect8d41mffgr1a45
syntaqxcommitted Parentf1687c9Browse files
4 files+71−180/4 viewed
+14−3
44
55 import { Card } from "./ui/card";
66
7−/** Says a section could not be loaded, without taking the page down. */
8−export function Unavailable({ what }: { what: string }) {
7+/**
8+ * Says a section could not be loaded, without taking the page down.
9+ * `compact`: one quiet line, for a sidebar's card or a count's place.
10+ */
11+export function Unavailable({ what, compact = false }: { what: string; compact?: boolean }) {
12+ if (compact) {
13+ return (
14+ <p className="flex items-center gap-1.5 text-xs text-muted">
15+ <TriangleAlert size={12} className="shrink-0 text-warn" />
16+ {what} could not be loaded just now.
17+ </p>
18+ );
19+ }
920 return (
1021 <Card asChild tone="plain" className="flex items-center gap-2 border-dashed px-4 py-5 text-sm text-muted">
1122 <p>
2132 * that `what` could not be loaded, and the rest of the page stays. It tries
2233 * again on the next refresh or navigation (whatever reloads the page's
2334 * data). Data a part could not fetch is the loader's to say
24− * (lib/partial.server.ts `soft`); this catches what breaks while drawing.
35+ * (lib/partial.ts `part`); this catches what breaks while drawing.
2536 */
2637 export function Boundary({ what, children, fallback }: { what: string; children: ReactNode; fallback?: ReactNode }) {
2738 const { key } = useLocation();
+0−15
1−/**
2− * A part of a page that fails to load shows as missing, with the page
3− * still up: a loader reads what the page is about as usual (a failure
4− * there is the page's error), and each part beside it through `soft`,
5− * which gives null when it fails. The part then says it could not be
6− * loaded (components/boundary.tsx `Unavailable`) and is back on the next
7− * refresh. A 404 or a refusal is an answer, not a failure: read those as
8− * the service gives them, before `soft` hides them.
9− */
10−export function soft<T>(work: Promise<T>, what: string): Promise<T | null> {
11− return work.catch((error) => {
12− console.warn(`g1t: ${what} could not be loaded:`, error);
13− return null;
14− });
15−}
+22−0
1+import assert from "node:assert/strict";
2+import { test } from "node:test";
3+
4+import { isFailed, orElse, part } from "./partial.ts";
5+
6+test("a part that throws is kept as failed, by what it is; one that answers is its answer", async () => {
7+ const quiet = console.warn;
8+ console.warn = () => {};
9+ try {
10+ const failed = await part(Promise.reject(new Error("work failed with status 503")), "Reviewers");
11+ assert.deepEqual(failed, { failed: true, what: "Reviewers" });
12+ assert.equal(isFailed(failed), true);
13+ assert.equal(orElse(failed, []).length, 0);
14+ } finally {
15+ console.warn = quiet;
16+ }
17+ // A refusal is an answer, not a failure.
18+ const refused = await part(Promise.resolve({ ok: false, code: "not_found" }), "Reviewers");
19+ assert.equal(isFailed(refused), false);
20+ assert.deepEqual(await part(null, "Nothing asked"), null);
21+ assert.equal(isFailed({ failed: true }), false);
22+});
+35−0
1+/**
2+ * Each part of a page loads on its own, and one that fails says so in its
3+ * own place, the way a form says what is wrong beside the field. A loader
4+ * reads what the page is about as usual: when that fails, the page fails
5+ * (routes/page-boundary.tsx). Every part beside it (a sidebar card, a
6+ * panel, a count) is read through `part`, which keeps a failure as
7+ * `Failed` instead of throwing, and the part shows `Unavailable` in its
8+ * place (components/boundary.tsx) while the rest of the page stays up.
9+ *
10+ * Only failures are caught: a service that did not answer, or answered
11+ * with a 5xx. A refusal or a 404 is an answer (`{ ok: false }`), which the
12+ * page reads as it always has.
13+ */
14+
15+/** A part that could not be loaded, by what it is. Plain data, so a loader can send it. */
16+export type Failed = { failed: true; what: string };
17+
18+/** `work`, or `Failed` when it throws; logged, so the failure is still seen. */
19+export async function part<T>(work: Promise<T> | T, what: string): Promise<T | Failed> {
20+ try {
21+ return await work;
22+ } catch (error) {
23+ console.warn(`g1t: ${what} could not be loaded:`, error);
24+ return { failed: true, what };
25+ }
26+}
27+
28+export function isFailed(value: unknown): value is Failed {
29+ return typeof value === "object" && value !== null && (value as Failed).failed === true && typeof (value as Failed).what === "string";
30+}
31+
32+/** The value, or `fallback` when it failed: for a part that can quietly do without. */
33+export function orElse<T, F>(value: T | Failed, fallback: F): T | F {
34+ return isFailed(value) ? fallback : value;
35+}

Comments

Sign in to comment.