import type { Acc, User } from "./types"; const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000"; // Tab-switch responses come from the Next.js data cache for this many // seconds before re-fetching. Mutations call revalidateTag() to evict // the cached entry and force the next read to hit MySQL. const CACHE_REVALIDATE_SECONDS = 30; export const ACCOUNTS_TAG = "accounts"; export const USERS_TAG = "users"; // Page size used for both initial server-side fetch and infinite scroll // on the client. 200 keeps each cached payload under ~50KB and renders // in well under one frame even on phones. export const PAGE_SIZE = 200; export type Page = { rows: T[]; hasMore: boolean; total: number }; export type AccountsPageOpts = { offset?: number; prefix?: string; sort?: "username" | "status"; dir?: "asc" | "desc"; q?: string; }; export type UsersPageOpts = { offset?: number; prefix?: string; sort?: "f_username" | "t_username" | "last_update_time"; dir?: "asc" | "desc"; q?: string; }; type FetchInit = { method?: "GET" | "POST"; body?: unknown; cache?: RequestCache; next?: { revalidate?: number; tags?: string[] }; }; export async function fetchApi(path: string, options: FetchInit = {}): Promise { const url = `${API_BASE_URL}${path}`; const init: RequestInit & { next?: FetchInit["next"] } = { method: options.method ?? "GET", headers: options.body ? { "content-type": "application/json" } : undefined, body: options.body ? JSON.stringify(options.body) : undefined, }; if (options.next) { init.next = options.next; } else { init.cache = options.cache ?? "no-store"; } const res = await fetch(url, init); if (!res.ok) { throw new Error(`API ${options.method ?? "GET"} ${path} failed: ${res.status}`); } return res.json(); } function buildAccountsUrl(opts: AccountsPageOpts): string { const { offset = 0, prefix = "", sort = "username", dir = "desc", q = "" } = opts; const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset), sort, dir, }); if (prefix) params.set("prefix", prefix); if (q) params.set("q", q); return `/acc/?${params.toString()}`; } function buildUsersUrl(opts: UsersPageOpts): string { const { offset = 0, prefix = "", sort = "last_update_time", dir = "desc", q = "" } = opts; const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset), sort, dir, }); if (prefix) params.set("prefix", prefix); if (q) params.set("q", q); return `/user/?${params.toString()}`; } export async function getAccountsPage(opts: AccountsPageOpts = {}): Promise> { const data = (await fetchApi(buildAccountsUrl(opts), { next: { revalidate: CACHE_REVALIDATE_SECONDS, tags: [ACCOUNTS_TAG] }, })) as { rows: Acc[]; total: number }; return { rows: data.rows, hasMore: data.rows.length === PAGE_SIZE, total: data.total, }; } export async function getUsersPage(opts: UsersPageOpts = {}): Promise> { const data = (await fetchApi(buildUsersUrl(opts), { next: { revalidate: CACHE_REVALIDATE_SECONDS, tags: [USERS_TAG] }, })) as { rows: User[]; total: number }; return { rows: data.rows, hasMore: data.rows.length === PAGE_SIZE, total: data.total, }; }