SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
2.1 KB · 59 lines typescript
Raw Blame History
1import "server-only";2import { notFound } from "next/navigation";3import type { Envelope } from "./types";45export const API_URL = (process.env.API_URL ?? "http://127.0.0.1:8391").replace(/\/$/, "");67export class ApiRequestError extends Error {8  constructor(9    public status: number,10    public code: string,11    message: string,12  ) {13    super(message);14  }15}1617/** Server-side fetch of an API envelope. 404 → Next `notFound()`; other failures throw (caught by error.tsx). */18export async function apiEnvelope<T>(path: string, init: { revalidate?: number; timeoutMs?: number } = {}): Promise<Envelope<T>> {19  const ctrl = new AbortController();20  const timer = setTimeout(() => ctrl.abort(), init.timeoutMs ?? 12_000);21  try {22    const res = await fetch(`${API_URL}${path}`, {23      signal: ctrl.signal,24      headers: { accept: "application/json" },25      ...(init.revalidate === undefined ? { cache: "no-store" } : { next: { revalidate: init.revalidate } }),26    });27    if (res.status === 404) notFound();28    if (!res.ok) {29      let code = "API_ERROR";30      let message = `API ${res.status}`;31      try {32        const body = (await res.json()) as { error?: { code?: string; message?: string } };33        code = body.error?.code ?? code;34        message = body.error?.message ?? message;35      } catch {36        /* ignore */37      }38      throw new ApiRequestError(res.status, code, message);39    }40    return (await res.json()) as Envelope<T>;41  } finally {42    clearTimeout(timer);43  }44}4546export async function api<T>(path: string, init?: { revalidate?: number; timeoutMs?: number }): Promise<T> {47  return (await apiEnvelope<T>(path, init)).data;48}4950/** Best-effort variant for non-critical widgets: returns null instead of throwing. */51export async function apiOptional<T>(path: string, init?: { revalidate?: number; timeoutMs?: number }): Promise<T | null> {52  try {53    return await api<T>(path, init);54  } catch (err) {55    if (err && typeof err === "object" && "digest" in err && String((err as { digest: unknown }).digest).startsWith("NEXT_HTTP_ERROR_FALLBACK")) return null;56    return null;57  }58}59