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 path from "node:path";
import { readdir } from "node:fs/promises";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { fetchSourceFurnidata } from "@/lib/services/clone-import";
@@ -141,6 +140,13 @@ export async function runCatalogAudit(
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…" });
let items: Array<{
@@ -236,7 +242,7 @@ export async function runCatalogAudit(
});
// Only include items that actually have a .nitro asset — a catalog
// 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({
classname: item.item_name,
itemId: item.id,
@@ -305,8 +311,7 @@ export async function runCatalogAudit(
const missingNitroClassnames: string[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const nitroPath = path.join(nitroDir, `${item.item_name}.nitro`);
if (!existsSync(nitroPath)) {
if (!nitroSet.has(`${item.item_name}.nitro`)) {
missingNitroClassnames.push(item.item_name);
issues.push({
type: "missing_nitro",
@@ -331,8 +336,7 @@ export async function runCatalogAudit(
const missingIconClassnames: string[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const iconPath = path.join(iconDir, `${item.item_name}_icon.png`);
if (!existsSync(iconPath)) {
if (!iconSet.has(`${item.item_name}_icon.png`)) {
missingIconClassnames.push(item.item_name);
issues.push({
type: "missing_icon",
@@ -383,7 +387,7 @@ export async function runCatalogAudit(
// and drop the ones the repair fixed from the issue list.
const stillMissing: string[] = [];
for (const classname of missingIconClassnames) {
if (!existsSync(path.join(iconDir, `${classname}_icon.png`))) {
if (!iconSet.has(`${classname}_icon.png`)) {
stillMissing.push(classname);
}
}
@@ -438,7 +442,7 @@ export async function runCatalogAudit(
// and drop the ones the repair fixed from the issue list.
const stillMissing: string[] = [];
for (const classname of missingNitroClassnames) {
if (!existsSync(path.join(nitroDir, `${classname}.nitro`))) {
if (!nitroSet.has(`${classname}.nitro`)) {
stillMissing.push(classname);
}
}
@@ -601,27 +605,46 @@ export async function runCatalogAudit(
onEvent?.({ type: "progress", message: "Comparing with clone sources…" });
const sources = await listSources();
for (const source of sources) {
if (!source.furnidataUrl) continue;
try {
const entries = await fetchSourceFurnidata(source.furnidataUrl);
for (const entry of entries) {
if (!itemByName.has(entry.classname)) {
missingFromSources.push({
sourceName: source.name,
sourceId: source.id,
classname: entry.classname,
itemName: entry.name,
});
const sourceResults = await Promise.all(
sources
.filter((s) => s.furnidataUrl)
.map(async (source) => {
try {
const entries = await fetchSourceFurnidata(source.furnidataUrl);
return {
source,
entries,
error: null,
} 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({
type: "source_fetch_failed",
severity: "error",
sourceName: source.name,
message: `Failed to fetch furnidata from "${source.name}"`,
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)) {
missingFromSources.push({
sourceName: result.source.name,
sourceId: result.source.id,
classname: entry.classname,
itemName: entry.name,
});
}
}
}