@loaders.gl/worker-utils
Version:
Utilities for running tasks on worker threads
137 lines (136 loc) • 4.95 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { NodeWorker } from "../node/worker_threads.js";
import { isBrowser } from "../env-utils/globals.js";
import { assert } from "../env-utils/assert.js";
import { getLoadableWorkerURL } from "../worker-utils/get-loadable-worker-url.js";
import { getTransferList } from "../worker-utils/get-transfer-list.js";
const NOOP = () => { };
/**
* Represents one worker thread
*/
export default class WorkerThread {
name;
source;
url;
terminated = false;
worker;
onMessage;
onError;
_loadableURL = '';
/** Checks if workers are supported on this platform */
static isSupported() {
return ((typeof Worker !== 'undefined' && isBrowser) ||
(typeof NodeWorker !== 'undefined' && !isBrowser));
}
constructor(props) {
const { name, source, url } = props;
assert(source || url); // Either source or url must be defined
this.name = name;
this.source = source;
this.url = url;
this.onMessage = NOOP;
this.onError = (error) => console.log(error); // eslint-disable-line
this.worker = isBrowser ? this._createBrowserWorker() : this._createNodeWorker();
}
/**
* Terminate this worker thread
* @note Can free up significant memory
*/
destroy() {
this.onMessage = NOOP;
this.onError = NOOP;
this.worker.terminate(); // eslint-disable-line @typescript-eslint/no-floating-promises
this.terminated = true;
}
get isRunning() {
return Boolean(this.onMessage);
}
/**
* Send a message to this worker thread
* @param data any data structure, ideally consisting mostly of transferrable objects
* @param transferList If not supplied, calculated automatically by traversing data
*/
postMessage(data, transferList) {
transferList = transferList || getTransferList(data);
// @ts-ignore
this.worker.postMessage(data, transferList);
}
// PRIVATE
/**
* Generate a standard Error from an ErrorEvent
* @param event
*/
_getErrorFromErrorEvent(event) {
// Note Error object does not have the expected fields if loading failed completely
// https://developer.mozilla.org/en-US/docs/Web/API/Worker#Event_handlers
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
let message = 'Failed to load ';
message += `worker ${this.name} from ${this.url}. `;
if (event.message) {
message += `${event.message} in `;
}
// const hasFilename = event.filename && !event.filename.startsWith('blob:');
// message += hasFilename ? event.filename : this.source.slice(0, 100);
if (event.lineno) {
message += `:${event.lineno}:${event.colno}`;
}
return new Error(message);
}
/**
* Creates a worker thread on the browser
*/
_createBrowserWorker() {
this._loadableURL = getLoadableWorkerURL({ source: this.source, url: this.url });
const worker = new Worker(this._loadableURL, { name: this.name });
worker.onmessage = (event) => {
if (!event.data) {
this.onError(new Error('No data received'));
}
else {
this.onMessage(event.data);
}
};
// This callback represents an uncaught exception in the worker thread
worker.onerror = (error) => {
this.onError(this._getErrorFromErrorEvent(error));
this.terminated = true;
};
// TODO - not clear when this would be called, for now just log in case it happens
worker.onmessageerror = (event) => console.error(event); // eslint-disable-line
return worker;
}
/**
* Creates a worker thread in node.js
* @todo https://nodejs.org/api/async_hooks.html#async-resource-worker-pool
*/
_createNodeWorker() {
let worker;
if (this.url) {
// Make sure relative URLs start with './'
const absolute = this.url.includes(':/') || this.url.startsWith('/');
const url = absolute ? this.url : `./${this.url}`;
// console.log('Starting work from', url);
worker = new NodeWorker(url, { eval: false });
}
else if (this.source) {
worker = new NodeWorker(this.source, { eval: true });
}
else {
throw new Error('no worker');
}
worker.on('message', (data) => {
// console.error('message', data);
this.onMessage(data);
});
worker.on('error', (error) => {
// console.error('error', error);
this.onError(error);
});
worker.on('exit', (code) => {
// console.error('exit', code);
});
return worker;
}
}