@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
329 lines (256 loc) • 8.14 kB
JavaScript
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;