Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 18x 21x 11x 11x 11x 11x 2x 2x 2x 2x 18x 40x 21x 21x 1x 20x 12x 9x 3x 40x | import type * as z from 'zod/mini';
import { describeIssues } from '../types/schemas';
/**
* SDD-L07. One SWR fetcher, built from a schema.
*
* Before this the codebase had two conventions for the same job: six near-identical hand-rolled
* `fetchX` functions (each one an `await fetch`, an `if (!res.ok) throw`, and a `return data as X`),
* and a generic `fetcher` in `helpers/index.ts` returning `unknown`, used by `useWeather` and
* `useGithubStars`. The duplicated convention was the majority, and every copy of it ended in a cast
* the compiler believed and the runtime never checked.
*
* The cast is the part worth removing. `as` emits no code: when `/api/analytics` answered
* `{"pageViews":"12345"}` — which it did, because GA4 returns strings — the value travelled on as a
* `number` and surfaced four components later as an unseparated counter. `safeParse` turns that into
* a thrown error at the boundary, naming the field.
*/
/**
* Thrown when a response parses as JSON but does not match the contract.
*
* A distinct type so callers can tell "the route failed" from "the route answered something we do
* not understand". Those deserve different words in front of a reader, and until now every widget
* showed the same undifferentiated error icon for both.
*/
export class ResponseShapeError extends Error {
readonly label: string;
constructor(label: string, detail: string) {
super(`${label}: unexpected response shape — ${detail}`);
this.name = 'ResponseShapeError';
this.label = label;
}
}
/** Thrown when the route itself failed, i.e. answered a non-2xx status. */
export class ResponseStatusError extends Error {
readonly status: number;
constructor(label: string, status: number, statusText: string) {
const reason = statusText ? `${status} ${statusText}` : String(status);
super(`${label}: request failed with ${reason}`);
this.name = 'ResponseStatusError';
this.status = status;
}
}
/**
* @description Build an SWR fetcher that validates the response against `schema`.
* @param schema - The zod schema that *is* the contract for this route; the return type follows it.
* @param label - Route name, carried into the thrown message so a log says which boundary broke.
* @returns A fetcher usable directly as SWR's second argument.
* @example const fetchXRP = createFetcher(xrpSchema, '/api/xrp');
*/
const createFetcher =
<Schema extends z.ZodMiniType>(schema: Schema, label: string) =>
async (url: string): Promise<z.infer<Schema>> => {
const response = await fetch(url);
if (!response.ok) {
throw new ResponseStatusError(label, response.status, response.statusText);
}
const result = schema.safeParse(await response.json());
if (!result.success) {
throw new ResponseShapeError(label, describeIssues(result.error.issues));
}
return result.data;
};
export default createFetcher;
|