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
+45 -22
+45 -22
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,28 +605,47 @@ 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
.filter((s) => s.furnidataUrl)
.map(async (source) => {
try { try {
const entries = await fetchSourceFurnidata(source.furnidataUrl); const entries = await fetchSourceFurnidata(source.furnidataUrl);
for (const entry of entries) { return {
source,
entries,
error: null,
} as const;
} catch {
return {
source,
entries: [] as Array<{ classname: string; name: string }>,
error: true,
} as const;
}
}),
);
for (const result of sourceResults) {
if (result.error) {
issues.push({
type: "source_fetch_failed",
severity: "error",
sourceName: result.source.name,
message: `Failed to fetch furnidata from "${result.source.name}"`,
});
continue;
}
for (const entry of result.entries) {
if (!itemByName.has(entry.classname)) { if (!itemByName.has(entry.classname)) {
missingFromSources.push({ missingFromSources.push({
sourceName: source.name, sourceName: result.source.name,
sourceId: source.id, sourceId: result.source.id,
classname: entry.classname, classname: entry.classname,
itemName: entry.name, itemName: entry.name,
}); });
} }
} }
} catch {
issues.push({
type: "source_fetch_failed",
severity: "error",
sourceName: source.name,
message: `Failed to fetch furnidata from "${source.name}"`,
});
}
} }
issues.sort((a, b) => { issues.sort((a, b) => {