├── .gitignore ├── docs └── images │ └── mailicon.png ├── .eslintrc.js ├── src ├── constants.js ├── utils.js ├── messenger.js ├── mockPort.js ├── backgroundHub.js └── connection.js ├── package.json ├── webpack.config.js ├── LICENSE ├── README.md └── dist └── ext-messenger.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | *.min.js.map -------------------------------------------------------------------------------- /docs/images/mailicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asimen1/ext-messenger/HEAD/docs/images/mailicon.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | "env": { 5 | "browser": true, 6 | "es2021": true, 7 | "webextensions": true, 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module", 13 | }, 14 | "rules": { 15 | "semi": ["warn", "always"], 16 | "no-trailing-spaces": "warn", 17 | "space-before-blocks": ["warn", "always"], 18 | "comma-dangle": ["warn", "always-multiline"], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = { 4 | // Used to identify port connections from Messenger API and user "chrome.runtime.connect". 5 | MESSENGER_PORT_NAME_PREFIX: '__messenger__', 6 | 7 | // Wildcard identifier for sending to all of the extension part connections. 8 | TO_NAME_WILDCARD: '*', 9 | 10 | // Extension parts. 11 | BACKGROUND: 'background', 12 | POPUP: 'popup', 13 | DEVTOOL: 'devtool', 14 | CONTENT_SCRIPT: 'content_script', 15 | 16 | // Message types. 17 | INIT: 'init', 18 | INIT_SUCCESS: 'init_success', 19 | MESSAGE: 'message', 20 | RESPONSE: 'response', 21 | }; 22 | 23 | export default constants; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ext-messenger", 3 | "version": "4.0.0", 4 | "author": "Asaf Menahem", 5 | "description": "Extension message passing made easy", 6 | "main": "dist/ext-messenger.min.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "webpack --mode production --progress", 12 | "dev": "webpack --mode development --progress --watch" 13 | }, 14 | "repository": "https://github.com/asimen1/ext-messenger", 15 | "bugs": "https://github.com/asimen1/ext-messenger/issues", 16 | "license": "MIT", 17 | "keywords": [ 18 | "chrome extension", 19 | "message passing", 20 | "messenger", 21 | "messages" 22 | ], 23 | "devDependencies": { 24 | "clean-webpack-plugin": "^4.0.0", 25 | "copy-webpack-plugin": "^12.0.2", 26 | "eslint": "^8.57.0", 27 | "webpack": "^5.90.3", 28 | "webpack-cli": "^5.1.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | 6 | module.exports = (env, argv) => { 7 | let mode = argv.mode ?? 'production'; 8 | 9 | return { 10 | mode: mode, 11 | 12 | // NOTE: This is important to create a CSP compliant output that doesn't use eval and 13 | // NOTE: therefore doesn't require "unsafe-eval" policy declaration in the manifest.json. 14 | devtool: 'source-map', 15 | 16 | entry: path.join(__dirname, 'src', 'messenger.js'), 17 | 18 | output: { 19 | path: path.join(__dirname, 'dist'), 20 | filename: 'ext-messenger.min.js', 21 | library: 'ext-messenger', 22 | libraryTarget: 'umd', 23 | umdNamedDefine: true, 24 | }, 25 | 26 | plugins: [ 27 | new CleanWebpackPlugin(), 28 | ], 29 | }; 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Asaf Menahem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Constants from './constants.js'; 4 | 5 | const LOG_LEVEL = 'warn'; 6 | const LOG_LEVELS = ['log', 'info', 'warn', 'error']; 7 | const LOG_PREFIX = '[ext-messenger]'; 8 | 9 | // ANSI color codes. 10 | const COLORS = { 11 | log: '\x1b[32m', // green 12 | info: '\x1b[34m', // blue 13 | }; 14 | 15 | function shouldLog(logLevel) { 16 | return LOG_LEVELS.indexOf(logLevel) >= LOG_LEVELS.indexOf(LOG_LEVEL); 17 | } 18 | 19 | function log(level) { 20 | // Remove the 'level' argument. 21 | let loggerArgs = Array.prototype.slice.call(arguments, 1); 22 | 23 | if (!shouldLog(level)) { 24 | return; 25 | } 26 | 27 | switch (level) { 28 | case 'log': { 29 | console.log(COLORS.log + LOG_PREFIX + ` [${level}]`, ...loggerArgs); 30 | break; 31 | } 32 | case 'info': { 33 | console.info(COLORS.info + LOG_PREFIX + ` [${level}]`, ...loggerArgs); 34 | break; 35 | } 36 | case 'warn': { 37 | console.warn(LOG_PREFIX, ...loggerArgs); 38 | break; 39 | } 40 | case 'error': { 41 | console.error(LOG_PREFIX, ...loggerArgs); 42 | 43 | // Abort execution on error. 44 | throw '[ext-messenger] error occurred, check more information above...'; 45 | } 46 | 47 | default: { 48 | console.error(LOG_PREFIX, `Unknown log level: ${level}`); 49 | break; 50 | } 51 | } 52 | } 53 | 54 | // For each function: 55 | // - Add log level logging. 56 | // - Autobinding to ensure correct 'this' from all types of function invocations. 57 | function constructorTweakMethods(filename, thisObj) { 58 | let wrapMethod = function(methodName) { 59 | let origFunc = thisObj[methodName]; 60 | 61 | thisObj[methodName] = function() { 62 | log('log', `[${filename}:${methodName}()]`, arguments); 63 | 64 | return origFunc.apply(thisObj, arguments); 65 | }.bind(thisObj); 66 | }; 67 | 68 | for (let key in thisObj) { 69 | if (typeof thisObj[key] === 'function') { 70 | wrapMethod(key); 71 | } 72 | } 73 | 74 | // Autobinding to ensure correct 'this' from all types of function invocations. 75 | Object.getOwnPropertyNames(Object.getPrototypeOf(thisObj)) 76 | .filter(prop => typeof thisObj[prop] === 'function' && prop !== 'constructor') 77 | .forEach(method => { 78 | thisObj[method] = thisObj[method].bind(thisObj); 79 | }); 80 | } 81 | 82 | function removeMessengerPortNamePrefix(portName) { 83 | return portName.replace(new RegExp('^' + Constants.MESSENGER_PORT_NAME_PREFIX), ''); 84 | } 85 | 86 | export default { 87 | log, 88 | constructorTweakMethods, 89 | removeMessengerPortNamePrefix, 90 | }; 91 | -------------------------------------------------------------------------------- /src/messenger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import BackgroundHub from './backgroundHub.js'; 4 | import Connection from './connection.js'; 5 | import Utils from './utils.js'; 6 | import Constants from './constants.js'; 7 | 8 | // -------------------------------------------------------- 9 | // THE MESSENGER ! 10 | // -------------------------------------------------------- 11 | 12 | class Messenger { 13 | constructor(extPart) { 14 | Utils.constructorTweakMethods('Messenger', this); 15 | 16 | // Validate extension part argument. 17 | if (!Object.values(Messenger.EXT_PARTS).includes(extPart)) { 18 | Utils.log('error', '[Messenger:constructor]', `"${extPart}" provided is not a valid extension part. Valid parts are: ${Object.keys(Messenger.EXT_PARTS).join(', ')}`); 19 | return; 20 | } 21 | 22 | this._myExtPart = extPart; 23 | 24 | let api = { 25 | initConnection: this.initConnection, 26 | }; 27 | 28 | // Add the BackgroundHub API only for the background part. 29 | if (this._myExtPart === Constants.BACKGROUND) { 30 | api.initBackgroundHub = this.initBackgroundHub; 31 | } 32 | 33 | return api; 34 | } 35 | 36 | static isMessengerPort(port) { 37 | return port.name.indexOf(Constants.MESSENGER_PORT_NAME_PREFIX) === 0; 38 | } 39 | 40 | static EXT_PARTS = { 41 | BACKGROUND: Constants.BACKGROUND, 42 | POPUP: Constants.POPUP, 43 | DEVTOOL: Constants.DEVTOOL, 44 | CONTENT_SCRIPT: Constants.CONTENT_SCRIPT, 45 | }; 46 | 47 | // ------------------------------------------------------------ 48 | // Exposed API 49 | // ------------------------------------------------------------ 50 | 51 | initBackgroundHub(options) { 52 | if (this._myExtPart !== Constants.BACKGROUND) { 53 | Utils.log('warn', '[Messenger:initBackgroundHub]', 'Ignoring BackgroundHub init request since not called from background context'); 54 | return; 55 | } 56 | 57 | if (this._backgroundHub) { 58 | Utils.log('warn', '[Messenger:initBackgroundHub]', 'Ignoring BackgroundHub init request since it is already been inited'); 59 | return; 60 | } 61 | 62 | // NOTE: Saving reference in order to identify later if was already created. 63 | this._backgroundHub = new BackgroundHub(options); 64 | } 65 | 66 | initConnection(name, messageHandler) { 67 | if (!name) { 68 | Utils.log('error', '[Messenger:initConnection]', 'Missing "name" in arguments'); 69 | } 70 | 71 | if (name === Constants.TO_NAME_WILDCARD) { 72 | Utils.log('error', '[Messenger:initConnection]', '"*" is reserved as a wildcard identifier, please use another name'); 73 | } 74 | 75 | return new Connection(this._myExtPart, name, messageHandler); 76 | } 77 | } 78 | 79 | export default Messenger; -------------------------------------------------------------------------------- /src/mockPort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Utils from './utils.js'; 4 | 5 | class MockPort { 6 | constructor(options) { 7 | Utils.constructorTweakMethods('MockPort', this); 8 | 9 | let creatorMock = this._createMockPort(options); 10 | let targetMock = this._createMockPort(options); 11 | 12 | this._linkMocks(creatorMock, targetMock); 13 | 14 | // BackgroundHub might not have created this "onConnect" method yet. 15 | if (self && typeof self.mockPortOnConnect === 'function') { 16 | self.mockPortOnConnect(targetMock); 17 | } 18 | 19 | return creatorMock; 20 | } 21 | 22 | _createMockPort(options) { 23 | // ------------------------------------------ 24 | // Port API 25 | // ------------------------------------------ 26 | 27 | let mockPort = { 28 | _connected: true, 29 | 30 | _name: options.name, 31 | onMessageListeners: [], 32 | onDisconnectListeners: [], 33 | }; 34 | 35 | Object.defineProperty(mockPort, 'name', { 36 | get: function() { 37 | return mockPort._name; 38 | }, 39 | }); 40 | 41 | Object.defineProperty(mockPort, 'onMessage', { 42 | get: function() { 43 | return { 44 | addListener: function(listener) { 45 | mockPort.onMessageListeners.push(listener); 46 | }, 47 | 48 | removeListener: function(listener) { 49 | let index = mockPort.onMessageListeners.indexOf(listener); 50 | if (index !== -1) { 51 | mockPort.onMessageListeners.splice(index, 1); 52 | } 53 | }, 54 | }; 55 | }, 56 | }); 57 | 58 | Object.defineProperty(mockPort, 'onDisconnect', { 59 | get: function() { 60 | return { 61 | addListener: function(listener) { 62 | mockPort.onDisconnectListeners.push(listener); 63 | }, 64 | 65 | removeListener: function(listener) { 66 | let index = mockPort.onDisconnectListeners.indexOf(listener); 67 | if (index !== -1) { 68 | mockPort.onDisconnectListeners.splice(index, 1); 69 | } 70 | }, 71 | }; 72 | }, 73 | }); 74 | 75 | // Background mock ports should only have the extension id. 76 | // https://developer.chrome.com/extensions/runtime#type-MessageSender 77 | Object.defineProperty(mockPort, 'sender', { 78 | get: function() { 79 | return { id: chrome.runtime.id }; 80 | }, 81 | }); 82 | 83 | mockPort.postMessage = function(msg) { 84 | if (mockPort._connected) { 85 | if (mockPort.__targetRefPort) { 86 | mockPort.__targetRefPort.__invokeOnMessageHandlers(msg); 87 | } else { 88 | Utils.log('warn', '[MockPort:postMessage]', 'Missing __targetRefPort', arguments); 89 | } 90 | } else { 91 | Utils.log('warn', '[MockPort:postMessage]', 'Attempting to post message on a disconnected mock port', msg); 92 | } 93 | }; 94 | 95 | mockPort.disconnect = function() { 96 | mockPort._connected = false; 97 | 98 | if (mockPort.__targetRefPort) { 99 | mockPort.__targetRefPort.__invokeOnDisconnectHandlers(); 100 | } else { 101 | Utils.log('warn', '[MockPort:postMessage]', 'Missing __targetRefPort', arguments); 102 | } 103 | 104 | mockPort._onMessageListeners = []; 105 | mockPort._onDisconnectListeners = []; 106 | }; 107 | 108 | // ------------------------------------------ 109 | // PRIVATE HELPERS 110 | // ------------------------------------------ 111 | 112 | mockPort.__invokeOnMessageHandlers = function(msg) { 113 | mockPort.onMessageListeners.forEach(function(onMessageListener) { 114 | onMessageListener(msg, mockPort); 115 | }); 116 | }; 117 | 118 | mockPort.__invokeOnDisconnectHandlers = function() { 119 | mockPort.onDisconnectListeners.forEach(function(onDisconnectListener) { 120 | onDisconnectListener(mockPort); 121 | }); 122 | }; 123 | 124 | return mockPort; 125 | } 126 | 127 | _linkMocks(creatorMock, targetMock) { 128 | creatorMock.__targetRefPort = targetMock; 129 | targetMock.__targetRefPort = creatorMock; 130 | } 131 | } 132 | 133 | export default MockPort; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Browser Extension message passing made easy 2 | 3 | [![Latest Stable Version](https://img.shields.io/npm/v/ext-messenger.svg)](https://www.npmjs.com/package/ext-messenger) 4 | [![NPM Downloads](https://img.shields.io/npm/dt/ext-messenger.svg)](https://www.npmjs.com/package/ext-messenger) 5 | 6 | ### What? 7 | 8 | Small library for messaging across any browser extension parts (background, content script, popup or devtool). 9 | 10 | It has a simple API, promise based callback support and more. 11 | 12 | Supports extensions for `Chrome` and any Chromium based browser extensions like `Edge`, `Arc`, ... 13 | 14 | ### Why? 15 | 16 | Sending messages between extension parts can get complicated and usually requires some relaying mechanism in the background page. Adding callback functionality to these messages can make it even trickier. 17 | 18 | Furthermore, the messaging API is not coherent or straight forward, sometimes requiring you to use `runtime.*` API and sometimes `tabs.*` API depending on which extension part you are currently in. 19 | 20 | ### How? 21 | 22 | ```shell 23 | npm i ext-messenger 24 | ``` 25 | 26 | OR 27 | 28 | ```shell 29 | yarn add ext-messenger 30 | ``` 31 | 32 | #### 1) In the background page: create a messenger instance and init the background hub 33 | 34 | ```javascript 35 | const Messenger = require('ext-messenger'); 36 | let messenger = new Messenger(Messenger.EXT_PARTS.BACKGROUND); 37 | 38 | messenger.initBackgroundHub(); 39 | ``` 40 | 41 | > This step is **obligatory** and should be done as soon as possible in your background page. 42 | 43 | \* You can also add the [library](https://github.com/asimen1/ext-messenger/tree/master/dist) via script tag and use `window['ext-messenger']`. 44 | 45 | #### 2) Init connections (in any extension parts) 46 | 47 | ```javascript 48 | const Messenger = require('ext-messenger'); 49 | let messenger = new Messenger(Messenger.EXT_PARTS.CONTENT_SCRIPT); 50 | 51 | /* 52 | * {string} name - Identifier name for this connection. 53 | * {function} messageHandler - Handler for incoming messages. 54 | */ 55 | messenger.initConnection(name, messageHandler); 56 | ``` 57 | 58 | This returns a **connection** object. 59 | 60 | #### 3) Start sending messages across connections (in any extension parts) 61 | 62 | ```javascript 63 | /* 64 | * {string} to - ':'. 65 | * {*} message - The message to send (any JSON-ifiable object). 66 | */ 67 | connection.sendMessage(to, message); 68 | ``` 69 | 70 | * `` possible values: `background`, `content_script`, `popup`, `devtool`. 71 | * Sending messages from background require an additional tab id argument `:`. 72 | 73 | Returns a `Promise`: 74 | 75 | * The promise will be resolved if the receiver invoked the `sendResponse` method argument (see example below). 76 | * The promise will be rejected if the connection has been disconnected via the `disconnect()` API. 77 | 78 | #### More 79 | 80 | ```javascript 81 | // Init hub with handlers notifying someone connected/disconnected. 82 | messenger.initBackgroundHub({ 83 | connectedHandler: function(extensionPart, connectionName, tabId) {}, 84 | disconnectedHandler: function(extensionPart, connectionName, tabId) {} 85 | }); 86 | 87 | // Sending to multiple connections is supported via: 88 | // 'extension part:name1,name2,...'. 89 | c.sendMessage('content_script:main,main2', { text: 'HI!' }); 90 | 91 | // Sending to all connections is supported using wildcard value '*'. 92 | c.sendMessage('devtool:*', { text: 'HI!' }); 93 | 94 | // Disconnect the connection to stop listening for messages. 95 | c.disconnect(); 96 | ``` 97 | 98 | > When sending a message to multiple extension names (e.g. "name1,name2" or "*"), the returned promise will be resolved by the first connection that sends a response. 99 | 100 | ### Example 101 | 102 | ```javascript 103 | /* ---------------------------------------------- */ 104 | /* Init connections in desired extension part: */ 105 | /* BACKGROUND, CONTENT_SCRIPT, POPUP, DEVTOOL */ 106 | /* ---------------------------------------------- */ 107 | const Messenger = require('ext-messenger'); 108 | let messenger = new Messenger(Messenger.EXT_PARTS.BACKGROUND); 109 | 110 | let messageHandler = function(msg, from, sender, sendResponse) { 111 | if (msg.text === 'HI!') { 112 | sendResponse('HOWDY!'); 113 | } 114 | }; 115 | 116 | let c = messenger.initConnection('main', messageHandler); 117 | let c2 = messenger.initConnection('main2', messageHandler); 118 | ... 119 | 120 | let msg = { text: 'HI!' }; 121 | 122 | /* ------------------------------------------------------ */ 123 | /* Send message to content script */ 124 | /* ------------------------------------------------------ */ 125 | c.sendMessage('content_script:main', msg); 126 | 127 | /* ------------------------------------------------------ */ 128 | /* Send message to popup (with response) */ 129 | /* ------------------------------------------------------ */ 130 | c.sendMessage('popup:main2', msg).then((response) => { 131 | console.log(response); 132 | }); 133 | 134 | /* ------------------------------------------------------ */ 135 | /* Send message to background (with response) */ 136 | /* ------------------------------------------------------ */ 137 | c.sendMessage('background:main', msg).then((response) => { 138 | console.log(response); 139 | }); 140 | 141 | /* ------------------------------------------------------ */ 142 | /* Send message from background to devtool. */ 143 | /* '50' is an example tab id of the devtool. */ 144 | /* ------------------------------------------------------ */ 145 | c.sendMessage('devtool:main:50', msg).then((response) => { 146 | console.log(response); 147 | }); 148 | ``` 149 | 150 | ### Notes 151 | 152 | * Requires your extension to have ["tabs" permission](https://developer.chrome.com/extensions/declare_permissions). 153 | * Uses only long lived port connections via `runtime.*` API. 154 | * This library should satisfy all your message passing needs, however if you are still handling some port connections manually, using `runtime.onConnect` will also receive messenger ports connections. In order to identify connections originating from this library you can use the static method `Messenger.isMessengerPort(port)` which will return true/false. 155 | * The Messenger `messageHandler` and `runtime.onMessage` similarities and differences: 156 | * **Same** - `sender` object. 157 | * **Same** - `sendResponse` - The argument should be any JSON-ifiable object. 158 | * **Same** - `sendResponse` - With multiple message handler, the `sendResponse()` will work only for the first one to respond. 159 | * **Different** - `from` object indicating the senders formatted identifier e.g. `devtool:connection name`. 160 | * **Different** - Async `sendResponse` is supported directly (no need to return `true` value like with `runtime.onMessage`). 161 | * This library should probably also work for `Firefox` extensions but have never been tested. 162 | * This library is compatible with manifest V3 (if you are looking for a manifest V2 compatible version, you can use version [^3.0.2](https://www.npmjs.com/package/ext-messenger/v/3.0.2)). 163 | 164 | ### Extensions using messenger 165 | 166 | * [Restyler](https://chrome.google.com/webstore/detail/restyler/ofkkcnbmhaodoaehikkibjanliaeffel) 167 | 168 | * Working on one? let me know ext.messenger@gmail.com! [![](https://asimen1.github.io/ext-messenger/images/mailicon.png "email")](mailto:ext.messenger@gmail.com) 169 | 170 | ### Developing Locally 171 | 172 | ```shell 173 | npm install 174 | npm run dev 175 | ``` 176 | 177 | You can now use the built messenger from the `dist` folder in a local test extension (or use [npm link](https://docs.npmjs.com/cli/link)). 178 | 179 | I have created one (for internal testing purposes) that you can use: [ext-messenger-test](https://github.com/asimen1/ext-messenger-test). 180 | 181 | ### License 182 | 183 | MIT 184 | -------------------------------------------------------------------------------- /src/backgroundHub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Utils from './utils.js'; 4 | import Constants from './constants.js'; 5 | 6 | // Randomly selected unique id to use as keys for the background ports object. 7 | const BACKGROUND_PORT_UID_KEY = 1; 8 | 9 | class BackgroundHub { 10 | constructor(options = {}) { 11 | Utils.constructorTweakMethods('BackgroundHub', this); 12 | 13 | this._connectedHandler = options.connectedHandler; 14 | this._disconnectedHandler = options.disconnectedHandler; 15 | 16 | // Hold all ports created with unique ids as keys (usually tabId, except background). 17 | this._backgroundPorts = {}; 18 | this._contentScriptPorts = {}; 19 | this._popupPorts = {}; 20 | this._devtoolPorts = {}; 21 | 22 | // Listen to port connections. 23 | chrome.runtime.onConnect.addListener(this._onPortConnected); 24 | self.mockPortOnConnect = this._onPortConnected; 25 | } 26 | 27 | _onPortConnected(port) { 28 | Utils.log('log', '[BackgroundHub:runtime.onConnect]', arguments); 29 | 30 | // Handle this port only if came from our API. 31 | if (port.name.indexOf(Constants.MESSENGER_PORT_NAME_PREFIX) === 0) { 32 | // Handle ALL incoming port messages. 33 | port.onMessage.addListener(this._onPortMessageHandler); 34 | 35 | // Cleanup on port disconnections, this takes care of all disconnections 36 | // (other extension parts create the connection with this port). 37 | port.onDisconnect.addListener(this._onPortDisconnectionHandler); 38 | } 39 | } 40 | 41 | _onPortMessageHandler(message, fromPort) { 42 | switch (message.type) { 43 | case Constants.INIT: { 44 | this._initConnection(message, fromPort); 45 | 46 | break; 47 | } 48 | 49 | // This cases our similar except the actual handling. 50 | case Constants.MESSAGE: 51 | case Constants.RESPONSE: { 52 | // Validate input. 53 | if (!message.to) { Utils.log('error', '[BackgroundHub:_onPortMessageHandler]', 'Missing "to" in message:', message); } 54 | if (!message.toNames) { Utils.log('error', '[BackgroundHub:_onPortMessageHandler]', 'Missing "toNames" in message:', message); } 55 | 56 | // Background hub always acts as a relay of messages to appropriate connection. 57 | this._relayMessage(message, fromPort); 58 | 59 | break; 60 | } 61 | 62 | default: { 63 | Utils.log('error', '[BackgroundHub:_onPortMessageHandler]', 'Unknown message type: ' + message.type); 64 | } 65 | } 66 | } 67 | 68 | _getPortsObj(extensionPart) { 69 | switch (extensionPart) { 70 | case Constants.BACKGROUND: 71 | return this._backgroundPorts; 72 | case Constants.CONTENT_SCRIPT: 73 | return this._contentScriptPorts; 74 | case Constants.POPUP: 75 | return this._popupPorts; 76 | case Constants.DEVTOOL: 77 | return this._devtoolPorts; 78 | default: 79 | Utils.log('error', '[BackgroundHub:_onPortDisconnectionHandler]', 'Unknown extension part: ' + extensionPart); 80 | } 81 | } 82 | 83 | _initConnection(message, fromPort) { 84 | let doInit = function(extensionPart, id) { 85 | let portsObj = this._getPortsObj(extensionPart); 86 | 87 | portsObj[id] = portsObj[id] ? portsObj[id] : []; 88 | portsObj[id].push(fromPort); 89 | 90 | // Invoke the user connected handler if given. 91 | if (this._connectedHandler) { 92 | let tabId = extensionPart !== Constants.BACKGROUND ? id : null; 93 | let userPortName = Utils.removeMessengerPortNamePrefix(fromPort.name); 94 | this._connectedHandler(extensionPart, userPortName, tabId); 95 | } 96 | 97 | // Send the init success message back to the sender port. 98 | fromPort.postMessage({ from: Constants.BACKGROUND, type: Constants.INIT_SUCCESS }); 99 | }.bind(this); 100 | 101 | if (message.from === Constants.BACKGROUND) { 102 | doInit(Constants.BACKGROUND, BACKGROUND_PORT_UID_KEY); 103 | } else if (message.from === Constants.DEVTOOL) { 104 | doInit(Constants.DEVTOOL, message.tabId); 105 | } else if (message.from === Constants.CONTENT_SCRIPT) { 106 | doInit(Constants.CONTENT_SCRIPT, fromPort.sender.tab.id); 107 | } else if (message.from === Constants.POPUP) { 108 | doInit(Constants.POPUP , message.tabId); 109 | } else { 110 | throw new Error('Unknown "from" in message: ' + message.from); 111 | } 112 | } 113 | 114 | _relayMessage(message, fromPort) { 115 | let from = message.from; 116 | let to = message.to; 117 | let toNames = message.toNames; 118 | 119 | // Will have value only for messages from background to other parts. 120 | let toTabId = message.toTabId; 121 | 122 | // Get the tab id of sender (not relevant in case sender is background to background). 123 | let tabId; 124 | if (from === Constants.BACKGROUND) { 125 | // With background to background messages, tabId is not relevant/necessary. 126 | if (to !== Constants.BACKGROUND) { 127 | tabId = toTabId; 128 | } 129 | } else if (from === Constants.DEVTOOL) { 130 | tabId = message.tabId; 131 | } else if (from === Constants.POPUP) { 132 | tabId = message.tabId; 133 | } else if (from === Constants.CONTENT_SCRIPT) { 134 | tabId = fromPort.sender.tab.id; 135 | } else { 136 | Utils.log('error', '[BackgroundHub:_relayMessage]', 'Unknown "from" in message: ' + from); 137 | } 138 | 139 | // Note: Important to store this on the message for responses from background which require the original tab id. 140 | message.fromTabId = tabId; 141 | 142 | // Get all connections ports according extension part. 143 | // NOTE: Port might not exist, it can happen when: 144 | // NOTE: - devtool window is not open. 145 | // NOTE: - content_script is not running because the page is of chrome:// type. 146 | let toPorts; 147 | if (to === Constants.BACKGROUND) { 148 | toPorts = this._backgroundPorts[BACKGROUND_PORT_UID_KEY] ? this._backgroundPorts[BACKGROUND_PORT_UID_KEY] : []; 149 | } else if (to === Constants.DEVTOOL) { 150 | toPorts = this._devtoolPorts[tabId] ? this._devtoolPorts[tabId] : []; 151 | } else if (to === Constants.POPUP) { 152 | toPorts = this._popupPorts[tabId] ? this._popupPorts[tabId] : []; 153 | } else if (to === Constants.CONTENT_SCRIPT) { 154 | toPorts = this._contentScriptPorts[tabId] ? this._contentScriptPorts[tabId] : []; 155 | } else { 156 | Utils.log('error', '[BackgroundHub:_relayMessage]', 'Unknown "to" in message: ' + to); 157 | } 158 | 159 | // Logging... 160 | if (toPorts.length === 0) { Utils.log('info', '[BackgroundHub:_relayMessage]', 'Not sending relay because "to" port does not exist'); } 161 | 162 | // Go over names and find all matching ports. 163 | let matchingToPorts = []; 164 | toNames.forEach(function(toName) { 165 | let matchedPorts = toPorts.filter(function(toPort) { 166 | return toPort.name === toName || toName === Constants.TO_NAME_WILDCARD; 167 | }); 168 | 169 | if (matchedPorts.length > 0) { 170 | matchedPorts.forEach(function(matchedPort) { 171 | // Make sure to keep matching to ports unique in case someone gave both names and wildcard. 172 | if (matchingToPorts.indexOf(matchedPort) === -1) { 173 | matchingToPorts.push(matchedPort); 174 | } 175 | }); 176 | } else { 177 | Utils.log('warn', '[BackgroundHub:_relayMessage]', 'Could not find any connections with this name (probably no such name):', to, Utils.removeMessengerPortNamePrefix(toName)); 178 | } 179 | }.bind(this)); 180 | 181 | // NOTE: We store this on the message so it won't get lost when relying. 182 | message.fromPortSender = fromPort.sender; 183 | 184 | // Send the message/s. 185 | matchingToPorts.forEach(function(matchingToPort) { 186 | matchingToPort.postMessage(message); 187 | }.bind(this)); 188 | } 189 | 190 | _onPortDisconnectionHandler(disconnectedPort) { 191 | // Remove our message listener. 192 | disconnectedPort.onMessage.removeListener(this._onPortMessageHandler); 193 | 194 | let removePort = function(extensionPart, disconnectedPort) { 195 | let portsObj = this._getPortsObj(extensionPart); 196 | 197 | // NOTE: portKeys is usually the tab ids (except for background). 198 | let portKeys = Object.keys(portsObj); 199 | for (let i = 0; i < portKeys.length; i++) { 200 | let currPortKey = portKeys[i]; 201 | 202 | // Remove according matching port, traverse backward to be able to remove them on th go. 203 | let portsArr = portsObj[currPortKey]; 204 | let portsArrLength = portsArr.length; 205 | for (let j = portsArrLength; j >= 0; j--) { 206 | let port = portsArr[j]; 207 | if (port === disconnectedPort) { 208 | Utils.log('log', '[BackgroundHub:_onPortDisconnectionHandler]', 'Remove connection of port with unique id: ', currPortKey); 209 | portsArr.splice(j, 1); 210 | 211 | // Invoke the user disconnected handler if given. 212 | if (this._disconnectedHandler) { 213 | // Lets pass the tab id for which this port was working for 214 | // (and not the devtool sender tab id which is "-1"). 215 | // NOTE: Background ports are not identified by tab ids. 216 | // NOTE: parseInt required since the object keys are strings. 217 | let tabId = extensionPart !== Constants.BACKGROUND ? parseInt(currPortKey) : null; 218 | let userPortName = Utils.removeMessengerPortNamePrefix(disconnectedPort.name); 219 | this._disconnectedHandler(extensionPart, userPortName, tabId); 220 | } 221 | } 222 | } 223 | 224 | // If all ports removed, remove it from our stored ports object and invoke disconnect handler if given. 225 | if (portsObj[currPortKey].length === 0) { 226 | Utils.log('log', '[BackgroundHub:_onPortDisconnectionHandler]', 'Removing empty ports object for unique id: ', currPortKey); 227 | delete portsObj[currPortKey]; 228 | } 229 | } 230 | }.bind(this); 231 | 232 | // Attempt to remove from all our stored ports. 233 | removePort(Constants.BACKGROUND, disconnectedPort); 234 | removePort(Constants.CONTENT_SCRIPT, disconnectedPort); 235 | removePort(Constants.POPUP, disconnectedPort); 236 | removePort(Constants.DEVTOOL, disconnectedPort); 237 | } 238 | } 239 | 240 | export default BackgroundHub; -------------------------------------------------------------------------------- /dist/ext-messenger.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("ext-messenger",[],t):"object"==typeof exports?exports["ext-messenger"]=t():e["ext-messenger"]=t()}(self,(()=>(()=>{"use strict";var e={d:(t,n)=>{for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{default:()=>y});const n="__messenger__",o="*",s="background",r="popup",i="devtool",a="content_script",c="init",d="init_success",g="message",l="response",u=["log","info","warn","error"],h="[ext-messenger]",_="",m="";function p(e){let t=Array.prototype.slice.call(arguments,1);var n;if(n=e,u.indexOf(n)>=u.indexOf("warn"))switch(e){case"log":console.log(_+h+` [${e}]`,...t);break;case"info":console.info(m+h+` [${e}]`,...t);break;case"warn":console.warn(h,...t);break;case"error":throw console.error(h,...t),"[ext-messenger] error occurred, check more information above...";default:console.error(h,`Unknown log level: ${e}`)}}const f={log:p,constructorTweakMethods:function(e,t){let n=function(n){let o=t[n];t[n]=function(){return p("log",`[${e}:${n}()]`,arguments),o.apply(t,arguments)}.bind(t)};for(let e in t)"function"==typeof t[e]&&n(e);Object.getOwnPropertyNames(Object.getPrototypeOf(t)).filter((e=>"function"==typeof t[e]&&"constructor"!==e)).forEach((e=>{t[e]=t[e].bind(t)}))},removeMessengerPortNamePrefix:function(e){return e.replace(new RegExp("^"+n),"")}},b=class{constructor(e={}){f.constructorTweakMethods("BackgroundHub",this),this._connectedHandler=e.connectedHandler,this._disconnectedHandler=e.disconnectedHandler,this._backgroundPorts={},this._contentScriptPorts={},this._popupPorts={},this._devtoolPorts={},chrome.runtime.onConnect.addListener(this._onPortConnected),self.mockPortOnConnect=this._onPortConnected}_onPortConnected(e){f.log("log","[BackgroundHub:runtime.onConnect]",arguments),0===e.name.indexOf(n)&&(e.onMessage.addListener(this._onPortMessageHandler),e.onDisconnect.addListener(this._onPortDisconnectionHandler))}_onPortMessageHandler(e,t){switch(e.type){case c:this._initConnection(e,t);break;case g:case l:e.to||f.log("error","[BackgroundHub:_onPortMessageHandler]",'Missing "to" in message:',e),e.toNames||f.log("error","[BackgroundHub:_onPortMessageHandler]",'Missing "toNames" in message:',e),this._relayMessage(e,t);break;default:f.log("error","[BackgroundHub:_onPortMessageHandler]","Unknown message type: "+e.type)}}_getPortsObj(e){switch(e){case s:return this._backgroundPorts;case a:return this._contentScriptPorts;case r:return this._popupPorts;case i:return this._devtoolPorts;default:f.log("error","[BackgroundHub:_onPortDisconnectionHandler]","Unknown extension part: "+e)}}_initConnection(e,t){let n=function(e,n){let o=this._getPortsObj(e);if(o[n]=o[n]?o[n]:[],o[n].push(t),this._connectedHandler){let o=e!==s?n:null,r=f.removeMessengerPortNamePrefix(t.name);this._connectedHandler(e,r,o)}t.postMessage({from:s,type:d})}.bind(this);if(e.from===s)n(s,1);else if(e.from===i)n(i,e.tabId);else if(e.from===a)n(a,t.sender.tab.id);else{if(e.from!==r)throw new Error('Unknown "from" in message: '+e.from);n(r,e.tabId)}}_relayMessage(e,t){let n,c,d=e.from,g=e.to,l=e.toNames,u=e.toTabId;d===s?g!==s&&(n=u):d===i||d===r?n=e.tabId:d===a?n=t.sender.tab.id:f.log("error","[BackgroundHub:_relayMessage]",'Unknown "from" in message: '+d),e.fromTabId=n,g===s?c=this._backgroundPorts[1]?this._backgroundPorts[1]:[]:g===i?c=this._devtoolPorts[n]?this._devtoolPorts[n]:[]:g===r?c=this._popupPorts[n]?this._popupPorts[n]:[]:g===a?c=this._contentScriptPorts[n]?this._contentScriptPorts[n]:[]:f.log("error","[BackgroundHub:_relayMessage]",'Unknown "to" in message: '+g),0===c.length&&f.log("info","[BackgroundHub:_relayMessage]",'Not sending relay because "to" port does not exist');let h=[];l.forEach(function(e){let t=c.filter((function(t){return t.name===e||e===o}));t.length>0?t.forEach((function(e){-1===h.indexOf(e)&&h.push(e)})):f.log("warn","[BackgroundHub:_relayMessage]","Could not find any connections with this name (probably no such name):",g,f.removeMessengerPortNamePrefix(e))}.bind(this)),e.fromPortSender=t.sender,h.forEach(function(t){t.postMessage(e)}.bind(this))}_onPortDisconnectionHandler(e){e.onMessage.removeListener(this._onPortMessageHandler);let t=function(e,t){let n=this._getPortsObj(e),o=Object.keys(n);for(let r=0;r=0;n--)if(a[n]===t&&(f.log("log","[BackgroundHub:_onPortDisconnectionHandler]","Remove connection of port with unique id: ",i),a.splice(n,1),this._disconnectedHandler)){let n=e!==s?parseInt(i):null,o=f.removeMessengerPortNamePrefix(t.name);this._disconnectedHandler(e,o,n)}0===n[i].length&&(f.log("log","[BackgroundHub:_onPortDisconnectionHandler]","Removing empty ports object for unique id: ",i),delete n[i])}}.bind(this);t(s,e),t(a,e),t(r,e),t(i,e)}},P=class{constructor(e){f.constructorTweakMethods("MockPort",this);let t=this._createMockPort(e),n=this._createMockPort(e);return this._linkMocks(t,n),self&&"function"==typeof self.mockPortOnConnect&&self.mockPortOnConnect(n),t}_createMockPort(e){let t={_connected:!0,_name:e.name,onMessageListeners:[],onDisconnectListeners:[]};return Object.defineProperty(t,"name",{get:function(){return t._name}}),Object.defineProperty(t,"onMessage",{get:function(){return{addListener:function(e){t.onMessageListeners.push(e)},removeListener:function(e){let n=t.onMessageListeners.indexOf(e);-1!==n&&t.onMessageListeners.splice(n,1)}}}}),Object.defineProperty(t,"onDisconnect",{get:function(){return{addListener:function(e){t.onDisconnectListeners.push(e)},removeListener:function(e){let n=t.onDisconnectListeners.indexOf(e);-1!==n&&t.onDisconnectListeners.splice(n,1)}}}}),Object.defineProperty(t,"sender",{get:function(){return{id:chrome.runtime.id}}}),t.postMessage=function(e){t._connected?t.__targetRefPort?t.__targetRefPort.__invokeOnMessageHandlers(e):f.log("warn","[MockPort:postMessage]","Missing __targetRefPort",arguments):f.log("warn","[MockPort:postMessage]","Attempting to post message on a disconnected mock port",e)},t.disconnect=function(){t._connected=!1,t.__targetRefPort?t.__targetRefPort.__invokeOnDisconnectHandlers():f.log("warn","[MockPort:postMessage]","Missing __targetRefPort",arguments),t._onMessageListeners=[],t._onDisconnectListeners=[]},t.__invokeOnMessageHandlers=function(e){t.onMessageListeners.forEach((function(n){n(e,t)}))},t.__invokeOnDisconnectHandlers=function(){t.onDisconnectListeners.forEach((function(e){e(t)}))},t}_linkMocks(e,t){e.__targetRefPort=t,t.__targetRefPort=e}},M=class{constructor(e,t,n){return f.constructorTweakMethods("Connection",this),this.extPart=e,this.name=t,this._init(this.extPart,this.name,n),{extPart:this.extPart,name:this.name,sendMessage:this.sendMessage,disconnect:this.disconnect}}_init(e,t,o){switch(this._port=null,this._inited=!1,this._pendingInitMessages=[],this._pendingCb={},this._cbId=0,this._pendingCbCleanupIndex=0,this._myExtPart=e,this._myName=n+t,this._userMessageHandler=o||function(){},this._myExtPart){case s:case a:case r:case i:{let e=function(t){f.log("log","[Connection:_init]","Attempting connection initing..."),this._port=this._myExtPart===s?new P({name:this._myName}):chrome.runtime.connect({name:this._myName}),this._port.onMessage.addListener(this._onPortMessageHandler),this._port.postMessage({type:c,from:this._myExtPart,tabId:t});let n=arguments,o=setTimeout(function(){this._inited?clearTimeout(o):(this._port.disconnect(),e.apply(this,n))}.bind(this),500)}.bind(this);switch(this._myExtPart){case s:case a:e();break;case r:chrome.tabs.query({active:!0,currentWindow:!0},(function(t){e(t[0].id)}));break;case i:e(chrome.devtools.inspectedWindow.tabId)}break}default:f.log("error","[Connection:_init]","Unknown extension part: "+e)}}_attemptDeadCbCleanup(){if(Object.keys(this._pendingCb).length>1e5){f.log("log","[Connection:_attemptDeadCbCleanup]","Attempting dead callback cleaning... current callbacks number:".Object.keys(this._pendingCb).length);let e=this._pendingCbCleanupIndex+5e3;for(;this._pendingCbCleanupIndex{switch(t&&(this._cbId++,this._pendingCb[this._cbId]=t,e.cbId=this._cbId,this._attemptDeadCbCleanup()),this._myExtPart){case i:e.tabId=chrome.devtools.inspectedWindow.tabId,n();break;case r:chrome.tabs.query({active:!0,currentWindow:!0},function(t){e.tabId=t[0].id,n()}.bind(this));break;default:n()}}))}_postMessage(e,t,n){this._prepareMessage(t,n).then((()=>{this._inited?e.postMessage(t):this._pendingInitMessages.push(t)}))}_postResponse(e,t,n){let o={from:this._myExtPart,to:n.from,toNames:[n.fromName],type:l,cbId:n.cbId,cbValue:t};this._myExtPart===s&&(o.toTabId=n.fromTabId),this._postMessage(e,o)}_handleMessage(e,t){let n=function(n){e.cbId&&this._postResponse(t,n,e)}.bind(this),o=f.removeMessengerPortNamePrefix(e.fromName),r=e.fromTabId&&e.from!==s?`:${e.fromTabId}`:null,i=`${e.from}:${o}`+(r?`:${r}`:"");this._userMessageHandler(e.userMessage,i,e.fromPortSender,n)}_handleResponse(e){if(this._pendingCb[e.cbId]){let t=this._pendingCb[e.cbId];delete this._pendingCb[e.cbId],t(e.cbValue)}else f.log("info","[Connection:_handleResponse]","Ignoring response sending because callback does not exist (probably already been called)")}_sendMessage(e,t,n,o,s,r){n=this._addMessengerPortNamePrefix(n);let i={from:this._myExtPart,fromName:this._myName,to:t,toNames:n,toTabId:o,type:g,userMessage:s};this._postMessage(e,i,r)}_addMessengerPortNamePrefix(e){return e.map((function(e){return e===o?e:n+e}))}_validateMessage(e,t,n){if(!e)return'Missing extension part in "to" argument';if(e!==s&&e!==a&&e!==i&&e!==r)return'Unknown extension part in "to" argument: '+e+"\nSupported parts are: "+s+", "+a+", "+r+", "+i;if(!t)return'Missing connection name in "to" argument';if(this._myExtPart===s&&e!==s){if(!n)return'Messages from background to other extension parts must have a tab id in "to" argument';if(!Number.isInteger(parseFloat(n)))return"Tab id to send message to must be a valid number"}}_onPortMessageHandler(e,t){switch(e.type){case d:this._inited=!0,this._pendingInitMessages.forEach(function(e){this._port.postMessage(e)}.bind(this));break;case g:case l:e.to||f.log("error","[Connection:_onPortMessageHandler]",'Missing "to" in message: ',e),e.toNames||f.log("error","[Connection:_onPortMessageHandler]",'Missing "toNames" in message: ',e),e.type===g?this._handleMessage(e,t):e.type===l&&this._handleResponse(e);break;default:f.log("error","[Connection:_onPortMessageHandler]","Unknown message type: "+e.type)}}sendMessage(e,t){return new Promise(((n,o)=>{if(e||f.log("error","[Connection:sendMessage]",'Missing "to" arguments'),!this._port)return f.log("info","[Connection:sendMessage]","Rejecting sendMessage because connection does not exist anymore"),o(new Error("Connection port does not exist anymore, did you disconnect it?"));let s;try{s=e.split(":")}catch(t){f.log("error","[Connection:sendMessage]",'Invalid format given in "to" argument: '+e,arguments)}let r=s[0],i=s[1],a=s[2],c=this._validateMessage(r,i,a);c&&f.log("error","[Connection:sendMessage]",c,arguments);let d=i.split(",");this._sendMessage(this._port,r,d,a,t,n)}))}disconnect(){this._port&&(this._port.disconnect(),this._port=null)}};class k{constructor(e){if(f.constructorTweakMethods("Messenger",this),!Object.values(k.EXT_PARTS).includes(e))return void f.log("error","[Messenger:constructor]",`"${e}" provided is not a valid extension part. Valid parts are: ${Object.keys(k.EXT_PARTS).join(", ")}`);this._myExtPart=e;let t={initConnection:this.initConnection};return this._myExtPart===s&&(t.initBackgroundHub=this.initBackgroundHub),t}static isMessengerPort(e){return 0===e.name.indexOf(n)}static EXT_PARTS={BACKGROUND:s,POPUP:r,DEVTOOL:i,CONTENT_SCRIPT:a};initBackgroundHub(e){this._myExtPart===s?this._backgroundHub?f.log("warn","[Messenger:initBackgroundHub]","Ignoring BackgroundHub init request since it is already been inited"):this._backgroundHub=new b(e):f.log("warn","[Messenger:initBackgroundHub]","Ignoring BackgroundHub init request since not called from background context")}initConnection(e,t){return e||f.log("error","[Messenger:initConnection]",'Missing "name" in arguments'),e===o&&f.log("error","[Messenger:initConnection]",'"*" is reserved as a wildcard identifier, please use another name'),new M(this._myExtPart,e,t)}}const y=k;return t})())); 2 | //# sourceMappingURL=ext-messenger.min.js.map -------------------------------------------------------------------------------- /src/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import MockPort from './mockPort.js'; 4 | import Utils from './utils.js'; 5 | import Constants from './constants.js'; 6 | 7 | const INIT_CONNECTION_INTERVAL = 500; 8 | 9 | const PENDING_CB_SIZE_CLEANUP_TRIGGER = 100000; 10 | const PENDING_CB_SIZE_CLEANUP_AMOUNT = 5000; 11 | 12 | class Connection { 13 | constructor(extPart, name, messageHandler) { 14 | Utils.constructorTweakMethods('Connection', this); 15 | 16 | this.extPart = extPart; 17 | this.name = name; 18 | 19 | this._init(this.extPart, this.name, messageHandler); 20 | 21 | return { 22 | extPart: this.extPart, 23 | name: this.name, 24 | sendMessage: this.sendMessage, 25 | disconnect: this.disconnect, 26 | }; 27 | } 28 | 29 | _init(extPart, name, messageHandler) { 30 | this._port = null; 31 | this._inited = false; 32 | this._pendingInitMessages = []; 33 | this._pendingCb = {}; 34 | this._cbId = 0; 35 | this._pendingCbCleanupIndex = 0; 36 | 37 | this._myExtPart = extPart; 38 | this._myName = Constants.MESSENGER_PORT_NAME_PREFIX + name; 39 | this._userMessageHandler = messageHandler || function() {}; 40 | 41 | switch (this._myExtPart) { 42 | case Constants.BACKGROUND: 43 | case Constants.CONTENT_SCRIPT: 44 | case Constants.POPUP: 45 | case Constants.DEVTOOL: { 46 | let doInitConnection = function(tabId) { 47 | Utils.log('log', '[Connection:_init]', 'Attempting connection initing...'); 48 | 49 | this._port = this._myExtPart === Constants.BACKGROUND 50 | ? new MockPort({ name: this._myName }) 51 | : chrome.runtime.connect({ name: this._myName }); 52 | 53 | this._port.onMessage.addListener(this._onPortMessageHandler); 54 | 55 | this._port.postMessage({ 56 | type: Constants.INIT, 57 | from: this._myExtPart, 58 | tabId: tabId, 59 | }); 60 | 61 | // NOTE: The init connection from the extension parts can be called before the 62 | // NOTE: background hub has inited and started listening to connections. 63 | // NOTE: Retry init until we get the init success response from the background. 64 | // TODO: maybe can think of a better solution? 65 | let argsArr = arguments; 66 | let initInterval = setTimeout(function() { 67 | if (!this._inited) { 68 | this._port.disconnect(); 69 | doInitConnection.apply(this, argsArr); 70 | } else { 71 | clearTimeout(initInterval); 72 | } 73 | }.bind(this), INIT_CONNECTION_INTERVAL); 74 | }.bind(this); 75 | 76 | // Unlike content script which have the tab id in the "sender" object, 77 | // for devtool/popup we need to get and pass the tab id ourself. 78 | // NOTE: For background connection we don't have a notion of tab id. 79 | switch (this._myExtPart) { 80 | case Constants.BACKGROUND: 81 | case Constants.CONTENT_SCRIPT: 82 | doInitConnection(); 83 | 84 | break; 85 | case Constants.POPUP: 86 | chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { 87 | doInitConnection(tabs[0].id); 88 | }); 89 | 90 | break; 91 | case Constants.DEVTOOL: 92 | doInitConnection(chrome.devtools.inspectedWindow.tabId); 93 | 94 | break; 95 | } 96 | 97 | break; 98 | } 99 | 100 | default: { 101 | Utils.log('error', '[Connection:_init]', 'Unknown extension part: ' + extPart); 102 | } 103 | } 104 | } 105 | 106 | // Pending callback will get populated by unresponded callbacks. 107 | // Clean up at sensible sizes. 108 | _attemptDeadCbCleanup() { 109 | if (Object.keys(this._pendingCb).length > PENDING_CB_SIZE_CLEANUP_TRIGGER) { 110 | Utils.log('log', '[Connection:_attemptDeadCbCleanup]', 'Attempting dead callback cleaning... current callbacks number:'. Object.keys(this._pendingCb).length); 111 | 112 | let cleanUpToIndex = this._pendingCbCleanupIndex + PENDING_CB_SIZE_CLEANUP_AMOUNT; 113 | while (this._pendingCbCleanupIndex < cleanUpToIndex) { 114 | delete this._pendingCb[this._pendingCbCleanupIndex]; 115 | this._pendingCbCleanupIndex++; 116 | } 117 | 118 | Utils.log('log', '[Connection:_attemptDeadCbCleanup]', 'New callbacks number after cleaning done:', Object.keys(this._pendingCb).length); 119 | } 120 | } 121 | 122 | _prepareMessage(message, cbPromiseResolve) { 123 | return new Promise((resolve) => { 124 | // Handle callback if given. 125 | if (cbPromiseResolve) { 126 | this._cbId++; 127 | this._pendingCb[this._cbId] = cbPromiseResolve; 128 | message.cbId = this._cbId; 129 | 130 | this._attemptDeadCbCleanup(); 131 | } 132 | 133 | // Manually setting the "tabId" is important for relay for some extension parts... 134 | switch (this._myExtPart) { 135 | case Constants.DEVTOOL: { 136 | message.tabId = chrome.devtools.inspectedWindow.tabId; 137 | resolve(); 138 | 139 | break; 140 | } 141 | 142 | case Constants.POPUP: { 143 | chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { 144 | message.tabId = tabs[0].id; 145 | resolve(); 146 | }.bind(this)); 147 | 148 | break; 149 | } 150 | 151 | default: { 152 | resolve(); 153 | break; 154 | } 155 | } 156 | }); 157 | } 158 | 159 | // Generic post message with callback support. 160 | _postMessage(port, message, cbPromiseResolve) { 161 | this._prepareMessage(message, cbPromiseResolve).then(() => { 162 | if (this._inited) { 163 | port.postMessage(message); 164 | } else { 165 | this._pendingInitMessages.push(message); 166 | } 167 | }); 168 | } 169 | 170 | _postResponse(fromPort, responseValue, origMessage) { 171 | let response = { 172 | from: this._myExtPart, 173 | to: origMessage.from, 174 | 175 | // BackgroundHub expects toName to be an array. 176 | toNames: [origMessage.fromName], 177 | 178 | type: Constants.RESPONSE, 179 | cbId: origMessage.cbId, 180 | cbValue: responseValue, 181 | }; 182 | 183 | // If we are in the background, we need to specify the tab id to respond to. 184 | if (this._myExtPart === Constants.BACKGROUND) { 185 | response.toTabId = origMessage.fromTabId; 186 | } 187 | 188 | this._postMessage(fromPort, response); 189 | } 190 | 191 | _handleMessage(message, fromPort) { 192 | // Create the "sendResponse" callback for the message. 193 | let sendResponse = function(response) { 194 | // Message has callback... respond to it. 195 | if (message.cbId) { 196 | this._postResponse(fromPort, response, message); 197 | } 198 | }.bind(this); 199 | 200 | // Construct the from string (the sender's "to" string). 201 | // NOTE: Background connections have "fromTabId" only for the relay of response and should not be added. 202 | let fromName = Utils.removeMessengerPortNamePrefix(message.fromName); 203 | let fromTabId = (message.fromTabId && message.from !== Constants.BACKGROUND) ? `:${message.fromTabId}` : null; 204 | let from = `${message.from}:${fromName}` + (fromTabId ? `:${fromTabId}` : ''); 205 | 206 | // Invoke the user message handler. 207 | this._userMessageHandler(message.userMessage, from, message.fromPortSender, sendResponse); 208 | } 209 | 210 | _handleResponse(response) { 211 | if (this._pendingCb[response.cbId]) { 212 | let cbPromiseResolve = this._pendingCb[response.cbId]; 213 | delete this._pendingCb[response.cbId]; 214 | 215 | // Resolve the promise with the response callback value. 216 | cbPromiseResolve(response.cbValue); 217 | } else { 218 | Utils.log('info', '[Connection:_handleResponse]', 'Ignoring response sending because callback does not exist (probably already been called)'); 219 | } 220 | } 221 | 222 | _sendMessage(port, toExtPart, toNames, toTabId, userMessage, cbPromiseResolve) { 223 | // Add our port name prefix to the user given name (if given and not wildcard). 224 | toNames = this._addMessengerPortNamePrefix(toNames); 225 | 226 | let message = { 227 | from: this._myExtPart, 228 | fromName: this._myName, 229 | to: toExtPart, 230 | toNames: toNames, 231 | toTabId: toTabId, 232 | type: Constants.MESSAGE, 233 | userMessage: userMessage, 234 | }; 235 | 236 | this._postMessage(port, message, cbPromiseResolve); 237 | } 238 | 239 | _addMessengerPortNamePrefix(toNames) { 240 | return toNames.map(function(toName) { 241 | // Wildcards '*' should stay intact. 242 | return toName === Constants.TO_NAME_WILDCARD ? toName : Constants.MESSENGER_PORT_NAME_PREFIX + toName; 243 | }); 244 | } 245 | 246 | _validateMessage(toExtPart, toName, toTabId) { 247 | if (!toExtPart) { 248 | return 'Missing extension part in "to" argument'; 249 | } 250 | 251 | if (toExtPart !== Constants.BACKGROUND && toExtPart !== Constants.CONTENT_SCRIPT && toExtPart !== Constants.DEVTOOL && toExtPart !== Constants.POPUP) { 252 | return 'Unknown extension part in "to" argument: ' + toExtPart + '\nSupported parts are: ' + Constants.BACKGROUND + ', ' + Constants.CONTENT_SCRIPT + ', ' + Constants.POPUP + ', ' + Constants.DEVTOOL; 253 | } 254 | 255 | if (!toName) { 256 | return 'Missing connection name in "to" argument'; 257 | } 258 | 259 | if (this._myExtPart === Constants.BACKGROUND && toExtPart !== Constants.BACKGROUND) { 260 | if (!toTabId) { 261 | return 'Messages from background to other extension parts must have a tab id in "to" argument'; 262 | } 263 | 264 | if (!Number.isInteger(parseFloat(toTabId))) { 265 | return 'Tab id to send message to must be a valid number'; 266 | } 267 | } 268 | } 269 | 270 | _onPortMessageHandler(message, fromPort) { 271 | switch (message.type) { 272 | case Constants.INIT_SUCCESS: { 273 | this._inited = true; 274 | 275 | // Handle all the pending messages added before init succeeded. 276 | this._pendingInitMessages.forEach(function(pendingInitMessage) { 277 | this._port.postMessage(pendingInitMessage); 278 | }.bind(this)); 279 | 280 | break; 281 | } 282 | 283 | // This cases our similar except the actual handling. 284 | case Constants.MESSAGE: 285 | case Constants.RESPONSE: { 286 | if (!message.to) { Utils.log('error', '[Connection:_onPortMessageHandler]', 'Missing "to" in message: ', message); } 287 | if (!message.toNames) { Utils.log('error', '[Connection:_onPortMessageHandler]', 'Missing "toNames" in message: ', message); } 288 | 289 | // If we got a message/response it means the background hub has already 290 | // decided that we should handle it. 291 | if (message.type === Constants.MESSAGE) { 292 | this._handleMessage(message, fromPort); 293 | } else if (message.type === Constants.RESPONSE) { 294 | this._handleResponse(message); 295 | } 296 | 297 | break; 298 | } 299 | 300 | default: { 301 | Utils.log('error', '[Connection:_onPortMessageHandler]', 'Unknown message type: ' + message.type); 302 | } 303 | } 304 | } 305 | 306 | // ------------------------------------------------------------ 307 | // Exposed API 308 | // ------------------------------------------------------------ 309 | 310 | sendMessage(to, message) { 311 | // Always returns a promise (callback support). 312 | return new Promise((cbPromiseResolve, reject) => { 313 | if (!to) { Utils.log('error', '[Connection:sendMessage]', 'Missing "to" arguments'); } 314 | 315 | if (!this._port) { 316 | Utils.log('info', '[Connection:sendMessage]', 'Rejecting sendMessage because connection does not exist anymore'); 317 | return reject(new Error('Connection port does not exist anymore, did you disconnect it?')); 318 | } 319 | 320 | // Parse 'to' to args... for example => 'devtool:main:1225' 321 | let toArgs; 322 | try { 323 | toArgs = to.split(':'); 324 | } catch (e) { 325 | Utils.log('error', '[Connection:sendMessage]', 'Invalid format given in "to" argument: ' + to, arguments); 326 | } 327 | 328 | let toExtPart = toArgs[0]; 329 | let toName = toArgs[1]; 330 | let toTabId = toArgs[2]; 331 | 332 | // Validate (will throw error if something is invalid). 333 | let errorMsg = this._validateMessage(toExtPart, toName, toTabId); 334 | if (errorMsg) { Utils.log('error', '[Connection:sendMessage]', errorMsg, arguments); } 335 | 336 | // Normalize to array to support multiple names. 337 | let toNames = toName.split(','); 338 | 339 | this._sendMessage(this._port, toExtPart, toNames, toTabId, message, cbPromiseResolve); 340 | }); 341 | } 342 | 343 | disconnect() { 344 | if (this._port) { 345 | this._port.disconnect(); 346 | this._port = null; 347 | } 348 | } 349 | } 350 | 351 | export default Connection; --------------------------------------------------------------------------------