mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- 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
110 lines
3.7 KiB
JavaScript
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),
|
|
}),
|
|
}),
|
|
);
|
|
}
|