UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

329 lines (256 loc) • 8.14 kB
import { assert } from "../../assert.js"; import { array_remove_first } from "../../collection/array/array_remove_first.js"; /** * * @param {Worker} worker * @param {*} message * @return {boolean} true if sending was successful, false otherwise */ function trySendMessage(worker, message) { try { worker.postMessage(message); return true; } catch (e) { //request failed console.error("failed to send message: ", message, e); return false; } } function needsSerialization(value) { if (value === null) { return false; } if (typeof value !== "object") { return false; } if (value.hasOwnProperty('toJSON') && value.toJSON === "function") { return true; } return false; } class WorkerProxy { /** * * @type {Object<Array<{id:number, parameters:[], resolve: function, reject:function}>>} * @private */ __pending = {}; __isRunning = false; /** * * @type {Worker|null} * @private */ __worker = null; /** * * @type {number} * @private */ __id_counter = 0; /** * Created worker will assume this name as well * Useful for debug purposes and reflection * @type {string} * @private */ __name = "worker" /** * * @param {string} url * @param {Object} methods */ constructor(url, methods) { this.url = url; this.methods = methods; this.__generateAPI(); } /** * Invoke a given method on the worker, as defined by the `WorkerBuilder` * @template T * @param {string} name Method's name * @param {Array} args * @return {Promise<T>} eventual result of the invoked method */ $submitRequest(name, args) { assert.isString(name, "name"); const pending = this.__pending[name]; const argumentCount = args.length; const parameters = new Array(argumentCount); for (let i = 0; i < argumentCount; i++) { const argument = args[i]; if (needsSerialization(argument)) { //use toJSON method on an argument if possible parameters[i] = argument.toJSON(); } else { parameters[i] = argument; } } const request_id = this.__id_counter++; return new Promise((resolve, reject) => { const request = { parameters: parameters, id: request_id, resolve: resolve, reject: reject }; pending.push(request); if (this.isRunning()) { const message = { methodName: name, id: request.id, parameters: parameters }; if (!trySendMessage(this.__worker, message)) { // failed to send the message // drop the pending request array_remove_first(pending, request); } } }); } /** * * @param {string} name * @private */ __makeMethod(name) { assert.isString(name, 'name'); if (this.__pending.hasOwnProperty(name)) { throw new Error(`Method '${name}' already defined`); } // initialize the pending request queue for this method this.__pending[name] = []; const proxy = this; this[name] = function (...args) { return proxy.$submitRequest(name, args); }; } /** * * @private */ __generateAPI() { for (let methodName in this.methods) { if (this.methods.hasOwnProperty(methodName)) { this.__makeMethod(methodName); } } } /** * * @param {Event} event * @private */ __handleMessage = (event) => { const pending = this.__pending; const data = event.data; const requestId = data.id; const methodName = data.methodName; //find pending request queue for method const requestQueue = pending[methodName]; if (requestQueue === undefined) { throw new Error('Unexpected method \'' + methodName + '\''); } else { const n = requestQueue.length; for (let i = 0; i < n; i++) { const request = requestQueue[i]; if (request.id === requestId) { //found the right one requestQueue.splice(i, 1); if (data.hasOwnProperty('error')) { request.reject(data.error); } else { request.resolve(data.result); } return; } } throw new Error(`Request ${requestId} not found in the request queue`); } } isRunning() { return this.__isRunning; } /** * Stop the worker. * If the worker is not running, this method does nothing. */ stop() { if (!this.__isRunning) { //not running return; } this.__worker.terminate(); this.__isRunning = false; } /** * * @param {number} id * @param {string} method_name * @returns {boolean} */ cancelRequest(id, method_name) { assert.isString(method_name, 'method_name'); //find request const requestQueue = this.__pending[method_name]; if (requestQueue === undefined) { throw new Error(`No request queue for method name '${method_name}'`); } const n = requestQueue.length; for (let i = 0; i < n; i++) { const request = requestQueue[i]; if (request.id === id) { if (!this.__isRunning) { // not running, simply cut from the queue requestQueue.splice(i, 1); return true; } else { // worker is running, send a termination request for this ID throw new Error('Ability to cancel pending requests while worker is running is not implemented'); } } } } sendPendingRequests() { for (let methodName in this.__pending) { if (this.__pending.hasOwnProperty(methodName)) { const pending = this.__pending[methodName]; const n = pending.length; for (let i = 0; i < n; i++) { const request = pending[i]; const message = { methodName: methodName, id: request.id, parameters: request.parameters }; trySendMessage(this.__worker, message); } } } } /** * Start the worker. * Any requests made while the worker is not running will be queued and sent once the worker is started. * * If the worker is already running, this method does nothing. * */ start() { if (this.__isRunning) { //already running return; } this.__worker = new Worker(this.url,{ name: this.__name }); this.__worker.onmessage = this.__handleMessage; // TODO attach proper error handler this.__worker.onerror = (errorEvent) => { console.error('Worker error:', errorEvent); }; this.__isRunning = true; this.sendPendingRequests(); } } export default WorkerProxy;