feat(转换能力): 支持原生与网页批量并行
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-18 01:48:36 +08:00
1 parent 0eae7c72d0
commit a0899fa150
15 files changed
+549 -119

No files matched your search

+1
View File
@@ -199,6 +199,7 @@
<div id="convertFontRow" class="form-row" hidden><label for="convertFontMode">字体</label><select id="convertFontMode"><option value="0">保持</option><option value="1">内嵌</option><option value="2">外部</option></select></div>
<div id="convertDPIRow" class="form-row" hidden><label for="convertDPI">精度</label><select id="convertDPI"><option value="96">96 dpi</option><option value="150">150 dpi</option><option value="300" selected>300 dpi</option><option value="600">600 dpi</option></select></div>
<div class="form-row"><label for="convertDestination">保存</label><select id="convertDestination"><option value="directory">目录</option><option value="archive">打包</option></select></div>
<div class="form-row"><label for="convertParallel">并行</label><input id="convertParallel" type="number" min="1" step="1" placeholder="自动"></div>
</div>
<button id="convertClose" class="button" type="button">关闭</button><button id="convertCancel" class="button" type="button" hidden>取消</button><button id="convertStart" class="button" type="submit" disabled>转换</button>
</div>
+6 -5
View File
@@ -803,7 +803,7 @@ button:not(:disabled):hover {
.convert-options {
flex: none;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-column: 1 / -1;
grid-row: 1;
width: 100%;
@@ -812,11 +812,11 @@ button:not(:disabled):hover {
}
.convert-options:has(#convertDPIRow[hidden]):has(#convertFontRow[hidden]) {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.convert-options:has(#convertDPIRow:not([hidden])):has(#convertFontRow:not([hidden])) {
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.convert-options .form-row {
@@ -1059,12 +1059,13 @@ button:not(:disabled):hover {
margin: 0;
}
.convert-options:has(#convertDPIRow[hidden]):has(#convertFontRow[hidden]) {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.convert-options:has(#convertDPIRow:not([hidden])):has(#convertFontRow:not([hidden])) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.convert-options:has(#convertDPIRow:not([hidden])):has(#convertFontRow:not([hidden])) .form-row:last-child {
.convert-options:has(#convertDPIRow[hidden]):has(#convertFontRow:not([hidden])) .form-row:last-child,
.convert-options:has(#convertDPIRow:not([hidden])):has(#convertFontRow[hidden]) .form-row:last-child {
grid-column: 1 / -1;
}
.convert-options .form-row {
+118 -66
View File
@@ -20,8 +20,8 @@ const STATUS = {
};
const wasmRequests = new Map();
const metaContents = new WeakMap();
const batch = { items: [], formats: [], running: false, canceled: false, worker: null, requests: new Map(), sequence: 0, activeID: 0, progressTime: 0, progress: null };
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
const batch = { items: [], formats: [], running: false, canceled: false, workers: new Set(), requests: new Map(), sequence: 0, progress: null, jobs: [], credentials: Promise.resolve() };
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "Parallel", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
const fontBinding = { font: null, choices: [], sequence: 0 };
const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)]));
const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "ReferenceGroup", "Actions", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "BindingActions", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)]));
@@ -8128,7 +8128,7 @@ function renderBatchList() {
}
function updateBatchControls() {
for (const name of ["Add", "Clear", "Format", "Compression", "FontMode", "Destination", "DPI"]) batchElements[name].disabled = batch.running;
for (const name of ["Add", "Clear", "Format", "Compression", "FontMode", "Destination", "DPI", "Parallel"]) batchElements[name].disabled = batch.running;
batchElements.FontRow.hidden = !(batch.items.length ? batch.items : [null]).some(item => (item?.format || batchElements.Format.value) === "ofd");
batchElements.Compression.disabled ||= (batch.items.length ? batch.items : [null]).every(item => (item?.format || batchElements.Format.value) === "txt");
batchElements.Clear.disabled ||= !batch.items.length;
@@ -8159,16 +8159,31 @@ function batchStatus(item, status, text) {
function batchProgress(item, position, count, progress) {
if (batch.canceled) return;
const now = performance.now();
const previous = batch.progress;
const previous = item ? item.progress : batch.progress;
const sameStage = previous?.item === item && previous?.progress.phase === progress.phase;
if (sameStage && now - batch.progressTime < 80 && progress.completed !== progress.total) return;
batch.progressTime = now;
batch.progress = { item, position, count, progress, started: sameStage ? previous.started : now };
if (sameStage && now - previous.updated < 80 && progress.completed !== progress.total) return;
const current = { item, position, count, progress, started: sameStage ? previous.started : now, updated: now };
if (item) item.progress = current;
else batch.progress = current;
updateBatchProgress();
}
function updateBatchProgress() {
if (!batch.progress) return;
if (batch.canceled) return;
for (const item of batch.jobs) {
if (item.status !== "running" || !item.progress) continue;
const progress = item.progress.progress;
const stage = progress.phase?.includes("compress") ? "压缩" : progress.phase?.startsWith("write") ? "写入" : { open: "读取", pages: "解析", convert: "转换", prepare: "准备", fonts: "字体", export: "导出" }[progress.phase] || "处理";
batchStatus(item, "running", `${stage}${progress.total > 0 ? ` ${progress.completed}/${progress.total}` : ""}`);
}
if (!batch.progress) {
const completed = batch.jobs.filter(item => ["done", "ready", "unchanged", "failed", "canceled"].includes(item.status)).length;
const running = batch.jobs.filter(item => item.status === "running").length;
batchElements.Status.textContent = `完成 ${completed}/${batch.jobs.length} · 运行 ${running}`;
batchElements.Progress.max = batch.jobs.length || 1;
batchElements.Progress.value = completed;
return;
}
const { item, position, count, progress, started } = batch.progress;
const compressing = progress.phase === "compress" || progress.phase === "write.compress";
const fontMode = (item?.format || batchElements.Format.value) === "ofd" ? batchElements.FontMode.value : "0";
@@ -8182,29 +8197,29 @@ function updateBatchProgress() {
else batchElements.Progress.removeAttribute("value");
}
function batchCall(name, args, onProgress) {
function batchCall(slot, name, args, onProgress) {
return new Promise((resolve, reject) => {
if (batch.workerError) { reject(batch.workerError); return; }
if (slot.error) { reject(slot.error); return; }
if (batch.canceled) { reject(new DOMException("已取消转换", "AbortError")); return; }
const id = ++batch.sequence;
batch.activeID = id;
batch.requests.set(id, { resolve, reject, onProgress, destination: name === "ofdgoConvertFile" ? args[3] : null });
try { batch.worker.postMessage({ id, name, args }); }
catch (err) { batch.requests.delete(id); batch.activeID = 0; reject(err); }
batch.requests.set(id, { slot, resolve, reject, onProgress, destination: name === "ofdgoConvertFile" ? args[3] : null });
try { slot.worker.postMessage({ id, name, args }); }
catch (err) { batch.requests.delete(id); reject(err); }
});
}
function startBatchWorker() {
batch.workerError = null;
return new Promise((resolve, reject) => {
const worker = createEngineWorker();
batch.worker = worker;
const slot = { worker, error: null };
batch.workers.add(slot);
const fail = async error => {
batch.workerError = error;
slot.error = error;
worker.terminate();
reject(error);
const requests = [...batch.requests.values()];
batch.requests.clear();
for (const request of requests) {
for (const [id, request] of batch.requests) {
if (request.slot !== slot) continue;
batch.requests.delete(id);
if (request.destination && request.outputName) {
try { await request.destination.removeEntry(request.outputName, { recursive: true }); }
catch (err) { error = new Error(`${error.message};清理失败:${err.message}`); }
@@ -8215,7 +8230,7 @@ function startBatchWorker() {
worker.onerror = event => fail(new Error(event.message || "转换引擎异常"));
worker.onmessageerror = () => fail(new Error("转换数据传递失败"));
worker.onmessage = async ({ data }) => {
if (data.type === "ready") { resolve(); return; }
if (data.type === "ready") { resolve(slot); return; }
if (data.type === "exit") { fail(new Error(data.error)); return; }
if (data.type === "progress") {
if (!batch.canceled) batchElements.Status.textContent = { download: "正在下载", compile: "正在编译", start: "正在启动" }[data.phase] || "正在准备";
@@ -8237,8 +8252,8 @@ function startBatchWorker() {
}
if (data.type === "output") { request.outputName = data.name; return; }
if (data.type === "conversion") { request.onProgress?.(data); return; }
if (data.memory) slot.memory = data.memory;
batch.requests.delete(data.id);
if (batch.activeID === data.id) batch.activeID = 0;
if (data.ok) request.resolve(data.data);
else request.reject(Object.assign(new Error(data.error), { code: data.code, name: data.canceled ? "AbortError" : "Error" }));
};
@@ -8250,7 +8265,8 @@ function cancelBatch() {
batchElements.Cancel.disabled = true;
batch.progress = null;
batchElements.Status.textContent = "正在取消";
if (batch.activeID) batch.worker.postMessage({ type: "cancel", id: batch.activeID });
if (batch.unlocking) finishCredentials(null);
for (const [id, request] of batch.requests) request.slot.worker.postMessage({ type: "cancel", id });
}
function warnBatch(event) {
@@ -8286,6 +8302,8 @@ async function runBatch() {
batch.running = true;
batch.canceled = false;
batch.progress = null;
batch.jobs = jobs;
batch.credentials = Promise.resolve();
const progressTimer = window.setInterval(updateBatchProgress, 1000);
renderBatchList();
window.addEventListener("beforeunload", warnBatch);
@@ -8294,7 +8312,7 @@ async function runBatch() {
let storage = null;
let stagingName = "";
let packed = false;
const entries = [];
const results = new Array(jobs.length);
try {
if (archive) {
zipFile = await saveFilePicker({ suggestedName: "转换结果.zip", types: [{ description: "ZIP", accept: { "application/zip": [".zip"] } }] });
@@ -8313,59 +8331,93 @@ async function runBatch() {
batch.localFonts = fontManager.localEntries();
batch.fontData = new Map();
batchElements.Status.textContent = "正在准备";
await startBatchWorker();
const first = await startBatchWorker();
const fonts = await fontManager.files(fontManager.records());
const names = new Set();
for (const [position, item] of jobs.entries()) {
if (batch.canceled) break;
const plans = jobs.map(item => {
item.details = [];
const format = batch.formats.find(format => format.value === (item.format || defaultFormat.value));
const base = item.file.name.replace(/\.[^.]+$/, "").replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/[. ]+$/g, "") || "document";
let unique = base;
const outputName = () => (format.paged ? unique : `${unique}.${format.extension}`).toLowerCase();
for (let suffix = 2; archive && names.has(outputName()); suffix++) unique = `${base} (${suffix})`;
for (let suffix = 2; names.has(outputName()); suffix++) unique = `${base} (${suffix})`;
names.add(outputName());
const options = { format: format.value, pages: pageRange(item), dpi, backend, archive, compression, base: unique, credentials: null,
fontNames: [...batch.localFonts.keys()], fontMode: format.value === "ofd" ? Number(batchElements.FontMode.value) : 0 };
try {
for (;;) {
try {
const result = await batchCall("ofdgoConvertFile", [item.file, options, fonts, destination], progress => batchProgress(item, position, jobs.length, progress));
for (const file of result.files) entries.push(file);
item.details = (result.pdf?.Warnings || []).map(warning => `${warning.Page ? `第${warning.Page}页:` : ""}${warning.Message}`);
for (const font of result.fonts || []) item.details.push(`${font.name}:${font.changed ? options.fontMode === 1 ? "已嵌入" : "已外置" : `未处理 · ${fontBindingError(font.reason)}`}`);
if (result.unchanged && !result.bytes) {
batchStatus(item, "unchanged", "原样");
item.details = ["无需转换,源文件未改动"];
} else batchStatus(item, archive ? "ready" : "done", archive ? "待存" : "完成");
break;
} catch (err) {
if (batch.canceled || !["credentialsRequired", "invalidCredentials", "pdfPassword"].includes(err.code)) throw err;
const code = err.code === "pdfPassword" ? options.credentials ? "invalidCredentials" : "credentialsRequired" : err.code;
options.credentials?.password?.fill(0);
options.credentials?.key?.fill(0);
options.credentials?.keyPassword?.fill(0);
options.credentials = await requestCredentials(code, state.openSeq, err.code === "pdfPassword");
if (!options.credentials) throw new DOMException("已取消解锁", "AbortError");
const preservesEncryption = err.code === "pdfPassword" && format.value === "pdf" && compression.mode !== 0 && !options.pages;
if (!preservesEncryption && !window.confirm("转换结果将不保留原加密,是否继续?")) throw new DOMException("已取消转换", "AbortError");
return { item, format, options };
});
const capacity = nativeHost ? (await nativeHost.call("batch", {})).parallelism : Math.max(1, (navigator.hardwareConcurrency || 2) - 1);
const automatic = nativeHost ? capacity : Math.min(capacity, Math.max(1, Math.floor((navigator.deviceMemory || 4) / 2)));
const parallel = Math.min(jobs.length, nativeHost ? capacity : jobs.length, Number(batchElements.Parallel.value) || automatic);
const workerMemory = (navigator.deviceMemory || 4) * 1024 ** 3 / 4 / parallel;
let next = 0;
const run = async initial => {
let slot = initial;
while (!batch.canceled && next < plans.length) {
const position = next++;
const { item, format, options } = plans[position];
batchStatus(item, "running", "准备");
updateBatchProgress();
try {
if (!slot || slot.error) slot = await startBatchWorker();
for (;;) {
try {
const result = await batchCall(slot, "ofdgoConvertFile", [item.file, options, fonts, destination], progress => batchProgress(item, position, jobs.length, progress));
results[position] = result.files;
item.details = (result.pdf?.Warnings || []).map(warning => `${warning.Page ? `第${warning.Page}页:` : ""}${warning.Message}`);
for (const font of result.fonts || []) item.details.push(`${font.name}:${font.changed ? options.fontMode === 1 ? "已嵌入" : "已外置" : `未处理 · ${fontBindingError(font.reason)}`}`);
if (result.unchanged && !result.bytes) {
batchStatus(item, "unchanged", "原样");
item.details = ["无需转换,源文件未改动"];
} else batchStatus(item, archive ? "ready" : "done", archive ? "待存" : "完成");
break;
} catch (err) {
if (batch.canceled || !["credentialsRequired", "invalidCredentials", "pdfPassword"].includes(err.code)) throw err;
const code = err.code === "pdfPassword" ? options.credentials ? "invalidCredentials" : "credentialsRequired" : err.code;
options.credentials?.password?.fill(0);
options.credentials?.key?.fill(0);
options.credentials?.keyPassword?.fill(0);
const unlock = batch.credentials.then(async () => {
if (batch.canceled) throw new DOMException("已取消转换", "AbortError");
batch.unlocking = true;
try { return await requestCredentials(code, state.openSeq, err.code === "pdfPassword"); }
finally { batch.unlocking = false; }
});
batch.credentials = unlock.then(() => {}, () => {});
options.credentials = await unlock;
if (!options.credentials) throw new DOMException("已取消解锁", "AbortError");
const preservesEncryption = err.code === "pdfPassword" && format.value === "pdf" && compression.mode !== 0 && !options.pages;
if (!preservesEncryption && !window.confirm("转换结果将不保留原加密,是否继续?")) throw new DOMException("已取消转换", "AbortError");
}
}
} catch (err) {
batchStatus(item, err.name === "AbortError" ? "canceled" : "failed", err.name === "AbortError" ? "取消" : "失败");
item.details = [err.message];
} finally {
item.progress = null;
options.credentials?.password?.fill(0);
options.credentials?.key?.fill(0);
options.credentials?.keyPassword?.fill(0);
if (!nativeHost && slot && (next >= plans.length || slot.memory >= workerMemory)) {
slot.worker.terminate();
batch.workers.delete(slot);
slot = null;
}
}
} catch (err) {
batchStatus(item, err.name === "AbortError" ? "canceled" : "failed", err.name === "AbortError" ? "取消" : "失败");
item.details = [err.message];
if (batch.workerError) throw err;
} finally {
batch.progress = null;
options.credentials?.password?.fill(0);
options.credentials?.key?.fill(0);
options.credentials?.keyPassword?.fill(0);
updateBatchProgress();
}
batchElements.Progress.max = jobs.length;
batchElements.Progress.value = position + 1;
}
};
const running = Array.from({ length: parallel }, (_, index) => (async () => {
if (index) await new Promise(resolve => window.setTimeout(resolve, index * 200));
await run(index === 0 ? first : null);
})());
const settled = await Promise.allSettled(running);
const failure = settled.find(result => result.status === "rejected");
if (failure) throw failure.reason;
const entries = results.flat().filter(Boolean);
if (archive && entries.length && !batch.canceled) {
const result = await batchCall("ofdgoPackFiles", [entries, zipFile, compression], progress => batchProgress(null, 0, 1, progress));
const slot = [...batch.workers].find(slot => !slot.error) || await startBatchWorker();
const result = await batchCall(slot, "ofdgoPackFiles", [entries, zipFile, compression], progress => batchProgress(null, 0, 1, progress));
if (result.blob) downloadBytes(result.blob, "application/zip", "转换结果.zip");
packed = true;
for (const item of jobs) if (item.status === "ready") batchStatus(item, "done", "完成");
@@ -8381,10 +8433,10 @@ async function runBatch() {
batch.progress = null;
if (!batchElements.Progress.hasAttribute("value")) batchElements.Progress.value = 0;
if (archive && !packed) for (const item of jobs) if (item.status === "ready") { item.status = "pending"; item.text = "待转"; }
batch.worker?.terminate();
batch.worker = null;
for (const slot of batch.workers) slot.worker.terminate();
batch.workers.clear();
batch.requests.clear();
batch.activeID = 0;
if (nativeHost) await nativeHost.release(results);
batch.fontData?.clear();
batch.localFonts?.clear();
if (storage && stagingName) {
+36 -5
View File
@@ -1,5 +1,15 @@
let engineModule = null;
export function createEngineWorker() {
return nativeHost ? nativeHost.worker() : new Worker("./ofdgo_wasm.js");
if (nativeHost) return nativeHost.worker();
const worker = new Worker("./ofdgo_wasm.js");
worker.addEventListener("message", event => {
if (event.data.type !== "module") return;
event.stopImmediatePropagation();
engineModule ||= WebAssembly.compileStreaming(fetch("./ofdgo.wasm")).catch(error => { engineModule = null; throw error; });
engineModule.then(module => worker.postMessage({ type: "module", module }), error => worker.postMessage({ type: "module", error: error.message }));
});
return worker;
}
export async function documentBytes(file) {
@@ -124,7 +134,11 @@ class NativeHost {
let unsubscribe;
let pending = Promise.resolve();
const requests = new Map();
const emit = data => { if (!closed) worker.onmessage?.({data}); };
const completions = new Map();
const emit = data => {
if (data.type === "result") { completions.get(data.id)?.(data.result); return; }
if (!closed) worker.onmessage?.({data});
};
const ready = host.call("engine.open", {}).then(({engine}) => {
if (closed) return engine;
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
@@ -147,7 +161,21 @@ class NativeHost {
const engine = await ready;
if (closed) return;
controller.signal.throwIfAborted();
const result = await host.call("engine.call", {engine, id:data.id, name:data.name, args:data.args}, controller.signal);
const args = {engine, id:data.id, name:data.name, args:data.args};
let result;
if (data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles") {
const completed = new Promise(resolve => completions.set(data.id, resolve));
const cancel = () => host.call("engine.cancel", {engine, id:data.id}).catch(() => {});
controller.signal.addEventListener("abort", cancel, {once:true});
try {
await host.call("engine.start", args, controller.signal);
result = await completed;
await host.call("engine.finish", {engine, id:data.id});
} finally {
controller.signal.removeEventListener("abort", cancel);
completions.delete(data.id);
}
} else result = await host.call("engine.call", args, controller.signal);
if (closed) { await host.release(result); return; }
let decoded;
try {
@@ -163,9 +191,12 @@ class NativeHost {
terminate() {
if (closed) return;
closed = true;
unsubscribe?.();
for (const controller of requests.values()) controller.abort();
ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
ready.then(engine => host.call("engine.close", {engine})).catch(() => {}).finally(() => {
unsubscribe?.();
for (const resolve of completions.values()) resolve({ok:false, canceled:true, error:"会话已关闭"});
completions.clear();
});
},
};
ready.catch(() => {});
+19 -12
View File
@@ -4,6 +4,7 @@ let pending = Promise.resolve();
const operations = new Map();
const fontRequests = new Map();
let fontSequence = 0;
let engineMemory = null;
function requestFont(id, signal, name, done) {
if (signal.aborted) { done(null, "", true); return; }
@@ -20,6 +21,11 @@ function requestFont(id, signal, name, done) {
self.postMessage({ id, type: "font", request, name });
}
self.onmessage = ({ data }) => {
if (data.type === "module") {
if (data.error) exitWASM(new Error(data.error));
else loadWASM(data.module).catch(exitWASM);
return;
}
if (data.type === "font") {
fontRequests.get(data.request)?.(data);
fontRequests.delete(data.request);
@@ -84,12 +90,15 @@ async function handleBatchMessage({ id, name, args }) {
if (!format) throw new Error("不支持此格式");
root = destination;
const base = options.base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/[. ]+$/g, "") || "document";
const target = root ? await availableOutput(root, base, format) : { stem: base, name: format.paged ? base : `${base}.${format.extension}` };
if (root && format.paged) {
directory = await root.getDirectoryHandle(target.name, { create: true });
const target = root ? await navigator.locks.request("ofdgo-convert-output", { signal }, async () => {
signal.throwIfAborted();
const target = await availableOutput(root, base, format);
if (format.paged) directory = await root.getDirectoryHandle(target.name, { create: true });
else { await root.getFileHandle(target.name, { create: true }); directory = root; }
created = target.name;
self.postMessage({ id, type: "output", name: created });
} else directory = root;
return target;
}) : { stem: base, name: format.paged ? base : `${base}.${format.extension}` };
const boundary = async (action, pageName) => {
signal.throwIfAborted();
if (action === "open") {
@@ -144,7 +153,7 @@ async function handleBatchMessage({ id, name, args }) {
}
result.data.size = size;
operations.delete(id);
self.postMessage({ id, ...result });
self.postMessage({ id, ...result, memory: engineMemory.buffer.byteLength });
} catch (err) {
await output?.abort().catch(() => {});
let message = signal.aborted ? "转换已取消" : err.message;
@@ -152,7 +161,7 @@ async function handleBatchMessage({ id, name, args }) {
try { await root.removeEntry(created, { recursive: true }); }
catch (cleanupError) { message += `;清理失败:${cleanupError.message}`; }
}
self.postMessage({ id, ok: false, error: message, code: err.code, canceled: signal.aborted });
self.postMessage({ id, ok: false, error: message, code: err.code, canceled: signal.aborted, memory: engineMemory.buffer.byteLength });
} finally {
credentials?.password?.fill(0);
credentials?.key?.fill(0);
@@ -301,12 +310,10 @@ function exitWASM(err) {
self.close();
}
async function loadWASM() {
async function loadWASM(module) {
const go = new Go();
self.postMessage({ type: "progress", phase: "download", text: "正在下载引擎", percent: 16 });
const response = await fetch("./ofdgo.wasm");
self.postMessage({ type: "progress", phase: "compile", text: "正在编译引擎", percent: 35 });
const { instance } = await WebAssembly.instantiateStreaming(response, go.importObject);
const instance = await WebAssembly.instantiate(module, go.importObject);
engineMemory = instance.exports.mem;
self.postMessage({ type: "progress", phase: "start", text: "正在启动引擎", percent: 58 });
go.run(instance).then(exitWASM, exitWASM);
if (!go.exited) {
@@ -316,4 +323,4 @@ async function loadWASM() {
}
}
loadWASM().catch(exitWASM);
self.postMessage({ type: "module" });