Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159import { browser } from "$app/env";import { error, type RequestEvent } from "@sveltejs/kit";import { ClientResponseError } from "#lib/api/client.ts";
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 => { throw error(httpStatusFor(cause), errorMessage(cause, fallbackMessage));};
export const parallel = async <T extends Record<string, Promise<unknown>>>( tasks: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> => { 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<T[K]> }; keys.forEach((key, index) => { out[key] = values[index] as Awaited<T[keyof T]>; }); return out;};
// unhandled rejections kill node if nothing on the page awaits the promiseexport const stream = <T>(promise: Promise<T>): Promise<T> => { promise.catch(() => {}); return promise;};
export const orNull = <T>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null);
// a skeleton that appears only to be replaced a frame later is worse than a slightly longer waitconst PATIENCE_MS = 200;
// all loads in one navigation share the windowlet 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 DataEvent = Pick<RequestEvent, "isDataRequest" | "request">;
const serverWindows = new WeakMap<Request, number>();
const serverPatience = ({ request }: DataEvent): number => { const now = performance.now(); let ends = serverWindows.get(request); if (ends === undefined || now - ends > PATIENCE_MS) { ends = now + PATIENCE_MS; serverWindows.set(request, ends); } return Math.max(0, ends - now);};
type Held<T> = { value: T } | { promise: Promise<T> };
const within = <T>(promise: Promise<T>, left: number): Promise<Held<T>> => 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<T> = { [K in keyof T]: Awaited<T[K]> | T[K];};
export const hold = async <T extends Record<string, unknown>>( data: T, event?: DataEvent): Promise<Settled<T>> => { const left = event ? serverPatience(event) : 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<T>;};
// the navigation waits on these, so the page it is leaving stays until the next one can paintexport const settle = async <T extends Record<string, unknown>>( data: T, event?: DataEvent): Promise<Settled<T>> => { if (!browser && !event?.isDataRequest) return data as Settled<T>; return hold(data, event);};
export const early = async (event: DataEvent, promise: Promise<unknown>): Promise<void> => { if (!event.isDataRequest) { await promise; return; } await Promise.race([ promise, new Promise<void>((resolve) => setTimeout(resolve, serverPatience(event))) ]);};
// per-request promise de-dupe cache.export interface RequestCache { run<T>(key: string, load: () => Promise<T>): Promise<T>;}
export const createRequestCache = (): RequestCache => { const entries = new Map<string, Promise<unknown>>(); return { run<T>(key: string, load: () => Promise<T>): Promise<T> { const existing = entries.get(key) as Promise<T> | undefined; if (existing) return existing; const pending = load(); entries.set(key, pending); return pending; } };};