summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/worker-utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/worker-utils.js')
-rw-r--r--devtools/client/shared/worker-utils.js157
1 files changed, 157 insertions, 0 deletions
diff --git a/devtools/client/shared/worker-utils.js b/devtools/client/shared/worker-utils.js
new file mode 100644
index 0000000000..bb5c54dac3
--- /dev/null
+++ b/devtools/client/shared/worker-utils.js
@@ -0,0 +1,157 @@
+/* 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,
+ 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,
+ };
+}