diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index ed6626468..85bdec7ee 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -519,97 +519,159 @@ vAPI.messaging = { /******************************************************************************/ -// This allows to avoid creating a closure for every single message which -// expects an answer. Having a closure created each time a message is processed -// has been always bothering me. Another benefit of the implementation here -// is to reuse the callback proxy object, so less memory churning. -// -// https://developers.google.com/speed/articles/optimizing-javascript -// "Creating a closure is significantly slower then creating an inner -// function without a closure, and much slower than reusing a static -// function" -// -// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html -// "the dreaded 'uniformly slow code' case where every function takes 1% -// of CPU and you have to make one hundred separate performance optimizations -// to improve performance at all" -// -// http://jsperf.com/closure-no-closure/2 - -var CallbackWrapper = function(port, request) { - // No need to bind every single time - this.callback = this.proxy.bind(this); - this.messaging = vAPI.messaging; - this.init(port, request); -}; - -CallbackWrapper.junkyard = []; - -CallbackWrapper.factory = function(port, request) { - var wrapper = CallbackWrapper.junkyard.pop(); - if ( wrapper ) { - wrapper.init(port, request); - return wrapper; - } - return new CallbackWrapper(port, request); -}; - -CallbackWrapper.prototype.init = function(port, request) { - this.port = port; - this.request = request; -}; - -CallbackWrapper.prototype.proxy = function(response) { - // https://github.com/chrisaljoudi/uBlock/issues/383 - if ( this.messaging.ports.hasOwnProperty(this.port.name) ) { - this.port.postMessage({ - requestId: this.request.requestId, - channelName: this.request.channelName, - msg: response !== undefined ? response : null - }); - } - // Mark for reuse - this.port = this.request = null; - CallbackWrapper.junkyard.push(this); -}; - -/******************************************************************************/ - vAPI.messaging.listen = function(listenerName, callback) { this.listeners[listenerName] = callback; }; /******************************************************************************/ -vAPI.messaging.onPortMessage = function(request, port) { - var callback = vAPI.messaging.NOOPFUNC; - if ( request.requestId !== undefined ) { - callback = CallbackWrapper.factory(port, request).callback; - } +vAPI.messaging.onPortMessage = (function() { + var messaging = vAPI.messaging; + var toAuxPending = {}; - // Specific handler - var r = vAPI.messaging.UNHANDLED; - var listener = vAPI.messaging.listeners[request.channelName]; - if ( typeof listener === 'function' ) { - r = listener(request.msg, port.sender, callback); - } - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } + // Use a wrapper to avoid closure and to allow reuse. + var CallbackWrapper = function(port, request, timeout) { + this.callback = this.proxy.bind(this); // bind once + this.init(port, request, timeout); + }; - // Default handler - r = vAPI.messaging.defaultHandler(request.msg, port.sender, callback); - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } + CallbackWrapper.prototype.init = function(port, request, timeout) { + this.port = port; + this.request = request; + this.timerId = timeout !== undefined ? + vAPI.setTimeout(this.callback, timeout) : + null; + return this; + }; - console.error('uBlock> messaging > unknown request: %o', request); + CallbackWrapper.prototype.proxy = function(response) { + if ( this.timerId !== null ) { + clearTimeout(this.timerId); + delete toAuxPending[this.timerId]; + this.timerId = null; + } + // https://github.com/chrisaljoudi/uBlock/issues/383 + if ( messaging.ports.hasOwnProperty(this.port.name) ) { + this.port.postMessage({ + auxProcessId: this.request.auxProcessId, + channelName: this.request.channelName, + msg: response !== undefined ? response : null + }); + } + // Mark for reuse + this.port = this.request = null; + callbackWrapperJunkyard.push(this); + }; - // Unhandled: - // Need to callback anyways in case caller expected an answer, or - // else there is a memory leak on caller's side - callback(); -}; + var callbackWrapperJunkyard = []; + + var callbackWrapperFactory = function(port, request, timeout) { + var wrapper = callbackWrapperJunkyard.pop(); + if ( wrapper ) { + return wrapper.init(port, request, timeout); + } + return new CallbackWrapper(port, request, timeout); + }; + + var toAux = function(details, portFrom) { + var portTo; + var chromiumTabId = toChromiumTabId(details.toTabId); + + // TODO: This could be an issue with a lot of tabs: easy to address + // with a port name to tab id map. + for ( var portName in messaging.ports ) { + if ( messaging.ports.hasOwnProperty(portName) === false ) { + continue; + } + if ( messaging.ports[portName].sender.tab.id === chromiumTabId ) { + portTo = messaging.ports[portName]; + break; + } + } + + var wrapper; + if ( details.auxProcessId !== undefined ) { + wrapper = callbackWrapperFactory(portFrom, details, 1023); + } + + // Destination not found: + if ( portTo === undefined ) { + if ( wrapper !== undefined ) { + wrapper.callback(); + } + return; + } + + // As per HTML5, timer id is always an integer, thus suitable to be + // used as a key, and which value is safe to use across process + // boundaries. + if ( wrapper !== undefined ) { + toAuxPending[wrapper.timerId] = wrapper; + } + + portTo.postMessage({ + mainProcessId: wrapper && wrapper.timerId, + channelName: details.toChannel, + msg: details.msg + }); + }; + + var toAuxResponse = function(details) { + var mainProcessId = details.mainProcessId; + if ( mainProcessId === undefined ) { + return; + } + if ( toAuxPending.hasOwnProperty(mainProcessId) === false ) { + return; + } + var wrapper = toAuxPending[mainProcessId]; + delete toAuxPending[mainProcessId]; + wrapper.callback(details.msg); + }; + + return function(request, port) { + // Auxiliary process to auxiliary process + if ( request.toTabId !== undefined ) { + toAux(request, port); + return; + } + + // Auxiliary process to auxiliary process: response + if ( request.mainProcessId !== undefined ) { + toAuxResponse(request); + return; + } + + // Auxiliary process to main process: prepare response + var callback = messaging.NOOPFUNC; + if ( request.auxProcessId !== undefined ) { + callback = callbackWrapperFactory(port, request).callback; + } + + // Auxiliary process to main process: specific handler + var r = messaging.UNHANDLED; + var listener = messaging.listeners[request.channelName]; + if ( typeof listener === 'function' ) { + r = listener(request.msg, port.sender, callback); + } + if ( r !== messaging.UNHANDLED ) { + return; + } + + // Auxiliary process to main process: default handler + r = messaging.defaultHandler(request.msg, port.sender, callback); + if ( r !== messaging.UNHANDLED ) { + return; + } + + // Auxiliary process to main process: no handler + console.error('uBlock> messaging > unknown request: %o', request); + + // Need to callback anyways in case caller expected an answer, or + // else there is a memory leak on caller's side + callback(); + }; +})(); /******************************************************************************/ @@ -661,46 +723,6 @@ vAPI.messaging.broadcast = function(message) { /******************************************************************************/ -// "Auxiliary process": any process other than main process. -// -// Main process to auxiliary processes messaging. The approach is that of -// smoke-signal messaging, so emitters have to be ready to deal with no -// response at all (i.e. use timeout where needed). -// -// Mandatory: -// - receiverTabId: Which tab to send the message. -// No target tab id means sends to all tabs. -// - receiverChannel: Which channel to send the message. -// -// Optional: -// - senderTabId: From which tab the message originates. -// - senderChannel: From which channel the message originates. -// These optional fields are useful for the target, and may be used -// to send back a response to the sender. - -vAPI.messaging.post = function(message) { - var port; - var chromiumTabId = toChromiumTabId(message.receiverTabId); - for ( var portName in this.ports ) { - if ( this.ports.hasOwnProperty(portName) === false ) { - continue; - } - port = this.ports[portName]; - if ( chromiumTabId !== 0 && port.sender.tab.id !== chromiumTabId ) { - continue; - } - port.postMessage({ - channelName: message.receiverChannel, - msg: message - }); - if ( chromiumTabId !== 0 ) { - break; - } - } -}; - -/******************************************************************************/ - vAPI.net = {}; /******************************************************************************/ diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 763ebbbd0..598113d79 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -73,157 +73,11 @@ vAPI.shutdown = (function() { /******************************************************************************/ -var MessagingListeners = function(callback) { - this.listeners = []; - if ( typeof callback === 'function' ) { - this.listeners.push(callback); - } -}; - -MessagingListeners.prototype.add = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - if ( this.listeners.indexOf(callback) !== -1 ) { - throw new Error('Duplicate listener.'); - } - this.listeners.push(callback); -}; - -MessagingListeners.prototype.remove = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - if ( this.listeners.indexOf(callback) === -1 ) { - throw new Error('Listener not found.'); - } - this.listeners.splice(this.listeners.indexOf(callback), 1); -}; - -MessagingListeners.prototype.removeAll = function() { - this.listeners = []; -}; - -MessagingListeners.prototype.process = function(msg) { - var listeners = this.listeners; - var n = listeners.length; - for ( var i = 0; i < n; i++ ) { - listeners[i](msg); - } -}; - -/******************************************************************************/ - -var messagingConnector = function(response) { - if ( !response ) { - return; - } - - var messaging = vAPI.messaging; - var channels = messaging.channels; - var channel; - - // Sent to all channels - if ( response.broadcast === true && !response.channelName ) { - for ( channel in channels ) { - if ( channels[channel] instanceof MessagingChannel === false ) { - continue; - } - channels[channel].listeners.process(response.msg); - } - return; - } - - // Response to specific message previously sent - if ( response.requestId ) { - var listener = messaging.pending[response.requestId]; - delete messaging.pending[response.requestId]; - delete response.requestId; // TODO: why? - if ( listener ) { - listener(response.msg); - return; - } - } - - // Sent to a specific channel - channel = channels[response.channelName]; - if ( channel instanceof MessagingChannel ) { - channel.listeners.process(response.msg); - } -}; - -/******************************************************************************/ - -var MessagingChannel = function(name, callback) { - this.channelName = name; - this.listeners = new MessagingListeners(callback); - this.refCount = 1; - if ( typeof callback === 'function' ) { - var messaging = vAPI.messaging; - if ( messaging.port === null ) { - messaging.setup(); - } - } -}; - -MessagingChannel.prototype.send = function(message, callback) { - var messaging = vAPI.messaging; - if ( messaging.port === null ) { - messaging.setup(); - } - var requestId; - if ( callback ) { - requestId = messaging.requestId++; - messaging.pending[requestId] = callback; - } - messaging.port.postMessage({ - channelName: this.channelName, - requestId: requestId, - msg: message - }); -}; - -MessagingChannel.prototype.close = function() { - this.refCount -= 1; - if ( this.refCount !== 0 ) { - return; - } - var messaging = vAPI.messaging; - delete messaging.channels[this.channelName]; - if ( Object.keys(messaging.channels).length === 0 ) { - messaging.close(); - } -}; - -MessagingChannel.prototype.addListener = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - this.listeners.add(callback); - var messaging = vAPI.messaging; - if ( messaging.port === null ) { - messaging.setup(); - } -}; - -MessagingChannel.prototype.removeListener = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - this.listeners.remove(callback); -}; - -MessagingChannel.prototype.removeAllListeners = function() { - this.listeners.removeAll(); -}; - -/******************************************************************************/ - vAPI.messaging = { port: null, channels: {}, pending: {}, - requestId: 1, + auxProcessId: 1, setup: function() { this.port = chrome.runtime.connect({name: vAPI.sessionId}); @@ -258,6 +112,148 @@ vAPI.messaging = { /******************************************************************************/ +var messagingConnector = function(details) { + if ( !details ) { + return; + } + + var messaging = vAPI.messaging; + var channels = messaging.channels; + var channel; + + // Sent to all channels + if ( details.broadcast === true && !details.channelName ) { + for ( channel in channels ) { + if ( channels[channel] instanceof MessagingChannel === false ) { + continue; + } + channels[channel].sendToListeners(details.msg); + } + return; + } + + // Response to specific message previously sent + if ( details.auxProcessId ) { + var listener = messaging.pending[details.auxProcessId]; + delete messaging.pending[details.auxProcessId]; + delete details.auxProcessId; // TODO: why? + if ( listener ) { + listener(details.msg); + return; + } + } + + // Sent to a specific channel + var response; + channel = channels[details.channelName]; + if ( channel instanceof MessagingChannel ) { + response = channel.sendToListeners(details.msg); + } + + // Respond back if required + if ( details.mainProcessId !== undefined ) { + messaging.port.postMessage({ + mainProcessId: details.mainProcessId, + msg: response + }); + } +}; + +/******************************************************************************/ + +var MessagingChannel = function(name, callback) { + this.channelName = name; + this.listeners = typeof callback === 'function' ? [callback] : []; + this.refCount = 1; + if ( typeof callback === 'function' ) { + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } + } +}; + +MessagingChannel.prototype.send = function(message, callback) { + this.sendTo(message, undefined, undefined, callback); +}; + +MessagingChannel.prototype.sendTo = function(message, toTabId, toChannel, callback) { + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } + var auxProcessId; + if ( callback ) { + auxProcessId = messaging.auxProcessId++; + messaging.pending[auxProcessId] = callback; + } + messaging.port.postMessage({ + channelName: this.channelName, + auxProcessId: auxProcessId, + toTabId: toTabId, + toChannel: toChannel, + msg: message + }); +}; + +MessagingChannel.prototype.close = function() { + this.refCount -= 1; + if ( this.refCount !== 0 ) { + return; + } + var messaging = vAPI.messaging; + delete messaging.channels[this.channelName]; + if ( Object.keys(messaging.channels).length === 0 ) { + messaging.close(); + } +}; + +MessagingChannel.prototype.addListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) !== -1 ) { + throw new Error('Duplicate listener.'); + } + this.listeners.push(callback); + var messaging = vAPI.messaging; + if ( messaging.port === null ) { + messaging.setup(); + } +}; + +MessagingChannel.prototype.removeListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + var pos = this.listeners.indexOf(callback); + if ( pos === -1 ) { + throw new Error('Listener not found.'); + } + this.listeners.splice(pos, 1); +}; + +MessagingChannel.prototype.removeAllListeners = function() { + this.listeners = []; +}; + +MessagingChannel.prototype.sendToListeners = function(msg) { + var response; + var listeners = this.listeners; + for ( var i = 0, n = listeners.length; i < n; i++ ) { + response = listeners[i](msg); + if ( response !== undefined ) { + break; + } + } + return response; +}; + +// https://www.youtube.com/watch?v=rT5zCHn0tsg +// https://www.youtube.com/watch?v=E-jS4e3zacI + +/******************************************************************************/ + // No need to have vAPI client linger around after shutdown if // we are not a top window (because element picker can still // be injected in top window). diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 67bda4ff2..fd070fabb 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1149,60 +1149,202 @@ vAPI.messaging.listen = function(listenerName, callback) { /******************************************************************************/ -vAPI.messaging.onMessage = function({target, data}) { - var messageManager = target.messageManager; +vAPI.messaging.onMessage = (function() { + var messaging = vAPI.messaging; + var toAuxPending = {}; + + // Use a wrapper to avoid closure and to allow reuse. + var CallbackWrapper = function(messageManager, channelName, listenerId, auxProcessId, timeout) { + this.callback = this.proxy.bind(this); // bind once + this.init(messageManager, listenerId, channelName, auxProcessId, timeout); + }; + + CallbackWrapper.prototype.init = function(messageManager, listenerId, channelName, auxProcessId, timeout) { + this.messageManager = messageManager; + this.listenerId = listenerId; + this.channelName = channelName; + this.auxProcessId = auxProcessId; + this.timerId = timeout !== undefined ? + vAPI.setTimeout(this.callback, timeout) : + null; + return this; + }; + + CallbackWrapper.prototype.proxy = function(response) { + if ( this.timerId !== null ) { + clearTimeout(this.timerId); + delete toAuxPending[this.timerId]; + this.timerId = null; + } + var message = JSON.stringify({ + auxProcessId: this.auxProcessId, + channelName: this.channelName, + msg: response !== undefined ? response : null + }); + + if ( this.messageManager.sendAsyncMessage ) { + this.messageManager.sendAsyncMessage(this.listenerId, message); + } else { + this.messageManager.broadcastAsyncMessage(this.listenerId, message); + } + + // Mark for reuse + this.messageManager = + this.listenerId = + this.channelName = + this.auxProcessId = null; + callbackWrapperJunkyard.push(this); + }; + + var callbackWrapperJunkyard = []; + + var callbackWrapperFactory = function(messageManager, listenerId, channelName, auxProcessId, timeout) { + var wrapper = callbackWrapperJunkyard.pop(); + if ( wrapper ) { + return wrapper.init(messageManager, listenerId, channelName, auxProcessId, timeout); + } + return new CallbackWrapper(messageManager, listenerId, channelName, auxProcessId, timeout); + }; + + // "Auxiliary process": any process other than main process. + var toAux = function(target, details) { + var messageManagerFrom = target.messageManager; - if ( !messageManager ) { // Message came from a popup, and its message manager is not usable. // So instead we broadcast to the parent window. - messageManager = getOwnerWindow( - target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler - ).messageManager; - } + if ( !messageManagerFrom ) { + messageManagerFrom = getOwnerWindow( + target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler + ).messageManager; + } - var channelNameRaw = data.channelName; - var pos = channelNameRaw.indexOf('|'); - var channelName = channelNameRaw.slice(pos + 1); + var wrapper; + if ( details.auxProcessId !== undefined ) { + var channelNameRaw = details.channelName; + var pos = channelNameRaw.indexOf('|'); + wrapper = callbackWrapperFactory( + messageManagerFrom, + channelNameRaw.slice(0, pos), + channelNameRaw.slice(pos + 1), + details.auxProcessId, + 1023 + ); + } - var callback = vAPI.messaging.NOOPFUNC; - if ( data.requestId !== undefined ) { - callback = CallbackWrapper.factory( - messageManager, - channelName, - channelNameRaw.slice(0, pos), - data.requestId - ).callback; - } + var messageManagerTo = null; + var browser = tabWatcher.browserFromTabId(details.toTabId); + if ( browser !== null && browser.messageManager ) { + messageManagerTo = browser.messageManager; + } + if ( messageManagerTo === null ) { + if ( wrapper !== undefined ) { + wrapper.callback(); + } + return; + } - var sender = { - tab: { - id: tabWatcher.tabIdFromTarget(target) + // As per HTML5, timer id is always an integer, thus suitable to be used + // as a key, and which value is safe to use across process boundaries. + if ( wrapper !== undefined ) { + toAuxPending[wrapper.timerId] = wrapper; + } + + var targetId = location.host + ':broadcast'; + var payload = JSON.stringify({ + mainProcessId: wrapper && wrapper.timerId, + channelName: details.toChannel, + msg: details.msg + }); + + if ( messageManagerTo.sendAsyncMessage ) { + messageManagerTo.sendAsyncMessage(targetId, payload); + } else { + messageManagerTo.broadcastAsyncMessage(targetId, payload); } }; - // Specific handler - var r = vAPI.messaging.UNHANDLED; - var listener = vAPI.messaging.listeners[channelName]; - if ( typeof listener === 'function' ) { - r = listener(data.msg, sender, callback); - } - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } + var toAuxResponse = function(details) { + var mainProcessId = details.mainProcessId; + if ( mainProcessId === undefined ) { + return; + } + if ( toAuxPending.hasOwnProperty(mainProcessId) === false ) { + return; + } + var wrapper = toAuxPending[mainProcessId]; + delete toAuxPending[mainProcessId]; + wrapper.callback(details.msg); + }; - // Default handler - r = vAPI.messaging.defaultHandler(data.msg, sender, callback); - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } + return function({target, data}) { + // Auxiliary process to auxiliary process + if ( data.toTabId !== undefined ) { + toAux(target, data); + return; + } - console.error('uBlock> messaging > unknown request: %o', data); + // Auxiliary process to auxiliary process: response + if ( data.mainProcessId !== undefined ) { + toAuxResponse(data); + return; + } - // Unhandled: - // Need to callback anyways in case caller expected an answer, or - // else there is a memory leak on caller's side - callback(); -}; + // Auxiliary process to main process + var messageManager = target.messageManager; + + // Message came from a popup, and its message manager is not usable. + // So instead we broadcast to the parent window. + if ( !messageManager ) { + messageManager = getOwnerWindow( + target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler + ).messageManager; + } + + var channelNameRaw = data.channelName; + var pos = channelNameRaw.indexOf('|'); + var channelName = channelNameRaw.slice(pos + 1); + + // Auxiliary process to main process: prepare response + var callback = messaging.NOOPFUNC; + if ( data.auxProcessId !== undefined ) { + callback = callbackWrapperFactory( + messageManager, + channelNameRaw.slice(0, pos), + channelName, + data.auxProcessId + ).callback; + } + + var sender = { + tab: { + id: tabWatcher.tabIdFromTarget(target) + } + }; + + // Auxiliary process to main process: specific handler + var r = messaging.UNHANDLED; + var listener = messaging.listeners[channelName]; + if ( typeof listener === 'function' ) { + r = listener(data.msg, sender, callback); + } + if ( r !== messaging.UNHANDLED ) { + return; + } + + // Auxiliary process to main process: default handler + r = messaging.defaultHandler(data.msg, sender, callback); + if ( r !== messaging.UNHANDLED ) { + return; + } + + // Auxiliary process to main process: no handler + console.error('uBlock> messaging > unknown request: %o', data); + + // Need to callback anyways in case caller expected an answer, or + // else there is a memory leak on caller's side + callback(); + }; +})(); /******************************************************************************/ @@ -1239,55 +1381,6 @@ vAPI.messaging.setup = function(defaultHandler) { /******************************************************************************/ -// "Auxiliary process": any process other than main process. -// -// Main process to auxiliary processes messaging. The approach is that of -// smoke-signal messaging, so emitters have to be ready to deal with no -// response at all (i.e. use timeout where needed). -// -// Mandatory: -// - receiverTabId: Which tab to send the message. -// No target tab id means sends to all tabs. -// - receiverChannel: Which channel to send the message. -// -// Optional: -// - senderTabId: From which tab the message originates. -// - senderChannel: From which channel the message originates. -// These optional fields are useful for the target, and may be used -// to send back a response to the sender. - -vAPI.messaging.post = function(message) { - var ffTabId = message.receiverTabId || ''; - var targetId = location.host + ':broadcast'; - var payload = JSON.stringify({ - channelName: message.receiverChannel, - msg: message - }); - - if ( ffTabId === '' ) { - this.globalMessageManager.broadcastAsyncMessage(targetId, payload); - return; - } - - var browser = tabWatcher.browserFromTabId(ffTabId); - if ( browser === null ) { - return; - } - - var messageManager = browser.messageManager || null; - if ( messageManager === null ) { - return; - } - - if ( messageManager.sendAsyncMessage ) { - messageManager.sendAsyncMessage(targetId, payload); - } else { - messageManager.broadcastAsyncMessage(targetId, payload); - } -}; - -/******************************************************************************/ - vAPI.messaging.broadcast = function(message) { this.globalMessageManager.broadcastAsyncMessage( location.host + ':broadcast', @@ -1297,69 +1390,6 @@ vAPI.messaging.broadcast = function(message) { /******************************************************************************/ -// This allows to avoid creating a closure for every single message which -// expects an answer. Having a closure created each time a message is processed -// has been always bothering me. Another benefit of the implementation here -// is to reuse the callback proxy object, so less memory churning. -// -// https://developers.google.com/speed/articles/optimizing-javascript -// "Creating a closure is significantly slower then creating an inner -// function without a closure, and much slower than reusing a static -// function" -// -// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html -// "the dreaded 'uniformly slow code' case where every function takes 1% -// of CPU and you have to make one hundred separate performance optimizations -// to improve performance at all" -// -// http://jsperf.com/closure-no-closure/2 - -var CallbackWrapper = function(messageManager, channelName, listenerId, requestId) { - this.callback = this.proxy.bind(this); // bind once - this.init(messageManager, channelName, listenerId, requestId); -}; - -CallbackWrapper.junkyard = []; - -CallbackWrapper.factory = function(messageManager, channelName, listenerId, requestId) { - var wrapper = CallbackWrapper.junkyard.pop(); - if ( wrapper ) { - wrapper.init(messageManager, channelName, listenerId, requestId); - return wrapper; - } - return new CallbackWrapper(messageManager, channelName, listenerId, requestId); -}; - -CallbackWrapper.prototype.init = function(messageManager, channelName, listenerId, requestId) { - this.messageManager = messageManager; - this.channelName = channelName; - this.listenerId = listenerId; - this.requestId = requestId; -}; - -CallbackWrapper.prototype.proxy = function(response) { - var message = JSON.stringify({ - requestId: this.requestId, - channelName: this.channelName, - msg: response !== undefined ? response : null - }); - - if ( this.messageManager.sendAsyncMessage ) { - this.messageManager.sendAsyncMessage(this.listenerId, message); - } else { - this.messageManager.broadcastAsyncMessage(this.listenerId, message); - } - - // Mark for reuse - this.messageManager = - this.channelName = - this.requestId = - this.listenerId = null; - CallbackWrapper.junkyard.push(this); -}; - -/******************************************************************************/ - var httpObserver = { classDescription: 'net-channel-event-sinks for ' + location.host, classID: Components.ID('{dc8d6319-5f6e-4438-999e-53722db99e84}'), diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index 778a7c31d..62fa77e07 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -67,152 +67,10 @@ vAPI.shutdown = (function() { /******************************************************************************/ -var MessagingListeners = function(callback) { - this.listeners = []; - if ( typeof callback === 'function' ) { - this.listeners.push(callback); - } -}; - -MessagingListeners.prototype.add = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - if ( this.listeners.indexOf(callback) !== -1 ) { - throw new Error('Duplicate listener.'); - } - this.listeners.push(callback); -}; - -MessagingListeners.prototype.remove = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - if ( this.listeners.indexOf(callback) === -1 ) { - throw new Error('Listener not found.'); - } - this.listeners.splice(this.listeners.indexOf(callback), 1); -}; - -MessagingListeners.prototype.removeAll = function() { - this.listeners = []; -}; - -MessagingListeners.prototype.process = function(msg) { - var listeners = this.listeners; - var n = listeners.length; - for ( var i = 0; i < n; i++ ) { - listeners[i](msg); - } -}; - -/******************************************************************************/ - -var messagingConnector = function(response) { - if ( !response ) { - return; - } - - var messaging = vAPI.messaging; - var channels = messaging.channels; - var channel; - - // Sent to all channels - if ( response.broadcast === true && !response.channelName ) { - for ( channel in channels ) { - if ( channels[channel] instanceof MessagingChannel === false ) { - continue; - } - channels[channel].listeners.process(response.msg); - } - return; - } - - // Response to specific message previously sent - if ( response.requestId ) { - var listener = messaging.pending[response.requestId]; - delete messaging.pending[response.requestId]; - delete response.requestId; // TODO: why? - if ( listener ) { - listener(response.msg); - return; - } - } - - // Sent to a specific channel - channel = channels[response.channelName]; - if ( channel instanceof MessagingChannel ) { - channel.listeners.process(response.msg); - } -}; - -/******************************************************************************/ - -var MessagingChannel = function(name, callback) { - this.channelName = name; - this.listeners = new MessagingListeners(callback); - this.refCount = 1; - if ( typeof callback === 'function' ) { - var messaging = vAPI.messaging; - if ( !messaging.connected ) { - messaging.setup(); - } - } -}; - -MessagingChannel.prototype.send = function(message, callback) { - var messaging = vAPI.messaging; - if ( !messaging.connected ) { - messaging.setup(); - } - var requestId; - if ( callback ) { - requestId = messaging.requestId++; - messaging.pending[requestId] = callback; - } - sendAsyncMessage('ublock0:background', { - channelName: self._sandboxId_ + '|' + this.channelName, - requestId: requestId, - msg: message - }); -}; - -MessagingChannel.prototype.close = function() { - this.refCount -= 1; - if ( this.refCount !== 0 ) { - return; - } - delete vAPI.messaging.channels[this.channelName]; -}; - -MessagingChannel.prototype.addListener = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - this.listeners.add(callback); - var messaging = vAPI.messaging; - if ( !messaging.connected ) { - messaging.setup(); - } -}; - -MessagingChannel.prototype.removeListener = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - this.listeners.remove(callback); -}; - -MessagingChannel.prototype.removeAllListeners = function() { - this.listeners.removeAll(); -}; - -/******************************************************************************/ - vAPI.messaging = { channels: {}, pending: {}, - requestId: 1, + auxProcessId: 1, connected: false, connector: function(msg) { messagingConnector(JSON.parse(msg)); @@ -283,6 +141,143 @@ window.addEventListener('pageshow', vAPI.messaging.toggleListener, true); /******************************************************************************/ +var messagingConnector = function(details) { + if ( !details ) { + return; + } + + var messaging = vAPI.messaging; + var channels = messaging.channels; + var channel; + + // Sent to all channels + if ( details.broadcast === true && !details.channelName ) { + for ( channel in channels ) { + if ( channels[channel] instanceof MessagingChannel === false ) { + continue; + } + channels[channel].sendToListeners(details.msg); + } + return; + } + + // Response to specific message previously sent + if ( details.auxProcessId ) { + var listener = messaging.pending[details.auxProcessId]; + delete messaging.pending[details.auxProcessId]; + delete details.auxProcessId; // TODO: why? + if ( listener ) { + listener(details.msg); + return; + } + } + + // Sent to a specific channel + var response; + channel = channels[details.channelName]; + if ( channel instanceof MessagingChannel ) { + response = channel.sendToListeners(details.msg); + } + + // Respond back if required + if ( details.mainProcessId !== undefined ) { + sendAsyncMessage('ublock0:background', { + mainProcessId: details.mainProcessId, + msg: response + }); + } +}; + +/******************************************************************************/ + +var MessagingChannel = function(name, callback) { + this.channelName = name; + this.listeners = typeof callback === 'function' ? [callback] : []; + this.refCount = 1; + if ( typeof callback === 'function' ) { + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } + } +}; + +MessagingChannel.prototype.send = function(message, callback) { + this.sendTo(message, undefined, undefined, callback); +}; + +MessagingChannel.prototype.sendTo = function(message, toTabId, toChannel, callback) { + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } + var auxProcessId; + if ( callback ) { + auxProcessId = messaging.auxProcessId++; + messaging.pending[auxProcessId] = callback; + } + sendAsyncMessage('ublock0:background', { + channelName: self._sandboxId_ + '|' + this.channelName, + auxProcessId: auxProcessId, + toTabId: toTabId, + toChannel: toChannel, + msg: message + }); +}; + +MessagingChannel.prototype.close = function() { + this.refCount -= 1; + if ( this.refCount !== 0 ) { + return; + } + delete vAPI.messaging.channels[this.channelName]; +}; + +MessagingChannel.prototype.addListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + if ( this.listeners.indexOf(callback) !== -1 ) { + throw new Error('Duplicate listener.'); + } + this.listeners.push(callback); + var messaging = vAPI.messaging; + if ( !messaging.connected ) { + messaging.setup(); + } +}; + +MessagingChannel.prototype.removeListener = function(callback) { + if ( typeof callback !== 'function' ) { + return; + } + var pos = this.listeners.indexOf(callback); + if ( pos === -1 ) { + throw new Error('Listener not found.'); + } + this.listeners.splice(pos, 1); +}; + +MessagingChannel.prototype.removeAllListeners = function() { + this.listeners = []; +}; + +MessagingChannel.prototype.sendToListeners = function(msg) { + var response; + var listeners = this.listeners; + for ( var i = 0, n = listeners.length; i < n; i++ ) { + response = listeners[i](msg); + if ( response !== undefined ) { + break; + } + } + return response; +}; + +// https://www.youtube.com/watch?v=Cg0cmhjdiLs + +/******************************************************************************/ + // No need to have vAPI client linger around after shutdown if // we are not a top window (because element picker can still // be injected in top window). diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index f5dcc48e8..6933c2b56 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -142,6 +142,8 @@ var renderDOMFull = function(response) { inspector.appendChild(domTree); }; +// https://www.youtube.com/watch?v=IDGNA83mxDo + /******************************************************************************/ var patchIncremental = function(from, delta) { @@ -229,6 +231,8 @@ var renderDOMIncremental = function(response) { } }; +// https://www.youtube.com/watch?v=6u2KPtJB9h8 + /******************************************************************************/ var countFromNode = function(li) { @@ -358,20 +362,17 @@ var onClick = function(ev) { // Toggle selector if ( target.localName === 'code' ) { var original = target.classList.contains('filter') === false; - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { + messager.sendTo( + { what: 'toggleNodes', original: original, target: original !== target.classList.toggle('off'), selector: selectorFromNode(target, original ? 1 : 2), nid: original ? nidFromNode(target) : '' - } - }); + }, + inspectedTabId, + 'dom-inspector.js' + ); var cantCreate = inspector.querySelector('#domTree .off') === null; inspector.querySelector('.permatoolbar .revert').classList.toggle('disabled', cantCreate); inspector.querySelector('.permatoolbar .commit').classList.toggle('disabled', cantCreate); @@ -387,19 +388,16 @@ var onMouseOver = (function() { var timerHandler = function() { mouseoverTimer = null; - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { + messager.sendTo( + { what: 'highlightOne', selector: selectorFromNode(mouseoverTarget), nid: nidFromNode(mouseoverTarget), scrollTo: true - } - }); + }, + inspectedTabId, + 'dom-inspector.js' + ); }; return function(ev) { @@ -447,8 +445,8 @@ var cancelPollTimer = function() { /******************************************************************************/ var onDOMFetched = function(response) { - if ( response === undefined || currentTabId() !== inspectedTabId ) { - shutdownInspector(inspectedTabId); + if ( !response || currentTabId() !== inspectedTabId ) { + shutdownInspector(); injectInspectorAsync(250); return; } @@ -478,21 +476,15 @@ var onDOMFetched = function(response) { /******************************************************************************/ var fetchDOM = function() { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { + messager.sendTo( + { what: 'domLayout', fingerprint: fingerprint - } - }); - pollTimer = vAPI.setTimeout(function() { - pollTimer = null; - onDOMFetched(); - }, 1001); + }, + inspectedTabId, + 'dom-inspector.js', + onDOMFetched + ); }; /******************************************************************************/ @@ -504,7 +496,7 @@ var fetchDOMAsync = function(delay) { pollTimer = vAPI.setTimeout(function() { pollTimer = null; fetchDOM(); - }, delay || 1001); + }, delay || 2003); }; /******************************************************************************/ @@ -543,15 +535,10 @@ var injectInspectorAsync = function(delay) { /******************************************************************************/ -var shutdownInspector = function(tabId) { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: tabId, - receiverChannel: 'dom-inspector.js', - msg: { what: 'shutdown', } - }); +var shutdownInspector = function() { + if ( inspectedTabId !== '' ) { + messager.sendTo({ what: 'shutdown' }, inspectedTabId, 'dom-inspector.js'); + } logger.removeAllChildren(domTree); cancelPollTimer(); inspectedTabId = ''; @@ -569,31 +556,25 @@ var onTabIdChanged = function() { /******************************************************************************/ var toggleHighlightMode = function() { - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { + messager.sendTo( + { what: 'highlightMode', invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert') - } - }); + }, + inspectedTabId, + 'dom-inspector.js' + ); }; /******************************************************************************/ var revert = function() { uDom('#domTree .off').removeClass('off'); - messager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'logger-ui.js', - receiverTabId: inspectedTabId, - receiverChannel: 'dom-inspector.js', - msg: { what: 'resetToggledNodes' } - }); + messager.sendTo( + { what: 'resetToggledNodes' }, + inspectedTabId, + 'dom-inspector.js' + ); inspector.querySelector('.permatoolbar .revert').classList.add('disabled'); inspector.querySelector('.permatoolbar .commit').classList.add('disabled'); }; @@ -601,11 +582,10 @@ var revert = function() { /******************************************************************************/ var onMessage = function(request) { - var msg = request.what === 'postMessageTo' ? request.msg : request; - switch ( msg.what ) { + switch ( request.what ) { case 'domLayout': cancelPollTimer(); - onDOMFetched(msg); + onDOMFetched(request); break; default: diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 759128693..409defe69 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -1286,6 +1286,8 @@ var netFilteringManager = (function() { }; })(); +// https://www.youtube.com/watch?v=XyNYrmmdUd4 + /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index d734c0004..1e22bbf92 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -141,12 +141,6 @@ var onMessage = function(request, sender, callback) { vAPI.tabs.open(request.details); break; - // Passthrough for auxiliary process to auxiliary process messaging. - case 'postMessageTo': - request.senderTabId = tabId; - vAPI.messaging.post(request); - break; - case 'reloadTab': if ( vAPI.isBehindTheSceneTabId(request.tabId) === false ) { vAPI.tabs.reload(request.tabId); diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index c97849daf..ee0546921 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -561,6 +561,8 @@ var domLayout = (function() { }; })(); +// https://www.youtube.com/watch?v=qo8zKhd4Cf0 + /******************************************************************************/ var highlightElements = function(scrollTo) { @@ -724,6 +726,8 @@ var toggleNodes = function(nodes, originalState, targetState) { } }; +// https://www.youtube.com/watch?v=L5jRewnxSBY + /******************************************************************************/ var resetToggledNodes = function() { @@ -757,20 +761,19 @@ var shutdown = function() { /******************************************************************************/ var onMessage = function(request) { - var msg = request.what === 'postMessageTo' ? request.msg : request; var response; - switch ( msg.what ) { + switch ( request.what ) { case 'domLayout': - response = domLayout.get(msg.fingerprint); + response = domLayout.get(request.fingerprint); break; case 'highlightMode': - svgRoot.classList.toggle('invert', msg.invert); + svgRoot.classList.toggle('invert', request.invert); break; case 'highlightOne': - hightlightNodes(msg.selector, msg.nid, msg.scrollTo); + hightlightNodes(request.selector, request.nid, request.scrollTo); break; case 'resetToggledNodes': @@ -778,8 +781,8 @@ var onMessage = function(request) { break; case 'toggleNodes': - highlightedElements = selectNodes(msg.selector, msg.nid); - toggleNodes(highlightedElements, msg.original, msg.target); + highlightedElements = selectNodes(request.selector, request.nid); + toggleNodes(highlightedElements, request.original, request.target); highlightElements(true); break; @@ -791,16 +794,7 @@ var onMessage = function(request) { break; } - if ( response !== undefined && request.what === 'postMessageTo' ) { - localMessager.send({ - what: 'postMessageTo', - senderTabId: null, - senderChannel: 'dom-inspector.js', - receiverTabId: request.senderTabId, - receiverChannel: request.senderChannel, - msg: response - }); - } + return response; }; /******************************************************************************/