import { QueryClient, queryOptions } from "@tanstack/react-query"; import { z } from "zod/mini"; import { adminFetch } from "@/lib/admin-fetch"; import type { ImportJob } from "@/lib/furni/import-job"; const endpoint = "/api/admin/studio/import-jobs"; const historyTimeoutMs = 15_000; const jobSchema = z.looseObject({ id: z.uuid(), userId: z.int().check(z.positive()), createdAt: z.iso.datetime({ offset: true }), updatedAt: z.iso.datetime({ offset: true }), state: z.enum(["queued", "running", "completed", "interrupted", "cancelled"]), translate: z.boolean(), mode: z.optional(z.literal("repair")), syncKind: z.optional(z.enum(["official", "clone"])), operationId: z.optional(z.string()), retryOf: z.optional(z.string()), sourceId: z.optional(z.string()), cancelRequested: z.optional(z.boolean()), langs: z.optional(z.array(z.string())), items: z.array( z.looseObject({ id: z.int().check(z.nonnegative()), classname: z.string(), name: z.string(), description: z.string(), type: z.string(), revision: z.number(), category: z.string(), state: z.enum([ "pending", "running", "done", "failed", "cancelled", "interrupted", ]), error: z.optional(z.string()), warnings: z.optional(z.array(z.string())), itemId: z.optional(z.number()), }), ), }); const historySchema = z.object({ ok: z.literal(true), jobs: z.array(jobSchema).check(z.maxLength(30)), nextCursor: z.optional( z.nullable( z.string().check( z.regex( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\|[0-9a-f-]{36}$/i, ), z.refine((value) => { const [timestamp, id] = value.split("|"); return ( z.iso.datetime({ precision: 3 }).safeParse(timestamp).success && z.uuid().safeParse(id).success ); }), ), ), ), }); export interface FurnitureJobsPage { jobs: ImportJob[]; nextCursor: string | null; } export class FurnitureJobsHttpError extends Error { constructor( message: string, readonly status: number, ) { super(message); } } async function responseData( response: Response, fallback: string, ): Promise { const data: unknown = await response.json().catch(() => null); if (!response.ok) { const message = data && typeof data === "object" && "error" in data && typeof data.error === "string" ? data.error : `${fallback} (${response.status})`; throw new FurnitureJobsHttpError(message, response.status); } return data; } export async function readFurnitureJobResponse( response: Response, fallback: string, ): Promise { const data = await responseData(response, fallback); const parsed = z .object({ ok: z.literal(true), job: jobSchema }) .safeParse(data); if (!parsed.success) throw Error("Invalid import job response"); return parsed.data.job as ImportJob; } /** Each mounted history owns this client; no persisted or cross-page account cache. */ export function createFurnitureJobsClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, networkMode: "always", staleTime: 0, gcTime: 0, refetchOnWindowFocus: false, refetchOnReconnect: false, }, mutations: { retry: false }, }, }); } export function furnitureJobsQueryOptions( paginated: boolean, cursor: string | null, timeoutMs = historyTimeoutMs, ) { return queryOptions({ queryKey: [ "furniture-import-history", paginated, paginated ? cursor : null, ] as const, queryFn: async ({ signal }): Promise => { const timeout = AbortSignal.timeout(timeoutMs); const combined = AbortSignal.any([signal, timeout]); const url = new URL( endpoint, typeof window === "undefined" ? "http://localhost" : window.location.origin, ); if (paginated) { url.searchParams.set("page", "1"); if (cursor) url.searchParams.set("before", cursor); } try { const response = await adminFetch(url, { signal: combined, cache: "no-store", }); const data = await responseData(response, "Import history unavailable"); const parsed = historySchema.safeParse(data); if (!parsed.success) throw Error("Invalid import history response"); return { jobs: parsed.data.jobs as ImportJob[], nextCursor: parsed.data.nextCursor ?? null, }; } catch (error) { if (timeout.aborted && !signal.aborted) throw Error("Import history request timed out"); throw error; } }, }); }