import { useCallback, useRef, useState } from "react"; import { useAdapters } from "../contexts/adapter-context.js"; import { useRulesEditionContext } from "../contexts/rules-edition-context.js"; const BATCH_SIZE = 6; interface BulkImportState { readonly status: "idle" | "loading" | "complete" | "partial-failure"; readonly total: number; readonly completed: number; readonly failed: number; } const IDLE_STATE: BulkImportState = { status: "idle", total: 0, completed: 0, failed: 0, }; interface BulkImportHook { state: BulkImportState; startImport: ( baseUrl: string, fetchAndCacheSource: (sourceCode: string, url: string) => Promise, isSourceCached: (sourceCode: string) => Promise, refreshCache: () => Promise, ) => void; reset: () => void; } export function useBulkImport(): BulkImportHook { const { bestiaryIndex, pf2eBestiaryIndex } = useAdapters(); const { edition } = useRulesEditionContext(); const indexPort = edition === "pf2e" ? pf2eBestiaryIndex : bestiaryIndex; const [state, setState] = useState(IDLE_STATE); const countersRef = useRef({ completed: 0, failed: 0 }); const startImport = useCallback( ( baseUrl: string, fetchAndCacheSource: (sourceCode: string, url: string) => Promise, isSourceCached: (sourceCode: string) => Promise, refreshCache: () => Promise, ) => { const allCodes = indexPort.getAllSourceCodes(); const total = allCodes.length; countersRef.current = { completed: 0, failed: 0 }; setState({ status: "loading", total, completed: 0, failed: 0 }); void (async () => { const cacheChecks = await Promise.all( allCodes.map(async (code) => ({ code, cached: await isSourceCached(code), })), ); const alreadyCached = cacheChecks.filter((c) => c.cached).length; const uncached = cacheChecks.filter((c) => !c.cached); countersRef.current.completed = alreadyCached; if (uncached.length === 0) { setState({ status: "complete", total, completed: total, failed: 0, }); return; } setState((s) => ({ ...s, completed: alreadyCached })); const batches: { code: string }[][] = []; for (let i = 0; i < uncached.length; i += BATCH_SIZE) { batches.push(uncached.slice(i, i + BATCH_SIZE)); } await batches.reduce( (chain, batch) => chain.then(() => Promise.allSettled( batch.map(async ({ code }) => { const url = indexPort.getDefaultFetchUrl(code, baseUrl); try { await fetchAndCacheSource(code, url); countersRef.current.completed++; } catch (err) { countersRef.current.failed++; console.warn( `[bulk-import] FAILED ${code} (${url}):`, err instanceof Error ? err.message : err, ); } setState({ status: "loading", total, completed: countersRef.current.completed, failed: countersRef.current.failed, }); }), ), ), Promise.resolve() as Promise, ); await refreshCache(); const { completed, failed } = countersRef.current; setState({ status: failed > 0 ? "partial-failure" : "complete", total, completed, failed, }); })(); }, [indexPort], ); const reset = useCallback(() => { setState(IDLE_STATE); }, []); return { state, startImport, reset }; }