import { browser } from "$app/environment"; import { error, type NumericRange } from "@sveltejs/kit"; import { ClientResponseError } from "$lib/api/client"; export const httpStatusFor = (cause: unknown): number => { if (cause instanceof ClientResponseError) { if (cause.status >= 400 && cause.status <= 599) return cause.status; switch (cause.error) { case "RecordNotFound": return 404; case "InvalidRequest": return 400; case "UpstreamFailed": case "UpstreamGone": case "InvalidRecord": return 502; case "Overloaded": return 503; default: return 500; } } return 500; }; export const errorMessage = (cause: unknown, fallbackMessage = "Request failed"): string => cause instanceof ClientResponseError ? (cause.description ?? cause.error) : fallbackMessage; export const toHttpError = (cause: unknown, fallbackMessage = "Request failed"): never => { const status = httpStatusFor(cause) as NumericRange<400, 599>; throw error(status, errorMessage(cause, fallbackMessage)); }; export const parallel = async >>( tasks: T ): Promise<{ [K in keyof T]: Awaited }> => { const keys = Object.keys(tasks) as (keyof T)[]; const values = await Promise.all(keys.map((key) => tasks[key])); const out = {} as { [K in keyof T]: Awaited }; keys.forEach((key, index) => { out[key] = values[index] as Awaited; }); return out; }; // unhandled rejections kill node if nothing on the page awaits the promise export const stream = (promise: Promise): Promise => { promise.catch(() => {}); return promise; }; export const orNull = (promise: Promise): Promise => promise.catch(() => null); // a skeleton that appears only to be replaced a frame later is worse than a slightly longer wait const PATIENCE_MS = 200; // all loads in one navigation share the window let windowEnds: number | null = null; export const endNavigation = (): void => { windowEnds = null; }; const patience = (): number => { const now = performance.now(); if (windowEnds === null || now - windowEnds > PATIENCE_MS) windowEnds = now + PATIENCE_MS; return Math.max(0, windowEnds - now); }; type Held = { value: T } | { promise: Promise }; const within = (promise: Promise, left: number): Promise> => new Promise((resolve) => { const timer = setTimeout(() => resolve({ promise }), left); promise.then( (value) => { clearTimeout(timer); resolve({ value }); }, // the rejection stays on the promise, whoever awaits it in the page owns it () => { clearTimeout(timer); resolve({ promise }); } ); }); export type Settled = { [K in keyof T]: Awaited | T[K]; }; // the navigation waits on these, so the page it is leaving stays until the next one can paint export const settle = async >(data: T): Promise> => { if (!browser) return data as Settled; const left = patience(); const entries = await Promise.all( Object.entries(data).map(async ([key, value]) => { if (!(value instanceof Promise) || left <= 0) return [key, value] as const; const held = await within(value, left); return [key, "value" in held ? held.value : held.promise] as const; }) ); return Object.fromEntries(entries) as Settled; }; // per-request promise de-dupe cache. export interface RequestCache { run(key: string, load: () => Promise): Promise; } export const createRequestCache = (): RequestCache => { const entries = new Map>(); return { run(key: string, load: () => Promise): Promise { const existing = entries.get(key) as Promise | undefined; if (existing) return existing; const pending = load(); entries.set(key, pending); return pending; } }; };