Files
initiative/apps/web/src/hooks/use-bulk-import.ts

121 lines
2.9 KiB
TypeScript

import { useCallback, useRef, useState } from "react";
import {
getAllSourceCodes,
getDefaultFetchUrl,
} from "../adapters/bestiary-index-adapter.js";
const BATCH_SIZE = 6;
export 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<void>,
isSourceCached: (sourceCode: string) => Promise<boolean>,
refreshCache: () => Promise<void>,
) => void;
reset: () => void;
}
export function useBulkImport(): BulkImportHook {
const [state, setState] = useState<BulkImportState>(IDLE_STATE);
const countersRef = useRef({ completed: 0, failed: 0 });
const startImport = useCallback(
(
baseUrl: string,
fetchAndCacheSource: (sourceCode: string, url: string) => Promise<void>,
isSourceCached: (sourceCode: string) => Promise<boolean>,
refreshCache: () => Promise<void>,
) => {
const allCodes = getAllSourceCodes();
const total = allCodes.length;
countersRef.current = { completed: 0, failed: 0 };
setState({ status: "loading", total, completed: 0, failed: 0 });
(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 }));
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
const batch = uncached.slice(i, i + BATCH_SIZE);
await Promise.allSettled(
batch.map(async ({ code }) => {
const url = 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,
});
}),
);
}
await refreshCache();
const { completed, failed } = countersRef.current;
setState({
status: failed > 0 ? "partial-failure" : "complete",
total,
completed,
failed,
});
})();
},
[],
);
const reset = useCallback(() => {
setState(IDLE_STATE);
}, []);
return { state, startImport, reset };
}