| 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 | + | } |