141 lines
5.0 KiB
JavaScript
141 lines
5.0 KiB
JavaScript
|
|
/**
|
||
|
|
* Supported worker task actions
|
||
|
|
* @enum {string}
|
||
|
|
*/
|
||
|
|
const WORKER_TASK_ACTIONS = {
|
||
|
|
INIT: "init",
|
||
|
|
LOAD: "load",
|
||
|
|
EXECUTE: "execute"
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The name of this Worker thread
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
let _workerName;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is this Worker operating in debug mode?
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
let _debug = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A registry of loaded functions
|
||
|
|
* @type {Map<string, Function>}
|
||
|
|
*/
|
||
|
|
const functions = new Map();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle messages provided from the main thread via worker#postMessage
|
||
|
|
* @param {MessageEvent} event The message provided from the main thread
|
||
|
|
*/
|
||
|
|
onmessage = async function(event) {
|
||
|
|
const task = event.data;
|
||
|
|
|
||
|
|
// Get the task result
|
||
|
|
let response;
|
||
|
|
let transfer;
|
||
|
|
switch ( task.action ) {
|
||
|
|
case WORKER_TASK_ACTIONS.INIT:
|
||
|
|
response = await _handleInitializeWorker(task);
|
||
|
|
break;
|
||
|
|
case WORKER_TASK_ACTIONS.LOAD:
|
||
|
|
response = await _handleLoadFunction(task);
|
||
|
|
break;
|
||
|
|
case WORKER_TASK_ACTIONS.EXECUTE:
|
||
|
|
[response, transfer] = await _handleExecuteFunction(task);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Respond with the result, and transfer objects back to the main thread
|
||
|
|
postMessage(response, transfer);
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle the initialization workflow for a new Worker
|
||
|
|
* @param {object} [options={}] Options which configure worker initialization
|
||
|
|
* @param {number} [options.taskId] The task ID being performed
|
||
|
|
* @param {string} [options.workerName] The worker name
|
||
|
|
* @param {boolean} [options.debug] Should the worker run in debug mode?
|
||
|
|
* @param {boolean} [options.loadPrimitives] Should we automatically load primitives from module.mjs?
|
||
|
|
* @param {string[]} [options.scripts] An array of scripts to import.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
async function _handleInitializeWorker({taskId, workerName, debug, loadPrimitives, scripts}={}) {
|
||
|
|
_workerName = workerName;
|
||
|
|
_debug = debug;
|
||
|
|
if ( loadPrimitives ) await _loadLibrary("/common/utils/primitives/module.mjs");
|
||
|
|
if ( scripts ) importScripts(...scripts);
|
||
|
|
console.log(`Worker ${_workerName} | Initialized Worker`);
|
||
|
|
return {taskId};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Currently Chrome and Safari support web worker modules which can use ES Module imports directly.
|
||
|
|
* Firefox lags behind and this feature is not yet implemented: https://bugzilla.mozilla.org/show_bug.cgi?id=1247687
|
||
|
|
* FIXME: Once Firefox supports module workers, we can import commons libraries into workers directly.
|
||
|
|
* Until then, this is a hacky workaround to parse the source script into the global namespace of the worker thread.
|
||
|
|
* @param {string} path The commons ES Module to load
|
||
|
|
* @returns {Promise<void>} A Promise that resolves once the module has been "loaded"
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
async function _loadLibrary(path) {
|
||
|
|
let source = await fetch(path).then(r => r.text());
|
||
|
|
eval(source);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle a request from the main thread to load a function into Worker memory.
|
||
|
|
* @param {object} [options={}]
|
||
|
|
* @param {number} [options.taskId] The task ID being performed
|
||
|
|
* @param {string} [options.functionName] The name that the function should assume in the Worker global scope
|
||
|
|
* @param {string} [options.functionBody] The content of the function to be parsed.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
async function _handleLoadFunction({taskId, functionName, functionBody}={}) {
|
||
|
|
// Strip existing function name and parse it
|
||
|
|
functionBody = functionBody.replace(/^function [A-z0-9\s]+\(/, "function(");
|
||
|
|
let fn = eval(`${functionName} = ${functionBody}`);
|
||
|
|
if ( !fn ) throw new Error(`Failed to load function ${functionName}`);
|
||
|
|
|
||
|
|
// Record the function to the global scope
|
||
|
|
functions.set(functionName, fn);
|
||
|
|
globalThis.functionName = fn;
|
||
|
|
if ( _debug ) console.debug(`Worker ${_workerName} | Loaded function ${functionName}`);
|
||
|
|
return {taskId};
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle a request from the main thread to execute a function with provided parameters.
|
||
|
|
* @param {object} [options={}]
|
||
|
|
* @param {number} [options.taskId] The task ID being performed
|
||
|
|
* @param {string} [options.functionName] The name that the function should assume in the Worker global scope
|
||
|
|
* @param {Array<*>} [options.args] An array of arguments passed to the function
|
||
|
|
* @returns {[message: object, transfer?: object[]]}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
async function _handleExecuteFunction({taskId, functionName, args}) {
|
||
|
|
// Checking that function exists
|
||
|
|
const fn = this[functionName] || functions.get(functionName);
|
||
|
|
if ( !fn ) throw new Error(`Function ${functionName} does not exist into Worker ${_workerName}`);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const [result, transfer] = await fn(...args);
|
||
|
|
if ( _debug ) console.debug(`Worker ${_workerName} | Executed function ${functionName}`);
|
||
|
|
return [{taskId, result}, transfer];
|
||
|
|
} catch(error) {
|
||
|
|
if ( _debug ) console.debug(`Worker ${_workerName} | Failed to execute function ${functionName}`);
|
||
|
|
console.error(error);
|
||
|
|
return [{taskId, error}];
|
||
|
|
}
|
||
|
|
}
|