Files
SubMiner/vendor/hachidori/extension/engine-worker-runtime.js
T
sudacode d9fdc7ef6d feat(dictionary): add Hachidori backend support
- Add backend selection, setup gating, Anki integration, and external host support
- Add launcher flags, documentation, packaging, and focused tests
- Open on-demand overlay modals on the first attempt
2026-09-22 00:21:19 -07:00

110 lines
3.7 KiB
JavaScript

/*
* Dedicated owner of a pthread Hoshidicts WASM runtime.
*
* The runtime lives off the browser main thread so Hoshidicts can join pthreads
* and WasmFS can synchronously proxy OPFS operations without deadlocking an
* extension page. engine-worker.js starts it on WasmFS + direct OPFS and
* engine-worker-idbfs.js on the classic FS + IDBFS; both call startEngineWorker.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
configureEngineService,
handleEngineMessage,
startEngine,
} from "./engine-service.js";
import { boundResponseFailure } from "./response-limits.js";
let nextHostRequestId = 0;
let nextProgressId = 0;
const HOST_REQUEST_TIMEOUT_MS = 30_000;
const pendingHostRequests = new Map();
const pendingProgressAcks = new Map();
function describe(error) {
return error instanceof Error ? error.message || String(error) : String(error);
}
function requestHost(message) {
nextHostRequestId += 1;
const id = nextHostRequestId;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (!pendingHostRequests.delete(id)) return;
reject(new Error("the extension host did not answer the engine worker"));
}, HOST_REQUEST_TIMEOUT_MS);
pendingHostRequests.set(id, { resolve, reject, timer });
globalThis.postMessage({ channel: "host-request", id, message });
});
}
function reportEngineProgress(progress) {
if (progress?.phase !== "installing") {
globalThis.postMessage({ channel: "engine-progress", progress });
return undefined;
}
nextProgressId += 1;
const id = nextProgressId;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (!pendingProgressAcks.delete(id)) return;
reject(new Error("the extension host did not acknowledge the import lock"));
}, HOST_REQUEST_TIMEOUT_MS);
pendingProgressAcks.set(id, { resolve, reject, timer });
globalThis.postMessage({ channel: "engine-progress", id, progress });
});
}
export function startEngineWorker({ createHoshidicts, storageBackend }) {
configureEngineService(requestHost, {
createHoshidicts,
storageBackend,
lowRam: false,
reportProgress: reportEngineProgress,
});
startEngine();
// A dedicated worker receives only from its creator over its implicit
// MessagePort. MessageEvent.origin is always empty, so there is no origin
// value to validate; the channel checks in onHostMessage validate the
// expected protocol.
globalThis.onmessage = onHostMessage; // NOSONAR
}
function onHostMessage(event) {
const data = event.data;
if (data?.channel === "host-response") {
const pending = pendingHostRequests.get(data.id);
if (pending === undefined) return;
pendingHostRequests.delete(data.id);
clearTimeout(pending.timer);
if (data.ok === true) pending.resolve(data.response);
else pending.reject(new Error(data.error || "host request failed"));
return;
}
if (data?.channel === "engine-progress-ack") {
const pending = pendingProgressAcks.get(data.id);
if (pending === undefined) return;
pendingProgressAcks.delete(data.id);
clearTimeout(pending.timer);
if (data.ok === true) pending.resolve();
else pending.reject(new Error(data.error || "the extension host refused the import lock"));
return;
}
if (data?.channel !== "engine-request") return;
Promise.resolve(handleEngineMessage(data.message)).then(
(response) => globalThis.postMessage({ channel: "engine-response", id: data.id, response }),
(error) => globalThis.postMessage({
channel: "engine-response",
id: data.id,
response: boundResponseFailure({
type: `${data.message?.type || "hd_unknown"}_result`,
requestId: data.message?.requestId ?? null,
ok: false,
error: describe(error),
}),
}),
);
}