seahaven-ap/src/api/client.ts

88 lines
2.4 KiB
TypeScript
Raw Normal View History

import type { ErrorEnvelope } from "@/api/types";
function pathnameOf(input: RequestInfo | URL): string {
const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
try {
return new URL(raw, "http://local.invalid").pathname;
} catch {
return raw.split("?")[0] ?? raw;
}
}
function skipRefresh(input: RequestInfo | URL): boolean {
const path = pathnameOf(input);
return (
path === "/api/auth/login" ||
path === "/api/auth/callback" ||
path === "/api/auth/refresh" ||
path === "/api/auth/logout"
);
}
let refreshInFlight: Promise<boolean> | null = null;
async function refreshSession(): Promise<boolean> {
if (!refreshInFlight) {
refreshInFlight = fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
headers: { Accept: "application/json" },
})
.then((response) => response.status === 204)
.catch(() => false)
.finally(() => {
refreshInFlight = null;
});
}
return refreshInFlight;
}
export async function apiFetch(
input: RequestInfo | URL,
init: RequestInit = {},
): Promise<Response> {
const headers = new Headers(init.headers);
if (!headers.has("Accept")) headers.set("Accept", "application/json");
headers.delete("Authorization");
const requestInit: RequestInit = { ...init, credentials: "include", headers };
const response = await fetch(input, requestInit);
if ((response.status !== 401 && response.status !== 403) || skipRefresh(input)) {
return response;
}
const refreshed = await refreshSession();
if (refreshed) return fetch(input, requestInit);
return response;
}
export class ApiError extends Error {
readonly status: number;
readonly code?: string;
constructor(message: string, status: number, code?: string) {
super(message);
this.name = "ApiError";
this.status = status;
this.code = code;
}
}
export async function readApiJson<T>(response: Response): Promise<T> {
const text = await response.text();
let body: T & Partial<ErrorEnvelope>;
try {
body = JSON.parse(text) as T & Partial<ErrorEnvelope>;
} catch {
throw response.ok
? new Error("Invalid JSON from API")
: new ApiError(`HTTP ${response.status}`, response.status);
}
if (!response.ok) {
throw new ApiError(
body.error?.message || `HTTP ${response.status}`,
response.status,
body.error?.code,
);
}
return body;
}