├── .gitignore ├── LICENSE.txt ├── README.md ├── build └── queuebert.min.js ├── example ├── background.html └── content-script.js ├── images └── queuebert.png ├── lib ├── background-client.js ├── client.js └── server.js ├── manifest.json ├── package.json └── test ├── background-client-test.js ├── client-test.js └── server-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Attachments.me 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Queuebert 2 | ========= 3 | 4 |  5 | 6 | The Chrome extension paradigm is sandboxed. iframes and browser-tabs cannot directly communicate with each other. They must use a background script to mediate communication between them. 7 | 8 | In a prior post, I advocate using a queue-based approach to deal with this annoyance: 9 | 10 | https://github.com/bcoe/DoloresLabsTechTalk 11 | 12 | Queuebert is a library designed to simplify building Chrome extensions using a queue-based approach. 13 | 14 | Server 15 | ------ 16 | 17 | * You create one instance of a server in the background.html. 18 | * The server handles queuing up messages for the content scripts. 19 | 20 | ```javascript 21 | var server = new Queuebert.Server(); 22 | ``` 23 | 24 | Client 25 | ------ 26 | 27 | You create a client within your content scripts. The client is used to communicate between the sandboxed JavaScript environments. 28 | 29 | * The client is used to dispatch messages to other tabs, iframes, and to the background script. 30 | * The client has a delegate registered with it. The delegate receives messages from other clients in the system. 31 | 32 | ```javascript 33 | var client = new Queuebert.Client({ 34 | identifier: 'client', 35 | delegate: delegate 36 | }); 37 | ``` 38 | * _identifier_ a tab with multiple iframes within it can potentially run multiple clients. The _identifier_ is used to differentiate these clients. 39 | * _delegate_ the client automatically checks for inbound messages on a set interval. Messages are automatically dispatched to the delegate. 40 | 41 | *Example Delegate* 42 | 43 | ```javascript 44 | var delegate = { 45 | receivedMessage: function(clientId, clientTabId, body) { 46 | console.log(body.message); 47 | } 48 | }; 49 | ``` 50 | 51 | The _receivedMessage_ message method will be executed if the following message was dispatched by another client: 52 | 53 | ```javascript 54 | client.sendMessage({ 55 | action: 'receivedMessage', 56 | tabId: clientTabId, 57 | to: 'client', 58 | body: {message: 'a message'} 59 | }); 60 | ``` 61 | 62 | The _clientId_ and _clientTabId_ variables can be used to dispatch a message back to the originating client. The _body_ is the body of the message dispatched by the originating client. 63 | 64 | BackgroundClient 65 | ---------------- 66 | 67 | To keep the queue-based paradigm consistent, you can create an instance of a _BackgroundClient_ in your _background.html_. 68 | 69 | The behaviour of a background client is identical to clients in content scripts except, seeing as the background script has no _tab.id_, background clients have the special _tab.id_ _background_. 70 | 71 | *Creating a BackgroundClient* 72 | 73 | ```javascript 74 | var client = new Queuebert.BackgroundClient({ 75 | delegate: delegate, 76 | server: server 77 | }); 78 | ``` 79 | 80 | * _delegate_ the delegate provided to a background client is identical to a delegate provided to other clients. 81 | * _server_ a BackgroundClient requires that the _Server_ instance be provided as a dependency. 82 | 83 | *Sending a Message to a BackgroundClient* 84 | 85 | ```javascript 86 | client.sendMessage({ 87 | action: 'echo', 88 | tabId: 'background', 89 | to: 'background', 90 | body: {message: 'Hello World!'} 91 | }); 92 | ``` 93 | 94 | The Example 95 | ----------- 96 | 97 | In the _/example_ folder, there are examples of each of the concepts discussed. 98 | 99 | The _Queuebert_ project is a fully functional Chrome Extension. Install it, to see things in action. 100 | 101 | Testing 102 | ------- 103 | 104 | Queuebert uses node.js for unit testing. run _node test/index.js_ to execute the test suite. 105 | 106 | Copyright 107 | --------- 108 | 109 | Copyright (c) 2011 Attachments.me. See LICENSE.txt for further details. -------------------------------------------------------------------------------- /build/queuebert.min.js: -------------------------------------------------------------------------------- 1 | if(typeof(Queuebert)==="undefined"){Queuebert={}}(function(){function a(b){var c=b||{};this.delegate=c.delegate||{};this.identifier=c.identifier;this.messageInterval=c.messageInterval||150;this.sendRequest=c.sendRequest||chrome.extension.sendRequest;this.setInterval=c.setInterval||function(e,d){setInterval(e,d)};if(c.start===undefined||c.start){this.start()}}a.prototype.start=function(){var b=this;this.setInterval(function(){b.sendRequest({from:b.identifier,type:"get"},function(c){if(c){b.handleMessage(c.action,c.from,c.tabId,c.body,c.callback)}})},this.messageInterval)};a.prototype.handleMessage=function(e,c,d,b,f){if(this.delegate[e]){this.delegate[e](c,d,b,f)}};a.prototype.sendMessage=function(c,d){var b={tabId:c.tabId,to:c.to,from:this.identifier,action:c.action,type:"put",body:c.body};d?this.sendRequest(b,d):this.sendRequest(b)};Queuebert.Client=a})();if(typeof(exports)!=="undefined"){exports.Client=Queuebert.Client}if(typeof(Queuebert)==="undefined"){Queuebert={}}if(!Queuebert.Client&&typeof(require)!=="undefined"&&typeof(exports)!=="undefined"){Queuebert.Client=require("./client").Client}(function(){function b(c){var d=c||{},e=this;this.delegate=d.delegate||{};this.identifier=d.identifier||"background";this.server=d.server;this.messageInterval=d.messageInterval||150;this.setInterval=d.setInterval||function(g,f){setInterval(g,f)};this.sendRequest=function(f,g){if(f.type=="get"){f.tabId="background"}e.server.handleMessage(f,{tab:{id:"background"}},g)};if(d.start===undefined||d.start){this.start()}}for(var a in Queuebert.Client.prototype){b.prototype[a]=Queuebert.Client.prototype[a]}Queuebert.BackgroundClient=b})();if(typeof(exports)!=="undefined"){exports.BackgroundClient=Queuebert.BackgroundClient}if(typeof(Queuebert)==="undefined"){Queuebert={}}(function(){function a(b){var d=this,c=b||{};this.messages={};this.maxQueueSize=c.maxQueueSize||128;this.onRequest=c.onRequest||chrome.extension.onRequest;this.onRequest.addListener(function(f,e,g){d.handleMessage(f,e,g)})}a.prototype.handleMessage=function(f,d,h){var b=h||function(){},c=f.tabId||this.getTabId(d);this.registerClient(f.to,c);this.registerClient(f.from,c);if(f.type=="put"){var e={from:f.from,action:f.action,body:f.body,tabId:this.getTabId(d)};if(h){e.callback=h}this.messages[c][f.to].push(e);if(this.messages[c][f.to].length>this.maxQueueSize){this.messages[c][f.to].shift()}}else{if(f.type=="get"){var g=this.messages[c][f.from].shift();if(g){b(g)}else{b(null)}}}};a.prototype.registerClient=function(c,b){if(typeof(this.messages[b])!=="object"){this.messages[b]={}}if(typeof(this.messages[b][c])==="object"){return}this.messages[b][c]=[]};a.prototype.getTabId=function(b){return"tab_"+b.tab.id};Queuebert.Server=a})();if(typeof(exports)!=="undefined"){exports.Server=Queuebert.Server}; -------------------------------------------------------------------------------- /example/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /example/content-script.js: -------------------------------------------------------------------------------- 1 | var delegate = { 2 | receivedMessage: function(clientId, clientTabId, body) { 3 | console.log(body.message); 4 | } 5 | }; 6 | 7 | var client = new Queuebert.Client({ 8 | identifier: 'client', 9 | delegate: delegate 10 | }); 11 | 12 | setInterval(function() { 13 | client.sendMessage({ 14 | action: 'echo', 15 | tabId: 'background', 16 | to: 'background', 17 | body: {message: 'Hello World!'} 18 | }); 19 | }, 2000); 20 | 21 | setInterval(function() { 22 | client.sendMessage({ 23 | action: 'add', 24 | tabId: 'background', 25 | to: 'background2', 26 | body: {x: 5, y: 20} 27 | }); 28 | }, 4000); 29 | -------------------------------------------------------------------------------- /images/queuebert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/attachmentsme/Queuebert/201ca77e5bca5563c941331145091971ff8f5d21/images/queuebert.png -------------------------------------------------------------------------------- /lib/background-client.js: -------------------------------------------------------------------------------- 1 | if (typeof(Queuebert) === 'undefined') { 2 | Queuebert = {}; 3 | } 4 | 5 | // Specifically for our unit tests which are written in Node.js. 6 | if (!Queuebert.Client && typeof(require) !== 'undefined' && typeof(exports) !== 'undefined') { 7 | Queuebert.Client = require('./client').Client; 8 | } 9 | 10 | (function() { 11 | function BackgroundClient(_params) { 12 | var params = _params || {}, 13 | _this = this; 14 | 15 | this.delegate = params.delegate || {}; 16 | this.identifier = params.identifier || 'background'; 17 | this.server = params.server; 18 | this.messageInterval = params.messageInterval || 150; 19 | this.setInterval = params.setInterval || function(callback, time) { setInterval(callback, time); }; 20 | 21 | this.sendRequest = function(request, callback) { 22 | if (request.type == 'get') { 23 | request.tabId = 'background'; 24 | } 25 | _this.server.handleMessage(request, {tab: {id: 'background'}}, callback); 26 | }; 27 | 28 | if (params.start === undefined || params.start) { 29 | this.start(); 30 | } 31 | }; 32 | 33 | for (var key in Queuebert.Client.prototype) { 34 | BackgroundClient.prototype[key] = Queuebert.Client.prototype[key]; 35 | } 36 | 37 | Queuebert.BackgroundClient = BackgroundClient; 38 | })(); 39 | 40 | // Specifically for our unit tests which are written in Node.js. 41 | if (typeof(exports) !== 'undefined') { 42 | exports.BackgroundClient = Queuebert.BackgroundClient; 43 | } 44 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | if (typeof(Queuebert) === 'undefined') { 2 | Queuebert = {}; 3 | } 4 | 5 | (function() { 6 | function Client(_params) { 7 | var params = _params || {}; 8 | this.delegate = params.delegate || {}; 9 | this.identifier = params.identifier; 10 | this.messageInterval = params.messageInterval || 150; 11 | this.sendRequest = params.sendRequest || chrome.extension.sendRequest; 12 | this.setInterval = params.setInterval || function(callback, time) { setInterval(callback, time); }; 13 | 14 | if (params.start === undefined || params.start) { 15 | this.start(); 16 | } 17 | } 18 | 19 | Client.prototype.start = function() { 20 | var _this = this; 21 | 22 | this.setInterval(function() { 23 | _this.sendRequest({from: _this.identifier, type: 'get'}, function(rawMessage) { 24 | if (rawMessage) { 25 | _this.handleMessage(rawMessage.action, rawMessage.from, rawMessage.tabId, rawMessage.body, rawMessage.callback); 26 | } 27 | }); 28 | }, this.messageInterval); 29 | } 30 | 31 | Client.prototype.handleMessage = function(action, clientId, clientTabId, body, callback) { 32 | if (this.delegate[action]) { 33 | this.delegate[action](clientId, clientTabId, body, callback); 34 | } 35 | }; 36 | 37 | Client.prototype.sendMessage = function(msg, callback) { 38 | 39 | var message = { 40 | tabId: msg.tabId, 41 | to: msg.to, 42 | from: this.identifier, 43 | action: msg.action, 44 | type: 'put', 45 | body: msg.body 46 | }; 47 | 48 | callback ? this.sendRequest(message, callback) : this.sendRequest(message); 49 | }; 50 | 51 | Queuebert.Client = Client; 52 | })(); 53 | 54 | // Specifically for our unit tests which are written in Node.js. 55 | if (typeof(exports) !== 'undefined') { 56 | exports.Client = Queuebert.Client; 57 | } 58 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | if (typeof(Queuebert) === 'undefined') { 2 | Queuebert = {}; 3 | } 4 | 5 | (function() { 6 | function Server(_params) { 7 | var _this = this, 8 | params = _params || {}; 9 | 10 | this.messages = {}; 11 | this.maxQueueSize = params.maxQueueSize || 128; 12 | this.onRequest = params.onRequest || chrome.extension.onRequest; 13 | this.onRequest.addListener(function(request, sender, callback) { 14 | _this.handleMessage(request, sender, callback); 15 | }); 16 | } 17 | 18 | Server.prototype.handleMessage = function(request, sender, callback) { 19 | var _callback = callback || function() {}, 20 | tabId = request.tabId || this.getTabId(sender); 21 | 22 | this.registerClient(request.to, tabId); 23 | this.registerClient(request.from, tabId); 24 | 25 | if (request.type == 'put') { 26 | var message = { 27 | from: request.from, 28 | action: request.action, 29 | body: request.body, 30 | tabId: this.getTabId(sender) 31 | } 32 | 33 | if (callback) { 34 | message['callback'] = callback; 35 | } 36 | 37 | this.messages[ tabId ][request.to].push(message); 38 | 39 | if (this.messages[ tabId ][request.to].length > this.maxQueueSize) { 40 | this.messages[ tabId ][request.to].shift(); 41 | } 42 | } else if(request.type == 'get') { 43 | var rawMessage = this.messages[tabId][request.from].shift(); 44 | if (rawMessage) { 45 | _callback(rawMessage); 46 | } else { 47 | _callback(null); 48 | } 49 | } 50 | }; 51 | 52 | Server.prototype.registerClient = function(identifier, tabId) { 53 | 54 | if (typeof(this.messages[tabId]) !== 'object') { 55 | this.messages[tabId] = {}; 56 | } 57 | 58 | if (typeof(this.messages[tabId][identifier]) === 'object') return; 59 | this.messages[tabId][identifier] = []; 60 | }; 61 | 62 | Server.prototype.getTabId = function(sender) { 63 | return 'tab_' + sender.tab.id; 64 | }; 65 | 66 | Queuebert.Server = Server; 67 | })(); 68 | 69 | // Specifically for our unit tests which are written in Node.js. 70 | if (typeof(exports) !== 'undefined') { 71 | exports.Server = Queuebert.Server; 72 | } 73 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Queuebert", 3 | "version": "0.0.1", 4 | "description": "A queue for communicating between iframes and background.html in your chrome extension.", 5 | "background_page" : "example/background.html", 6 | "page_action": {}, 7 | "icons": {}, 8 | "permissions": ["tabs"], 9 | "content_scripts": [ 10 | { 11 | "matches": ["http://*/", "https://*/"], 12 | "js": [ 13 | "lib/client.js", 14 | "example/content-script.js" 15 | ], 16 | "all_frames": false 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Queuebert", 3 | "directories": { 4 | "lib": "./lib" 5 | }, 6 | "version": "1.0.0", 7 | "author": "Ben Coe