UNPKG

queen

Version:

A platform for running scripts on many browsers

314 lines (254 loc) 8.61 kB
var EventEmitter = require('events').EventEmitter, useragent = require('useragent'), generateId = require('node-uuid').v4, its = require('its'); var createWorker = require('./worker.js'), utils = require('./utils.js'), MESSAGE_TYPE = require('../protocol.js').WORKER_PROVIDER_MESSAGE_TYPE; var create = module.exports = function(socket, options){ var workerProvider = new BrowserWorkerProvider(socket); options = options || {}; if(options.log) workerProvider.log = options.log; if(options.debug) workerProvider.debug = options.debug; if(options.spawnWorkerTimeout) workerProvider.spawnWorkerTimeout = options.spawnWorkerTimeout; if(options.heartbeatInterval) workerProvider.heartbeatInterval = options.heartbeatInterval; return workerProvider.api; }; var BrowserWorkerProvider = function(socket){ its.object(socket, 'BrowserWorkerProvider requires a socket.'); this.socket = socket; this.id = generateId(); this.emitter = new EventEmitter(); this.workerEmitters = {}; this.pendingWorkers = {}; this.name = 'A Browser Provider'; this.available = true; this.isUnresponsive = false; this.heartbeatMissHandler = this.heartbeatMissHandler.bind(this); this.kill = utils.once(this.kill.bind(this)); socket.on('disconnect', this.kill.bind(this)); socket.on('message', this.messageHandler.bind(this)); Object.defineProperty(this, 'api', { value: Object.freeze(getApi(this)), enumerable: true }); this.startHeartbeats(); }; var getApi = function(provider){ var api = provider.getWorker.bind(provider); api.on = provider.emitter.on.bind(provider.emitter); api.removeListener = provider.emitter.removeListener.bind(provider.emitter); api.id = provider.id; api.toString = provider.toString.bind(provider); api.kill = provider.kill; api.killWorkers = provider.killWorkers.bind(provider); api.toMap = provider.toMap.bind(provider); Object.defineProperty(api, 'attributes', { get: function(){ return provider.attributes; }, enumerable: true }); api.toString = provider.toString.bind(provider); return api; }; BrowserWorkerProvider.prototype.log = utils.noop; BrowserWorkerProvider.prototype.debug = utils.noop; BrowserWorkerProvider.prototype.spawnWorkerTimeout = 1000; BrowserWorkerProvider.prototype.heartbeatInterval = 20 * 1000; // 20 seconds is a long time to block the ui thread. BrowserWorkerProvider.prototype.toString = function(){ return this.attributes && this.attributes.name || 'Browser Worker Provider'; }; BrowserWorkerProvider.prototype.toMap = function(){ var map = { id: this.id, attributes: this.attributes, workerCount: Object.keys(this.workerEmitters).length, isAvailable: this.available === true, isResponsive: this.isUnresponsive === false }; return map; }; BrowserWorkerProvider.prototype.sendToSocket = function(message){ message = JSON.stringify(message); this.socket.send(message); }; BrowserWorkerProvider.prototype.startHeartbeats = function(){ if(this.heartbeatInterval){ // We request divide heartbeat interval by two to allow for some fuzziness on when // heartbeats arrive. Otherwise even if the browser sent the heartbeat in exact timing // the transport timing would result in an unresponsive state. A.k.a. Nyquist frequency (thanks, Dr. Stiber). this.sendToSocket([MESSAGE_TYPE['start heartbeats'], (this.heartbeatInterval / 2)]); } this.heartbeatTimer = setTimeout(this.heartbeatMissHandler, this.heartbeatInterval); }; BrowserWorkerProvider.prototype.heartbeatMissHandler = function(){ this.emitter.emit('unresponsive'); this.isUnresponsive = true; }; BrowserWorkerProvider.prototype.recieveHeartbeat = function(){ if(this.isUnresponsive){ this.isUnresponsive = false; this.emitter.emit('responsive'); } clearTimeout(this.heartbeatTimer); this.heartbeatTimer = setTimeout(this.heartbeatMissHandler, this.heartbeatInterval); }; BrowserWorkerProvider.prototype.messageHandler = function(message){ message = JSON.parse(message); switch(message[0]){ case MESSAGE_TYPE['heartbeat']: this.recieveHeartbeat(); return; case MESSAGE_TYPE['worker message']: this.workerMessageHandler(message[1], message[2]); return; case MESSAGE_TYPE['worker spawned']: this.spawnedWorkerHandler(message[1]); return; case MESSAGE_TYPE['worker dead']: this.workerDeadHandler(message[1], message[2]); return; case MESSAGE_TYPE['register']: this.registerHandler(message[1]); return; case MESSAGE_TYPE['available']: this.availableHandler(); return; case MESSAGE_TYPE['unavailable']: this.unavailableHandler(); return; } }; BrowserWorkerProvider.prototype.availableHandler = function(){ this.available = true; this.emitter.emit('available'); }; BrowserWorkerProvider.prototype.unavailableHandler = function(){ this.available = false; this.emitter.emit('unavailable'); }; BrowserWorkerProvider.prototype.createWorker = function(workerId){ var self = this, workerEmitter = new EventEmitter(), onSendToSocket = function(message){ self.sendToSocket([ MESSAGE_TYPE['worker message'], workerId, message ]); }, worker = createWorker(workerId, this.api, workerEmitter, onSendToSocket); this.workerEmitters[workerId] = workerEmitter; // Handle the case when a kill signal is sent from the server side worker.on('dead', function(){ var workerEmitter = self.workerEmitters[workerId]; if(workerEmitter !== void 0){ self.sendToSocket([ MESSAGE_TYPE['kill worker'], workerId ]); self.removeWorker(workerId); } }); this.emitter.emit('worker', worker); return worker; }; BrowserWorkerProvider.prototype.spawnedWorkerHandler = function(workerId){ var callback = this.pendingWorkers[workerId], worker; if(callback !== void 0){ delete this.pendingWorkers[workerId]; worker = this.createWorker(workerId); callback(worker); } else { // We weren't expecting this worker, send kill signal this.sendToSocket([ MESSAGE_TYPE['kill worker'], workerId ]); } }; BrowserWorkerProvider.prototype.workerDeadHandler = function(workerId, reason){ var workerEmitter = this.workerEmitters[workerId]; if(workerEmitter === void 0) return; workerEmitter.emit('dead', reason); }; BrowserWorkerProvider.prototype.registerHandler = function(attributes){ var ua; attributes = attributes || {}; if(attributes.userAgent){ ua = useragent.parse(attributes.userAgent); attributes.name = ua.toAgent() + ' (' + ua.os + ')'; attributes.family = ua.family; attributes.os = ua.os; attributes.version = { major: ua.major, minor: ua.minor, patch: ua.patch }; } Object.freeze(attributes); this.attributes = attributes; this.emitter.emit('register', attributes); }; BrowserWorkerProvider.prototype.kill = function(){ this.killWorkers('host dead'); this.emitter.emit('dead'); this.emitter.removeAllListeners(); }; BrowserWorkerProvider.prototype.killWorkers = function(reason){ var self = this; utils.each(this.workerEmitters, function(workerEmitter, workerId){ // Emulate the immediate death of the socket workerEmitter.emit('dead', reason); self.sendToSocket([ MESSAGE_TYPE['kill worker'], workerId ]); }); // Cancel all pending worker spawn requests utils.each(this.pendingWorkers, function(callback, workerId){ self.sendToSocket([ MESSAGE_TYPE['kill worker'], workerId ]); callback(void 0); }); this.workerEmitters = {}; }; BrowserWorkerProvider.prototype.workerMessageHandler = function(workerId, workerMessage){ var workerEmitter = this.workerEmitters[workerId]; if(workerEmitter === void 0) return; workerEmitter.emit('message', workerMessage); }; BrowserWorkerProvider.prototype.getWorker = function(workerConfig, callback){ if(workerConfig === void 0) return; callback = callback || utils.noop; if(!this.available){ callback(void 0); return; } var self = this, workerId = generateId(); this.pendingWorkers[workerId] = callback; this.sendToSocket([ MESSAGE_TYPE['spawn worker'], workerId, workerConfig ]); // If the browser doesn't respond fast enough, emulate the killing of the worker setTimeout(function(){ if(self.pendingWorkers[workerId] !== void 0){ callback(void 0); self.debug('Spawn worker timed out'); delete self.pendingWorkers[workerId]; self.sendToSocket([ MESSAGE_TYPE['kill worker'], workerId ]); } }, this.spawnWorkerTimeout); }; BrowserWorkerProvider.prototype.removeWorker = function(workerId){ if(this.workerEmitters[workerId] === void 0) return; delete this.workerEmitters[workerId]; this.emitter.emit('workerDead', workerId); };