Files
Foundry-VTT-Docker/resources/app/client/core/workers.js
2025-01-04 00:34:03 +01:00

212 lines
7.1 KiB
JavaScript

/**
* @typedef {Record<string, any>} WorkerTask
* @property {number} [taskId] An incrementing task ID used to reference task progress
* @property {WorkerManager.WORKER_TASK_ACTIONS} action The task action being performed, from WorkerManager.WORKER_TASK_ACTIONS
*/
/**
* An asynchronous web Worker which can load user-defined functions and await execution using Promises.
* @param {string} name The worker name to be initialized
* @param {object} [options={}] Worker initialization options
* @param {boolean} [options.debug=false] Should the worker run in debug mode?
* @param {boolean} [options.loadPrimitives=false] Should the worker automatically load the primitives library?
* @param {string[]} [options.scripts] Should the worker operates in script modes? Optional scripts.
*/
class AsyncWorker extends Worker {
constructor(name, {debug=false, loadPrimitives=false, scripts}={}) {
super(AsyncWorker.WORKER_HARNESS_JS);
this.name = name;
this.addEventListener("message", this.#onMessage.bind(this));
this.addEventListener("error", this.#onError.bind(this));
this.#ready = this.#dispatchTask({
action: WorkerManager.WORKER_TASK_ACTIONS.INIT,
workerName: name,
debug,
loadPrimitives,
scripts
});
}
/**
* A path reference to the JavaScript file which provides companion worker-side functionality.
* @type {string}
*/
static WORKER_HARNESS_JS = "scripts/worker.js";
/**
* A queue of active tasks that this Worker is executing.
* @type {Map<number, {resolve: (result: any) => void, reject: (error: Error) => void}>}
*/
#tasks = new Map();
/**
* An auto-incrementing task index.
* @type {number}
*/
#taskIndex = 0;
/**
* A Promise which resolves once the Worker is ready to accept tasks
* @type {Promise}
*/
get ready() {
return this.#ready;
}
#ready;
/* -------------------------------------------- */
/* Task Management */
/* -------------------------------------------- */
/**
* Load a function onto a given Worker.
* The function must be a pure function with no external dependencies or requirements on global scope.
* @param {string} functionName The name of the function to load
* @param {Function} functionRef A reference to the function that should be loaded
* @returns {Promise<unknown>} A Promise which resolves once the Worker has loaded the function.
*/
async loadFunction(functionName, functionRef) {
return this.#dispatchTask({
action: WorkerManager.WORKER_TASK_ACTIONS.LOAD,
functionName,
functionBody: functionRef.toString()
});
}
/* -------------------------------------------- */
/**
* Execute a task on a specific Worker.
* @param {string} functionName The named function to execute on the worker. This function must first have been
* loaded.
* @param {Array<*>} [args] An array of parameters with which to call the requested function
* @param {Array<*>} [transfer] An array of transferable objects which are transferred to the worker thread.
* See https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
* @returns {Promise<unknown>} A Promise which resolves with the returned result of the function once complete.
*/
async executeFunction(functionName, args=[], transfer=[]) {
const action = WorkerManager.WORKER_TASK_ACTIONS.EXECUTE;
return this.#dispatchTask({action, functionName, args}, transfer);
}
/* -------------------------------------------- */
/**
* Dispatch a task to a named Worker, awaiting confirmation of the result.
* @param {WorkerTask} taskData Data to dispatch to the Worker as part of the task.
* @param {Array<*>} transfer An array of transferable objects which are transferred to the worker thread.
* @returns {Promise} A Promise which wraps the task transaction.
*/
async #dispatchTask(taskData={}, transfer=[]) {
const taskId = taskData.taskId = this.#taskIndex++;
return new Promise((resolve, reject) => {
this.#tasks.set(taskId, {resolve, reject});
this.postMessage(taskData, transfer);
});
}
/* -------------------------------------------- */
/**
* Handle messages emitted by the Worker thread.
* @param {MessageEvent} event The dispatched message event
*/
#onMessage(event) {
const response = event.data;
const task = this.#tasks.get(response.taskId);
if ( !task ) return;
this.#tasks.delete(response.taskId);
if ( response.error ) return task.reject(response.error);
return task.resolve(response.result);
}
/* -------------------------------------------- */
/**
* Handle errors emitted by the Worker thread.
* @param {ErrorEvent} error The dispatched error event
*/
#onError(error) {
error.message = `An error occurred in Worker ${this.name}: ${error.message}`;
console.error(error);
}
}
/* -------------------------------------------- */
/**
* A client-side class responsible for managing a set of web workers.
* This interface is accessed as a singleton instance via game.workers.
* @see Game#workers
*/
class WorkerManager extends Map {
constructor() {
if ( game.workers instanceof WorkerManager ) {
throw new Error("The singleton WorkerManager instance has already been constructed as Game#workers");
}
super();
}
/**
* Supported worker task actions
* @enum {string}
*/
static WORKER_TASK_ACTIONS = Object.freeze({
INIT: "init",
LOAD: "load",
EXECUTE: "execute"
});
/* -------------------------------------------- */
/* Worker Management */
/* -------------------------------------------- */
/**
* Create a new named Worker.
* @param {string} name The named Worker to create
* @param {object} [config={}] Worker configuration parameters passed to the AsyncWorker constructor
* @returns {Promise<AsyncWorker>} The created AsyncWorker which is ready to accept tasks
*/
async createWorker(name, config={}) {
if (this.has(name)) {
throw new Error(`A Worker already exists with the name "${name}"`);
}
const worker = new AsyncWorker(name, config);
this.set(name, worker);
await worker.ready;
return worker;
}
/* -------------------------------------------- */
/**
* Retire a current Worker, terminating it immediately.
* @see Worker#terminate
* @param {string} name The named worker to terminate
*/
retireWorker(name) {
const worker = this.get(name);
if ( !worker ) return;
worker.terminate();
this.delete(name);
}
/* -------------------------------------------- */
/**
* @deprecated since 11
* @ignore
*/
getWorker(name) {
foundry.utils.logCompatibilityWarning("WorkerManager#getWorker is deprecated in favor of WorkerManager#get",
{since: 11, until: 13});
const w = this.get(name);
if ( !w ) throw new Error(`No worker with name ${name} currently exists!`);
return w;
}
}
/* -------------------------------------------- */