159 lines
3.7 KiB
JavaScript
159 lines
3.7 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
|
|
|
"use strict";
|
|
|
|
class WorkerDispatcher {
|
|
#msgId = 1;
|
|
#worker = null;
|
|
// Map of message ids -> promise resolution functions, for dispatching worker responses
|
|
#pendingCalls = new Map();
|
|
#url = "";
|
|
|
|
constructor(url) {
|
|
this.#url = url;
|
|
}
|
|
|
|
start() {
|
|
// When running in debugger jest test, we don't have access to ChromeWorker
|
|
if (typeof ChromeWorker == "function") {
|
|
this.#worker = new ChromeWorker(this.#url);
|
|
} else {
|
|
this.#worker = new Worker(this.#url);
|
|
}
|
|
this.#worker.onerror = err => {
|
|
console.error(`Error in worker ${this.#url}`, err.message);
|
|
};
|
|
this.#worker.addEventListener("message", this.#onMessage);
|
|
}
|
|
|
|
stop() {
|
|
if (!this.#worker) {
|
|
return;
|
|
}
|
|
|
|
this.#worker.removeEventListener("message", this.#onMessage);
|
|
this.#worker.terminate();
|
|
this.#worker = null;
|
|
this.#pendingCalls.clear();
|
|
}
|
|
|
|
task(method, { queue = false } = {}) {
|
|
const calls = [];
|
|
const push = args => {
|
|
return new Promise((resolve, reject) => {
|
|
if (queue && calls.length === 0) {
|
|
Promise.resolve().then(flush);
|
|
}
|
|
|
|
calls.push({ args, resolve, reject });
|
|
|
|
if (!queue) {
|
|
flush();
|
|
}
|
|
});
|
|
};
|
|
|
|
const flush = () => {
|
|
const items = calls.slice();
|
|
calls.length = 0;
|
|
|
|
if (!this.#worker) {
|
|
this.start();
|
|
}
|
|
|
|
const id = this.#msgId++;
|
|
this.#worker.postMessage({
|
|
id,
|
|
method,
|
|
calls: items.map(item => item.args),
|
|
});
|
|
|
|
this.#pendingCalls.set(id, items);
|
|
};
|
|
|
|
return (...args) => push(args);
|
|
}
|
|
|
|
invoke(method, ...args) {
|
|
return this.task(method)(...args);
|
|
}
|
|
|
|
#onMessage = ({ data: result }) => {
|
|
const items = this.#pendingCalls.get(result.id);
|
|
this.#pendingCalls.delete(result.id);
|
|
if (!items) {
|
|
return;
|
|
}
|
|
|
|
if (!this.#worker) {
|
|
return;
|
|
}
|
|
|
|
result.results.forEach((resultData, i) => {
|
|
const { resolve, reject } = items[i];
|
|
|
|
if (resultData.error) {
|
|
const err = new Error(resultData.message);
|
|
err.metadata = resultData.metadata;
|
|
reject(err);
|
|
} else {
|
|
resolve(resultData.response);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
function workerHandler(publicInterface) {
|
|
return function (msg) {
|
|
const { id, method, calls } = msg.data;
|
|
|
|
Promise.all(
|
|
calls.map(args => {
|
|
try {
|
|
const response = publicInterface[method].apply(undefined, args);
|
|
if (response instanceof Promise) {
|
|
return response.then(
|
|
val => ({ response: val }),
|
|
err => asErrorMessage(err)
|
|
);
|
|
}
|
|
return { response };
|
|
} catch (error) {
|
|
return asErrorMessage(error);
|
|
}
|
|
})
|
|
).then(results => {
|
|
globalThis.postMessage({ id, results });
|
|
});
|
|
};
|
|
}
|
|
|
|
function asErrorMessage(error) {
|
|
if (typeof error === "object" && error && "message" in error) {
|
|
// Error can't be sent via postMessage, so be sure to convert to
|
|
// string.
|
|
return {
|
|
error: true,
|
|
message:
|
|
error.message +
|
|
(error.stack ? "\nStack in the worker:" + error.stack : ""),
|
|
metadata: error.metadata,
|
|
};
|
|
}
|
|
|
|
return {
|
|
error: true,
|
|
message: error == null ? error : error.toString(),
|
|
metadata: undefined,
|
|
};
|
|
}
|
|
|
|
// Might be loaded within a worker thread where `module` isn't available.
|
|
if (typeof module !== "undefined") {
|
|
module.exports = {
|
|
WorkerDispatcher,
|
|
workerHandler,
|
|
};
|
|
}
|