perf: offload FurnitureData translation build to a worker thread
CI / check (push) Successful in 33s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m0s

The translation build's CPU-heavy work (deep clone of the 100k+ entry
master, the two full scans and JSON.stringify per language) now runs in a
worker thread, so "Alles vertalen" no longer blocks the main event loop.
Network/DB parts (official Habbo fetch, LibreTranslate, file writes) stay
on the main thread. The pure helpers were extracted to furni-data-i18n-core
(no server-only deps) so the worker can import them. Falls back to the same
helpers on the main thread when no worker is available or in tests.
This commit is contained in:
openhands committed 2026-08-24 19:10:33 +02:00
1 parent 8d1b09f151
commit f94246e067
4 files changed
+451 -124

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import { parentPort } from "node:worker_threads";
import type { HabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
import {
type FurniDataMaster,
finalizeLangFile,
type PreparedLang,
prepareLangFile,
type Translation,
} from "./furni-data-i18n-core";
interface InitMsg {
id: number;
type: "init";
masterJson: string;
hotelMaps: Record<string, Map<string, Translation>>;
}
interface PrepareMsg {
id: number;
type: "prepare";
lang: string;
hotel: string;
}
interface FinalizeMsg {
id: number;
type: "finalize";
lang: string;
translations: Map<string, string>;
}
type Msg = InitMsg | PrepareMsg | FinalizeMsg;
let master: FurniDataMaster | null = null;
let hotelMaps: Record<string, Map<string, Translation>> = {};
const prepared = new Map<string, PreparedLang>();
if (parentPort) {
parentPort.on("message", (msg: Msg) => {
try {
if (msg.type === "init") {
master = JSON.parse(msg.masterJson);
hotelMaps = msg.hotelMaps ?? {};
parentPort!.postMessage({ id: msg.id, ok: true });
return;
}
if (msg.type === "prepare") {
if (!master) throw new Error("translation worker not initialized");
const officialMap =
hotelMaps[msg.hotel] ?? new Map<string, Translation>();
const r = prepareLangFile(
master,
msg.lang,
msg.hotel as HabboGamedataHotel,
officialMap,
);
prepared.set(msg.lang, r);
parentPort!.postMessage({
id: msg.id,
ok: true,
pendingTexts: r.pendingTexts,
});
return;
}
// finalize
const r = prepared.get(msg.lang);
if (!r) throw new Error(`no prepared data for ${msg.lang}`);
const res = finalizeLangFile(r.cloned, r.pendingRefs, msg.translations);
prepared.delete(msg.lang);
parentPort!.postMessage({ id: msg.id, ok: true, ...res });
} catch (err) {
parentPort!.postMessage({
id: msg.id,
ok: false,
error: (err as Error).message,
});
}
});
}