perf: speed up catalog audit by batching file checks and parallelizing source fetches
CI / check (push) Successful in 38s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m13s

This commit is contained in:
openhands committed 2026-08-03 19:52:09 +02:00
1 parent 40a21ba767
commit 1cbb33a4e2
1 file changed
+48 -25
+48 -25
View File
@@ -1,5 +1,4 @@
import { existsSync } from "node:fs"; import { readdir } from "node:fs/promises";
import path from "node:path";
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { fetchSourceFurnidata } from "@/lib/services/clone-import"; import { fetchSourceFurnidata } from "@/lib/services/clone-import";
@@ -141,6 +140,13 @@ export async function runCatalogAudit(
return; return;
} }
const [iconFiles, nitroFiles] = await Promise.all([
readdir(iconDir).catch(() => [] as string[]),
readdir(nitroDir).catch(() => [] as string[]),
]);
const iconSet = new Set(iconFiles);
const nitroSet = new Set(nitroFiles);
onEvent?.({ type: "progress", message: "Loading items_base…" }); onEvent?.({ type: "progress", message: "Loading items_base…" });
let items: Array<{ let items: Array<{
@@ -236,7 +242,7 @@ export async function runCatalogAudit(
}); });
// Only include items that actually have a .nitro asset — a catalog // Only include items that actually have a .nitro asset — a catalog
// entry for a furni without a bundle would be un-placeable in-game. // entry for a furni without a bundle would be un-placeable in-game.
if (existsSync(path.join(nitroDir, `${item.item_name}.nitro`))) { if (nitroSet.has(`${item.item_name}.nitro`)) {
missingCatalogEntries.push({ missingCatalogEntries.push({
classname: item.item_name, classname: item.item_name,
itemId: item.id, itemId: item.id,
@@ -305,8 +311,7 @@ export async function runCatalogAudit(
const missingNitroClassnames: string[] = []; const missingNitroClassnames: string[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
const nitroPath = path.join(nitroDir, `${item.item_name}.nitro`); if (!nitroSet.has(`${item.item_name}.nitro`)) {
if (!existsSync(nitroPath)) {
missingNitroClassnames.push(item.item_name); missingNitroClassnames.push(item.item_name);
issues.push({ issues.push({
type: "missing_nitro", type: "missing_nitro",
@@ -331,8 +336,7 @@ export async function runCatalogAudit(
const missingIconClassnames: string[] = []; const missingIconClassnames: string[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
const iconPath = path.join(iconDir, `${item.item_name}_icon.png`); if (!iconSet.has(`${item.item_name}_icon.png`)) {
if (!existsSync(iconPath)) {
missingIconClassnames.push(item.item_name); missingIconClassnames.push(item.item_name);
issues.push({ issues.push({
type: "missing_icon", type: "missing_icon",
@@ -383,7 +387,7 @@ export async function runCatalogAudit(
// and drop the ones the repair fixed from the issue list. // and drop the ones the repair fixed from the issue list.
const stillMissing: string[] = []; const stillMissing: string[] = [];
for (const classname of missingIconClassnames) { for (const classname of missingIconClassnames) {
if (!existsSync(path.join(iconDir, `${classname}_icon.png`))) { if (!iconSet.has(`${classname}_icon.png`)) {
stillMissing.push(classname); stillMissing.push(classname);
} }
} }
@@ -438,7 +442,7 @@ export async function runCatalogAudit(
// and drop the ones the repair fixed from the issue list. // and drop the ones the repair fixed from the issue list.
const stillMissing: string[] = []; const stillMissing: string[] = [];
for (const classname of missingNitroClassnames) { for (const classname of missingNitroClassnames) {
if (!existsSync(path.join(nitroDir, `${classname}.nitro`))) { if (!nitroSet.has(`${classname}.nitro`)) {
stillMissing.push(classname); stillMissing.push(classname);
} }
} }
@@ -601,27 +605,46 @@ export async function runCatalogAudit(
onEvent?.({ type: "progress", message: "Comparing with clone sources…" }); onEvent?.({ type: "progress", message: "Comparing with clone sources…" });
const sources = await listSources(); const sources = await listSources();
for (const source of sources) { const sourceResults = await Promise.all(
if (!source.furnidataUrl) continue; sources
try { .filter((s) => s.furnidataUrl)
const entries = await fetchSourceFurnidata(source.furnidataUrl); .map(async (source) => {
for (const entry of entries) { try {
if (!itemByName.has(entry.classname)) { const entries = await fetchSourceFurnidata(source.furnidataUrl);
missingFromSources.push({ return {
sourceName: source.name, source,
sourceId: source.id, entries,
classname: entry.classname, error: null,
itemName: entry.name, } as const;
}); } catch {
return {
source,
entries: [] as Array<{ classname: string; name: string }>,
error: true,
} as const;
} }
} }),
} catch { );
for (const result of sourceResults) {
if (result.error) {
issues.push({ issues.push({
type: "source_fetch_failed", type: "source_fetch_failed",
severity: "error", severity: "error",
sourceName: source.name, sourceName: result.source.name,
message: `Failed to fetch furnidata from "${source.name}"`, message: `Failed to fetch furnidata from "${result.source.name}"`,
}); });
continue;
}
for (const entry of result.entries) {
if (!itemByName.has(entry.classname)) {
missingFromSources.push({
sourceName: result.source.name,
sourceId: result.source.id,
classname: entry.classname,
itemName: entry.name,
});
}
} }
} }