mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
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
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user