"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { listLogs, type ApiError } from "@/lib/api-client"; import type { LogQueryParams, LogsPage } from "@/types/log"; export type QueryState = | { status: "loading"; data: T | null } | { status: "success"; data: T } | { status: "empty"; data: T } | { status: "error"; error: ApiError; data: T | null }; /** * Fetch a page of logs. * * Two behaviours the dashboard previously lacked. Failures are surfaced instead of * being swallowed into `console.error` and falling through to the empty state, so a * dead backend no longer looks like an empty database. And each request aborts the * one before it, so rapidly changing a filter cannot leave an older response * arriving last and overwriting the newer one. */ export function useLogsQuery(params: LogQueryParams) { const [state, setState] = useState>({ status: "loading", data: null }); const abortRef = useRef(null); const [reloadToken, setReloadToken] = useState(0); const key = JSON.stringify(params); useEffect(() => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setState((previous) => ({ status: "loading", data: previous.data })); void (async () => { const result = await listLogs(JSON.parse(key) as LogQueryParams, controller.signal); if (controller.signal.aborted) return; if (!result.ok) { // A cancelled request is not a failure to report; a newer one is running. if (result.error.kind === "aborted") return; setState((previous) => ({ status: "error", error: result.error, data: previous.data })); return; } setState({ status: result.data.items.length === 0 ? "empty" : "success", data: result.data, }); })(); return () => controller.abort(); }, [key, reloadToken]); const refetch = useCallback(() => setReloadToken((token) => token + 1), []); return { ...state, refetch }; }