import { sessionStore, type SessionStore } from './session.ts' import type { ErrorResponse, TokenPair } from './types.ts' const defaultApiBase = '/api/v1' type ResponseType = 'blob' | 'json' | 'void' export interface ApiRequestOptions { auth?: boolean body?: BodyInit | null headers?: HeadersInit json?: unknown method?: string responseType?: ResponseType signal?: AbortSignal } interface ApiClientOptions { apiBase?: string fetchImplementation?: typeof fetch store?: SessionStore } export class ApiError extends Error { readonly status: number readonly logId?: string constructor(status: number, message: string, logId?: string) { super(message) this.name = 'ApiError' this.status = status this.logId = logId } } async function responseError(response: Response): Promise { let body: ErrorResponse | undefined try { body = (await response.json()) as ErrorResponse } catch { // Non-JSON failures use a stable fallback message. } const message = body?.error?.message ?? `Request failed with status ${response.status}` return new ApiError(response.status, message, body?.error?.log_id) } async function readResponse( response: Response, responseType: ResponseType, ): Promise { if (!response.ok) { throw await responseError(response) } if (responseType === 'blob') { return (await response.blob()) as T } if (responseType === 'void' || response.status === 204) { return undefined as T } return (await response.json()) as T } export function createApiClient({ apiBase = defaultApiBase, fetchImplementation = fetch, store = sessionStore, }: ApiClientOptions = {}) { let refreshPromise: Promise | null = null const refreshSession = (): Promise => { if (refreshPromise) { return refreshPromise } const refreshToken = store.get()?.refresh_token if (!refreshToken) { store.clear() return Promise.reject(new ApiError(401, 'Your session has expired')) } refreshPromise = fetchImplementation(`${apiBase}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), }) .then((response) => readResponse(response, 'json')) .then((session) => { store.set(session) return session }) .catch((error: unknown) => { store.clear() throw error }) .finally(() => { refreshPromise = null }) return refreshPromise } const request = async ( path: string, options: ApiRequestOptions = {}, ): Promise => { const { auth = true, body, headers: providedHeaders, json, method = 'GET', responseType = 'json', signal, } = options const run = (accessToken?: string) => { const headers = new Headers(providedHeaders) let requestBody = body if (json !== undefined) { headers.set('Content-Type', 'application/json') requestBody = JSON.stringify(json) } if (accessToken) { headers.set('Authorization', `Bearer ${accessToken}`) } return fetchImplementation(`${apiBase}${path}`, { method, headers, body: requestBody, signal, }) } const accessToken = auth ? store.get()?.access_token : undefined if (auth && !accessToken) { throw new ApiError(401, 'Authentication required') } let response = await run(accessToken) if (auth && response.status === 401) { const refreshedSession = await refreshSession() response = await run(refreshedSession.access_token) if (response.status === 401) { store.clear() } } return readResponse(response, responseType) } return { request } } export const apiClient = createApiClient()