@loaders.gl/worker-utils
Version:
Utilities for running tasks on worker threads
159 lines (141 loc) • 5.01 kB
text/typescript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {NodeWorker, NodeWorkerType} from '../node/worker_threads';
import {isBrowser} from '../env-utils/globals';
import {assert} from '../env-utils/assert';
import {getLoadableWorkerURL} from '../worker-utils/get-loadable-worker-url';
import {getTransferList} from '../worker-utils/get-transfer-list';
const NOOP = () => {};
export type WorkerThreadProps = {
name: string;
source?: string;
url?: string;
};
/**
* Represents one worker thread
*/
export default class WorkerThread {
readonly name: string;
readonly source: string | undefined;
readonly url: string | undefined;
terminated: boolean = false;
worker: Worker | NodeWorkerType;
onMessage: (message: any) => void;
onError: (error: Error) => void;
private _loadableURL: string = '';
/** Checks if workers are supported on this platform */
static isSupported(): boolean {
return (
(typeof Worker !== 'undefined' && isBrowser) ||
(typeof NodeWorker !== 'undefined' && !isBrowser)
);
}
constructor(props: WorkerThreadProps) {
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(): void {
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: any, transferList?: any[]): void {
transferList = transferList || getTransferList(data);
// @ts-ignore
this.worker.postMessage(data, transferList);
}
// PRIVATE
/**
* Generate a standard Error from an ErrorEvent
* @param event
*/
_getErrorFromErrorEvent(event: ErrorEvent): Error {
// 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(): Worker {
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: ErrorEvent): void => {
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(): NodeWorkerType {
let worker: NodeWorkerType;
if (this.url) {
// Make sure relative URLs start with './'
const absolute = this.url.includes(':/') || this.url.startsWith('/');
const url = absolute ? this.url : `./${this.url}`;
const type = this.url.endsWith('.ts') || this.url.endsWith('.mjs') ? 'module' : 'commonjs';
// console.log('Starting work from', url);
// @ts-expect-error type is not known
worker = new NodeWorker(url, {eval: false, type});
} 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) => {
this.onError(error as Error);
});
worker.on('exit', (code) => {
// console.error('exit', code);
});
return worker;
}
}