UNPKG

tangram

Version:
515 lines (453 loc) 19.3 kB
/*jshint worker: true*/ // WorkerBroker routes messages between web workers and the main thread, allowing for simpler // async code via promises. Example usage: // // In web worker, register self as a callable "target", and define a method: // // WorkerBroker.addTarget('self', self); // // self.square = function (x) { // return x * x; // }; // // In main thread, invoke that method and receive the result (if any) as a promise. // // worker = new Worker(...); // WorkerBroker.addWorker(worker); // // WorkerBroker.postMessage(worker, 'self.square', 5).then(function(y) { // console.log(y); // }); // // -> prints 25 // // Async code: // // For synchronous code that must pass a return value to the main thread, the function can simply // return an immediate value (see example above). For cases where the worker method needs to run // asynchronous code, the function can return a promise, and the resolved or rejected value will // be sent back to the main thread when the promise is fulfilled. // // Error handling: // // If the worker method either throws an error, or returns a promise that is rejected, it will be // sent back to the main thread as a promise rejection. These two examples are equivalent: // // In worker, throwing an error: // // self.broken = function () { // throw new Error('error in worker!'); // }; // // In worker, returning a rejected promise: // // self.broken = function () { // return Promise.reject(new Error('error in worker!')); // }; // // In main thread, both errors are received as a promise rejection: // // WorkerBroker.postMessage(worker, 'self.broken').then( // // Promise resolved // function() { // console.log('success!'); // }, // // Promise rejected // function(error) { // console.log('error!', error); // }); // // -> prints 'error! error in worker' // // Calling from worker to main thread: // // The same style of calls can be made *from* a web worker, to the main thread. The API is the same // with the exception that the first argument, 'worker', is not needed for WorkerBroker.postMessage(), // since the main thread is the implicit target. // // In main thread, define a method and register it: // // var geometry = { // length: function(x, y) { // return Math.sqrt(x * x + y * y); // } // }; // // WorkerBroker.addTarget('geometry', geometry); // // In worker thread): // // WorkerBroker.postMessage('geometry.length', 3, 4).then(function(d) { // console.log(d); // }); // // -> prints 5 // import Thread from './thread'; import log from './log'; var WorkerBroker; export default WorkerBroker = {}; // Global list of all worker messages // Uniquely tracks every call made between main thread and a worker var message_id = 0; var messages = {}; // Register an object to receive calls from other thread WorkerBroker.targets = {}; WorkerBroker.addTarget = function (name, target) { WorkerBroker.targets[name] = target; }; WorkerBroker.removeTarget = function (name) { if (name) { delete WorkerBroker.targets[name]; } }; // Given a dot-notation-style method name, e.g. 'Object.object.method', // find the object to call the method on from the list of registered targets function findTarget (method) { var chain = []; if (typeof method === 'string') { chain = method.split('.'); method = chain.pop(); } var target = WorkerBroker.targets; for (let m=0; m < chain.length; m++) { if (target[chain[m]]) { target = target[chain[m]]; } else { return []; } } return [method, target]; } // Main thread: // - Send messages to workers, and optionally receive an async response as a promise // - Receive messages from workers, and optionally send an async response back as a promise function setupMainThread () { // Send a message to a worker, and optionally get an async response // Arguments: // - worker: one or more web worker instances to send the message to (single value or array) // - method: the method with this name, specified with dot-notation, will be invoked in the worker // - message: spread of arguments to call the method with // Returns: // - a promise that will be fulfilled if the worker method returns a value (could be immediately, or async) // WorkerBroker.postMessage = function (worker, method, ...message) { // If more than one worker specified, post to multiple if (Array.isArray(worker)) { return Promise.all( worker.map(w => WorkerBroker.postMessage(w, method, ...message)) ); } // Parse options let options = {}; if (typeof method === 'object') { options = method; method = method.method; } // Track state of this message var promise = new Promise((resolve, reject) => { messages[message_id] = { method, message, resolve, reject }; }); let payload, transferables = []; if (message && message.length === 1 && message[0] instanceof WorkerBroker.withTransferables) { transferables = message[0].transferables; message = message[0].value; } payload = { type: 'main_send', // mark message as method invocation from main thread message_id, // unique id for this message, for life of program method, // will dispatch to a function of this name within the worker message // message payload }; if (options.stringify) { payload = JSON.stringify(payload); } worker.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method}' transferred ${transferables.length} objects to worker thread`); } message_id++; return promise; }; // Add a worker to communicate with - each worker must be registered from the main thread WorkerBroker.addWorker = function (worker) { if (!(worker instanceof Worker)) { throw Error('Worker broker could not add non-Worker object', worker); } worker.addEventListener('message', function WorkerBrokerMainThreadHandler(event) { let data = ((typeof event.data === 'string') ? JSON.parse(event.data) : event.data); let id = data.message_id; // Listen for messages coming back from the worker, and fulfill that message's promise if (data.type === 'worker_reply') { // Pass the result to the promise if (messages[id]) { if (data.error) { messages[id].reject(data.error); } else { messages[id].resolve(data.message); } delete messages[id]; } } // Listen for messages initiating a call from the worker, dispatch them, // and send any return value back to the worker // Unique id for this message & return call to main thread else if (data.type === 'worker_send' && id != null) { // Call the requested method and save the return value let result, error, target, method_name, method; try { [method_name, target] = findTarget(data.method); if (!target) { throw Error(`Worker broker could not dispatch message type ${data.method} on target ${data.target} because no object with that name is registered on main thread`); } method = (typeof target[method_name] === 'function') && target[method_name]; if (!method) { throw Error(`Worker broker could not dispatch message type ${data.method} on target ${data.target} because object has no method with that name`); } result = method.apply(target, data.message); } catch(e) { // Thrown errors will be passed back (in string form) to worker error = e; } // Send return value to worker let payload, transferables = []; // Async result if (result instanceof Promise) { result.then((value) => { if (value instanceof WorkerBroker.withTransferables) { transferables = value.transferables; value = value.value[0]; } payload = { type: 'main_reply', message_id: id, message: value }; worker.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method_name}' transferred ${transferables.length} objects to worker thread`); } }, (error) => { worker.postMessage({ type: 'main_reply', message_id: id, error: (error instanceof Error ? `${error.message}: ${error.stack}` : error) }); }); } // Immediate result else { if (result instanceof WorkerBroker.withTransferables) { transferables = result.transferables; result = result.value[0]; } payload = { type: 'main_reply', message_id: id, message: result, error: (error instanceof Error ? `${error.message}: ${error.stack}` : error) }; worker.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method_name}' transferred ${transferables.length} objects to worker thread`); } } } }); }; // Expose for debugging WorkerBroker.getMessages = function () { return messages; }; WorkerBroker.getMessageId = function () { return message_id; }; } // Worker threads: // - Receive messages from main thread, and optionally send an async response back as a promise // - Send messages to main thread, and optionally receive an async response as a promise function setupWorkerThread () { // Send a message to the main thread, and optionally get an async response as a promise // Arguments: // - method: the method with this name, specified with dot-notation, will be invoked on the main thread // - message: array of arguments to call the method with // Returns: // - a promise that will be fulfilled if the main thread method returns a value (could be immediately, or async) // WorkerBroker.postMessage = function (method, ...message) { // Parse options let options = {}; if (typeof method === 'object') { options = method; method = method.method; } // Track state of this message var promise = new Promise((resolve, reject) => { messages[message_id] = { method, message, resolve, reject }; }); let payload, transferables = []; if (message && message.length === 1 && message[0] instanceof WorkerBroker.withTransferables) { transferables = message[0].transferables; message = message[0].value; } payload = { type: 'worker_send', // mark message as method invocation from worker message_id, // unique id for this message, for life of program method, // will dispatch to a method of this name on the main thread message // message payload }; if (options.stringify) { payload = JSON.stringify(payload); } self.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method}' transferred ${transferables.length} objects to main thread`); } message_id++; return promise; }; self.addEventListener('message', function WorkerBrokerWorkerThreadHandler(event) { let data = ((typeof event.data === 'string') ? JSON.parse(event.data) : event.data); let id = data.message_id; // Listen for messages coming back from the main thread, and fulfill that message's promise if (data.type === 'main_reply') { // Pass the result to the promise if (messages[id]) { if (data.error) { messages[id].reject(data.error); } else { messages[id].resolve(data.message); } delete messages[id]; } } // Receive messages from main thread, dispatch them, and send back a reply // Unique id for this message & return call to main thread else if (data.type === 'main_send' && id != null) { // Call the requested worker method and save the return value let result, error, target, method_name, method; try { [method_name, target] = findTarget(data.method); if (!target) { throw Error(`Worker broker could not dispatch message type ${data.method} on target ${data.target} because no object with that name is registered on main thread`); } method = (typeof target[method_name] === 'function') && target[method_name]; if (!method) { throw Error(`Worker broker could not dispatch message type ${data.method} because worker has no method with that name`); } result = method.apply(target, data.message); } catch(e) { // Thrown errors will be passed back (in string form) to main thread error = e; } // Send return value to main thread let payload, transferables = []; // Async result if (result instanceof Promise) { result.then((value) => { if (value instanceof WorkerBroker.withTransferables) { transferables = value.transferables; value = value.value[0]; } payload = { type: 'worker_reply', message_id: id, message: value }; self.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method_name}' transferred ${transferables.length} objects to main thread`); } }, (error) => { self.postMessage({ type: 'worker_reply', message_id: id, error: (error instanceof Error ? `${error.message}: ${error.stack}` : error) }); }); } // Immediate result else { if (result instanceof WorkerBroker.withTransferables) { transferables = result.transferables; result = result.value[0]; } payload = { type: 'worker_reply', message_id: id, message: result, error: (error instanceof Error ? `${error.message}: ${error.stack}` : error) }; self.postMessage(payload, transferables.map(t => t.object)); freeTransferables(transferables); if (transferables.length > 0) { log('trace', `'${method_name}' transferred ${transferables.length} objects to main thread`); } } } }); } // Special value wrapper, to indicate that we want to find and include transferable objects in the message WorkerBroker.withTransferables = function (...value) { if (!(this instanceof WorkerBroker.withTransferables)) { return new WorkerBroker.withTransferables(...value); } this.value = value; this.transferables = findTransferables(this.value); }; // Build a list of transferable objects from a source object // Returns a list of info about each transferable: // - object: the actual transferable object // - parent: the parent object that the transferable is a property of (if any) // - property: the property name of the transferable on the parent object (if any) // TODO: add option in case you DON'T want to transfer objects function findTransferables(source, parent = null, property = null, list = []) { if (!source) { return list; } if (Array.isArray(source)) { // Check each array element source.forEach((x, i) => findTransferables(x, source, i, list)); } else if (typeof source === 'object') { // Is the object a transferable array buffer? if (source instanceof ArrayBuffer) { list.push({ object: source, parent, property }); } // Or looks like a typed array (has an array buffer property)? else if (source.buffer instanceof ArrayBuffer) { list.push({ object: source.buffer, parent, property }); } // Otherwise check each property else { for (let prop in source) { findTransferables(source[prop], source, prop, list); } } } return list; } // Remove neutered transferables from parent objects, as they should no longer be accessed after transfer function freeTransferables(transferables) { if (!Array.isArray(transferables)) { return; } transferables.filter(t => t.parent && t.property).forEach(t => delete t.parent[t.property]); } // Setup this thread as appropriate if (Thread.is_main) { setupMainThread(); } if (Thread.is_worker) { setupWorkerThread(); }