Initial
This commit is contained in:
140
resources/app/public/scripts/worker.js
Normal file
140
resources/app/public/scripts/worker.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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}];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user