├── package.json
├── .gitignore
├── Gruntfile.js
├── README.md
├── index.html
├── samples
└── index.html
├── LICENSE
└── src
└── crosstab.js
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crosstab",
3 | "version": "0.0.0",
4 | "devDependencies": {
5 | "grunt": "~0.4.5",
6 | "grunt-contrib-jshint": "~0.10.0",
7 | "grunt-contrib-connect": "~0.8.0",
8 | "grunt-contrib-watch": "~0.6.1"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | grunt.initConfig({
3 | pkg: grunt.file.readJSON('package.json'),
4 | jshint: {
5 | files: ['Gruntfile.js', 'src/*.js']
6 | },
7 | watch: {
8 | scripts: {
9 | files: ['<%= jshint.files %>'],
10 | tasks: ['jshint']
11 | }
12 | },
13 | connect: {
14 | server: {
15 | options: {
16 | port: 9000,
17 | base: '.'
18 | }
19 | }
20 | }
21 | });
22 | grunt.loadNpmTasks('grunt-contrib-connect');
23 | grunt.loadNpmTasks('grunt-contrib-jshint');
24 | grunt.loadNpmTasks('grunt-contrib-watch');
25 |
26 | grunt.registerTask('default', ['connect', 'watch']);
27 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | crosstab
2 | ========
3 |
4 | A utility library from cross-tab communication using localStorage.
5 |
6 |
7 | What is it?
8 | -----------
9 |
10 | crosstab is a javascript utility library for inter-tab communication on the same domain. It offers the following features:
11 |
12 | * Tracks open tabs
13 | * Designates a `master` tab and updates the master tab if it closes or times out. This is useful for maintaining a single server connection across all tabs.
14 | * Broadcast messages to all tabs or a particular tab
15 |
16 | # Browser Compatibility #
17 |
18 |
19 | | Browser | Version Tested |
20 | |---------|----------------|
21 | | IE | 9+ |
22 | | Chrome | 35+ |
23 | | FireFox | 30+ |
24 | | Safari | 6.1+ |
25 |
26 |
27 | Why was it made?
28 | ----------------
29 |
30 | I wanted to be able to have robust cross tab communication for the purpose of resource sharing (such as websockets). Though there are some libraries which have a similar goal, they all had subtle issues. This library aims to be the most correct it can be for supported browsers. This library was created with inspiration from the excellent [intercom.js](https://github.com/diy/intercom.js/) library, and addresses several of it's shortcomings:
31 |
32 | * intercom.js doesn't implement proper locking.
33 | * does not guarantee that one tab holds the lock at a time (in fact this is impossible to guarantee flat out, but can be guaranteed within defined execution times).
34 | * locking on functions that throw will break.
35 | * Updates to any localStorage item will cause the locks to attempt to be acquired instead of only removals of the lock.
36 | * in trying to support IE8 message broadcasting in intercom.js has a race condition where messages can be dropped.
37 | * intercom.js leaks memory by maintaining a state of every message id received (also in an attempt to support IE8)
38 |
39 | crosstab solves these issues by dropping support for IE8 and using a lockless system that is entirely event driven (IE8 cannot pass messages via localStorage events, which is why intercom.js requires locking, because it supports IE8).
40 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
lastUpdated: {{((now() - tab.lastUpdated) / 1000) | number:1}}s ago
28 |
29 |
30 |
Messages
31 |
38 |
39 |
{{message}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/src/crosstab.js:
--------------------------------------------------------------------------------
1 | var crosstab = (function () {
2 |
3 | // --- Handle Support ---
4 | // See: http://detectmobilebrowsers.com/about
5 | var useragent = navigator.userAgent || navigator.vendor || window.opera;
6 | window.isMobile = (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(useragent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(useragent.substr(0, 4)));
7 |
8 | var localStorage = window.localStorage;
9 |
10 | // Other reasons
11 | var frozenTabEnvironment = false;
12 |
13 | function notSupported() {
14 | var errorMsg = 'crosstab not supported';
15 | var reasons = [];
16 | if (!localStorage) {
17 | reasons.push('localStorage not availabe');
18 | }
19 | if (!window.addEventListener) {
20 | reasons.push('addEventListener not available');
21 | }
22 | if (window.isMobile) {
23 | reasons.push('mobile browser');
24 | }
25 | if (frozenTabEnvironment) {
26 | reasons.push('frozen tab environment detected');
27 | }
28 |
29 | if(reasons.length > 0) {
30 | errorMsg += ': ' + reasons.join(', ');
31 | }
32 |
33 | throw new Error(errorMsg);
34 | }
35 |
36 | // --- Utility ---
37 | var util = {
38 | keys: {
39 | MESSAGE_KEY: 'crosstab.MESSAGE_KEY',
40 | TABS_KEY: 'crosstab.TABS_KEY',
41 | MASTER_TAB: 'MASTER_TAB',
42 | SUPPORTED_KEY: 'crosstab.SUPPORTED',
43 | FROZEN_TAB_ENVIRONMENT: 'crosstab.FROZEN_TAB_ENVIRONMENT'
44 | }
45 | };
46 |
47 | util.forEachObj = function (thing, fn) {
48 | for (var key in thing) {
49 | if (thing.hasOwnProperty(key)) {
50 | fn.call(thing, thing[key], key);
51 | }
52 | }
53 | };
54 |
55 | util.forEachArr = function (thing, fn) {
56 | for (var i = 0; i < thing.length; i++) {
57 | fn.call(thing, thing[i], i);
58 | }
59 | };
60 |
61 | util.forEach = function (thing, fn) {
62 | if (Object.prototype.toString.call(thing) === '[object Array]') {
63 | util.forEachArr(thing, fn);
64 | } else {
65 | util.forEachObj(thing, fn);
66 | }
67 | };
68 |
69 | util.map = function (thing, fn) {
70 | var res = [];
71 | util.forEach(thing, function (item) {
72 | res.push(fn(item));
73 | });
74 |
75 | return res;
76 | };
77 |
78 | util.filter = function (thing, fn) {
79 | var isArr = Object.prototype.toString.call(thing) === '[object Array]';
80 | var res = isArr ? [] : {};
81 |
82 | if (isArr) {
83 | util.forEachArr(thing, function (value, key) {
84 | if (fn(value, key)) {
85 | res.push(value);
86 | }
87 | });
88 | } else {
89 | util.forEachObj(thing, function (value, key) {
90 | if (fn(value, key)) {
91 | res[key] = value;
92 | }
93 | });
94 | }
95 |
96 | return res;
97 | };
98 |
99 | util.now = function () {
100 | return (new Date()).getTime();
101 | };
102 |
103 | util.tabs = getStoredTabs();
104 |
105 | util.eventTypes = {
106 | becomeMaster: 'becomeMaster',
107 | tabUpdated: 'tabUpdated',
108 | tabClosed: 'tabClosed',
109 | tabPromoted: 'tabPromoted'
110 | };
111 |
112 | // --- Events ---
113 | // node.js style events, with the main difference being object based
114 | // rather than array based, as well as being able to add/remove
115 | // events by key.
116 | util.createEventHandler = function () {
117 | var events = {};
118 |
119 | var addListener = function (event, listener, key) {
120 | key = key || listener;
121 | var handlers = listeners(event);
122 | handlers[key] = listener;
123 |
124 | return key;
125 | };
126 |
127 | var removeListener = function (event, key) {
128 | if (events[event] && events[event][key]) {
129 | delete events[event][key];
130 | return true;
131 | }
132 | return false;
133 | };
134 |
135 | var removeAllListeners = function (event) {
136 | if (event) {
137 | if (events[event]) {
138 | delete events[event];
139 | }
140 | } else {
141 | events = {};
142 | }
143 | };
144 |
145 | var emit = function (event) {
146 | var args = Array.prototype.slice.call(arguments, 1);
147 | var handlers = listeners(event);
148 |
149 | util.forEach(handlers, function (listener) {
150 | if (typeof (listener) === 'function') {
151 | listener.apply(this, args);
152 | }
153 | });
154 | };
155 |
156 | var once = function (event, listener, key) {
157 | // Generate a unique id for this listener
158 | var handlers = listeners(event);
159 | while (!key || handlers[key]) {
160 | key = util.generateId();
161 | }
162 |
163 | addListener(event, function () {
164 | removeListener(event, key);
165 | var args = Array.prototype.slice.call(arguments);
166 | listener.apply(this, args);
167 | }, key);
168 |
169 | return key;
170 | };
171 |
172 | var listeners = function (event) {
173 | var handlers = events[event] = events[event] || {};
174 | return handlers;
175 | };
176 |
177 | return {
178 | addListener: addListener,
179 | on: addListener,
180 | off: removeListener,
181 | once: once,
182 | emit: emit,
183 | listeners: listeners,
184 | removeListener: removeListener,
185 | removeAllListeners: removeAllListeners
186 | };
187 | };
188 |
189 | // --- Setup Events ---
190 | var eventHandler = util.createEventHandler();
191 |
192 | // wrap eventHandler so that setting it will not blow up
193 | // any of the internal workings
194 | util.events = {
195 | addListener: eventHandler.addListener,
196 | on: eventHandler.on,
197 | off: eventHandler.off,
198 | once: eventHandler.once,
199 | emit: eventHandler.emit,
200 | listeners: eventHandler.listeners,
201 | removeListener: eventHandler.removeListener,
202 | removeAllListeners: eventHandler.removeAllListeners
203 | };
204 |
205 | function onStorageEvent(event) {
206 | var eventValue;
207 | try {
208 | eventValue = event.newValue ? JSON.parse(event.newValue) : {};
209 | } catch (e) {
210 | eventValue = {};
211 | }
212 | if (!eventValue.id || eventValue.id === crosstab.id) {
213 | // This is to force IE to behave properly
214 | return;
215 | }
216 | if (event.key === util.keys.MESSAGE_KEY) {
217 | var message = eventValue.data;
218 | // only handle if this message was meant for this tab.
219 | if (!message.destination || message.destination === crosstab.id) {
220 | eventHandler.emit(message.event, message);
221 | }
222 | }
223 | }
224 |
225 | function setLocalStorageItem(key, data) {
226 | var storageItem = {
227 | id: crosstab.id,
228 | data: data,
229 | timestamp: util.now()
230 | };
231 |
232 | localStorage.setItem(key, JSON.stringify(storageItem));
233 | }
234 |
235 | function getLocalStorageItem(key) {
236 | var item = getLocalStorageRaw(key);
237 | return item.data;
238 | }
239 |
240 | function getLocalStorageRaw(key) {
241 | var json = localStorage.getItem(key);
242 | var item = json ? JSON.parse(json) : {};
243 | return item;
244 | }
245 |
246 | function beforeUnload() {
247 | var numTabs = 0;
248 | util.forEach(util.tabs, function (tab, key) {
249 | if (key !== util.keys.MASTER_TAB) {
250 | numTabs++;
251 | }
252 | });
253 |
254 | if (numTabs === 1) {
255 | util.tabs = {};
256 | setStoredTabs();
257 | } else {
258 | broadcast(util.eventTypes.tabClosed, crosstab.id);
259 | }
260 | }
261 |
262 | function getMaster() {
263 | return util.tabs[util.keys.MASTER_TAB];
264 | }
265 |
266 | function setMaster(newMaster) {
267 | util.tabs[util.keys.MASTER_TAB] = newMaster;
268 | }
269 |
270 | function deleteMaster() {
271 | delete util.tabs[util.keys.MASTER_TAB];
272 | }
273 |
274 | function isMaster() {
275 | return getMaster().id === crosstab.id;
276 | }
277 |
278 | function masterTabElection() {
279 | var maxId = null;
280 | util.forEach(util.tabs, function (tab) {
281 | if (!maxId || tab.id < maxId) {
282 | maxId = tab.id;
283 | }
284 | });
285 |
286 | // only broadcast the promotion if I am the new master
287 | if (maxId === crosstab.id) {
288 | broadcast(util.eventTypes.tabPromoted, crosstab.id);
289 | } else {
290 | // this is done so that in the case where multiple tabs are being
291 | // started at the same time, and there is no current saved tab
292 | // information, we will still have a value set for the master tab
293 | setMaster({
294 | id: maxId,
295 | lastUpdated: util.now()
296 | });
297 | }
298 | }
299 |
300 | // Handle other tabs closing by updating internal tab model, and promoting
301 | // self if we are the lowest tab id
302 | eventHandler.addListener(util.eventTypes.tabClosed, function (message) {
303 | var id = message.data;
304 | if (util.tabs[id]) {
305 | delete util.tabs[id];
306 | }
307 |
308 | if (!getMaster() || getMaster().id === id) {
309 | // If the master was the closed tab, delete it and the highest
310 | // tab ID becomes the new master, which will save the tabs
311 | if (getMaster()) {
312 | deleteMaster();
313 | }
314 | masterTabElection();
315 | } else if (getMaster().id === crosstab.id) {
316 | // If I am master, save the new tabs out
317 | setStoredTabs();
318 | }
319 | });
320 |
321 | eventHandler.addListener(util.eventTypes.tabUpdated, function (message) {
322 | var tab = message.data;
323 | util.tabs[tab.id] = tab;
324 |
325 | // If there is no master, hold an election
326 | if (!getMaster()) {
327 | masterTabElection();
328 | }
329 |
330 | if (getMaster().id === tab.id) {
331 | setMaster(tab);
332 | }
333 | if (getMaster().id === crosstab.id) {
334 | // If I am master, save the new tabs out
335 | setStoredTabs();
336 | }
337 | });
338 |
339 | eventHandler.addListener(util.eventTypes.tabPromoted, function (message) {
340 | var id = message.data;
341 | var lastUpdated = message.timestamp;
342 | setMaster({
343 | id: id,
344 | lastUpdated: lastUpdated
345 | });
346 |
347 | if (crosstab.id === id) {
348 | // set the tabs in localStorage
349 | setStoredTabs();
350 |
351 | // emit the become master event so we can handle it accordingly
352 | util.events.emit(util.eventTypes.becomeMaster);
353 | }
354 | });
355 |
356 | function pad(num, width, padChar) {
357 | padChar = padChar || '0';
358 | var numStr = (num.toString());
359 |
360 | if (numStr.length >= width) {
361 | return numStr;
362 | }
363 |
364 | return new Array(width - numStr.length + 1).join(padChar) + numStr;
365 | }
366 |
367 | util.generateId = function () {
368 | /*jshint bitwise: false*/
369 | return util.now().toString() + pad((Math.random() * 0x7FFFFFFF) | 0, 10);
370 | };
371 |
372 | // --- Setup message sending and handling ---
373 | function broadcast(event, data, destination) {
374 | var message = {
375 | event: event,
376 | data: data,
377 | destination: destination,
378 | origin: crosstab.id,
379 | timestamp: util.now()
380 | };
381 |
382 | // If the destination differs from the origin send it out, otherwise
383 | // handle it locally
384 | if (message.destination !== message.origin) {
385 | setLocalStorageItem(util.keys.MESSAGE_KEY, message);
386 | }
387 |
388 | if (!message.destination || message.destination === message.origin) {
389 | eventHandler.emit(event, message);
390 | }
391 | }
392 |
393 | function broadcastMaster(event, data) {
394 | broadcast(event, data, getMaster().id);
395 | }
396 |
397 | // ---- Return ----
398 | var setupComplete = false;
399 | util.events.once('setupComplete', function () {
400 | setupComplete = true;
401 | });
402 |
403 | var crosstab = function (fn) {
404 | if (setupComplete) {
405 | fn();
406 | } else {
407 | util.events.once('setupComplete', fn);
408 | }
409 | };
410 |
411 | crosstab.id = util.generateId();
412 | crosstab.supported = !!localStorage && window.addEventListener && !window.isMobile;
413 | crosstab.util = util;
414 | crosstab.broadcast = broadcast;
415 | crosstab.broadcastMaster = broadcastMaster;
416 |
417 | // --- Crosstab supported ---
418 | // Check to see if the global supported key has been set.
419 | if (!setupComplete) {
420 | var supportedRaw = getLocalStorageRaw(util.keys.SUPPORTED_KEY);
421 | var supported = supportedRaw.data;
422 | if (supported === false || supported === true) {
423 | // As long as it is explicitely set, use the value
424 | crosstab.supported = supported;
425 | util.events.emit('setupComplete');
426 | }
427 | }
428 |
429 | // Check to see if the global frozen tab environment key has been set.
430 | if (!setupComplete && crosstab.supported) {
431 | var frozenTabsRaw = getLocalStorageRaw(util.keys.FROZEN_TAB_ENVIRONMENT);
432 | var frozenTabs = frozenTabsRaw.data;
433 | if (frozenTabs === true) {
434 | frozenTabEnvironmentDetected();
435 | util.events.emit('setupComplete');
436 | }
437 | }
438 |
439 | function frozenTabEnvironmentDetected() {
440 | crosstab.supported = false;
441 | frozenTabEnvironment = true;
442 | setLocalStorageItem(util.keys.FROZEN_TAB_ENVIRONMENT, true);
443 | setLocalStorageItem(util.keys.SUPPORTED_KEY, false);
444 | crosstab.broadcast = notSupported;
445 | }
446 |
447 | // --- Tab Setup ---
448 | // 3 second keepalive
449 | var TAB_KEEPALIVE = 3 * 1000;
450 | // 5 second timeout
451 | var TAB_TIMEOUT = 5 * 1000;
452 | // 100 ms ping timeout
453 | var PING_TIMEOUT = 100;
454 |
455 | function getStoredTabs() {
456 | var storedTabs = getLocalStorageItem(util.keys.TABS_KEY);
457 | util.tabs = storedTabs || util.tabs || {};
458 | return util.tabs;
459 | }
460 |
461 | function setStoredTabs() {
462 | setLocalStorageItem(util.keys.TABS_KEY, util.tabs);
463 | }
464 |
465 | function keepalive() {
466 | var now = util.now();
467 |
468 | var myTab = {
469 | id: crosstab.id,
470 | lastUpdated: now
471 | };
472 |
473 | // broadcast tabUpdated event
474 | broadcast(util.eventTypes.tabUpdated, myTab);
475 |
476 | // broadcast tabClosed event for each tab that timed out
477 | function stillAlive(tab) {
478 | return now - tab.lastUpdated < TAB_TIMEOUT;
479 | }
480 |
481 | function notAlive(tab, key) {
482 | return key !== util.keys.MASTER_TAB && !stillAlive(tab);
483 | }
484 |
485 | var deadTabs = util.filter(util.tabs, notAlive);
486 | util.forEach(deadTabs, function (tab) {
487 | broadcast(util.eventTypes.tabClosed, tab.id);
488 | });
489 |
490 | // check to see if setup is complete
491 | if (!setupComplete) {
492 | var masterTab = crosstab.util.tabs[crosstab.util.keys.MASTER_TAB];
493 | // ping master
494 | if (masterTab && masterTab.id !== myTab.id) {
495 | var timeout;
496 | var start;
497 |
498 | crosstab.util.events.once('PONG', function () {
499 | if (!setupComplete) {
500 | clearTimeout(timeout);
501 | // set supported to true / frozen to false
502 | setLocalStorageItem(
503 | util.keys.SUPPORTED_KEY,
504 | true);
505 | setLocalStorageItem(
506 | util.keys.FROZEN_TAB_ENVIRONMENT,
507 | false);
508 | util.events.emit('setupComplete');
509 | }
510 | });
511 |
512 | start = util.now();
513 |
514 | // There is a nested timeout here. We'll give it 100ms
515 | // timeout, with iters "yields" to the event loop. So at least
516 | // iters number of blocks of javascript will be able to run
517 | // covering at least 100ms
518 | var recursiveTimeout = function (iters) {
519 | var diff = util.now() - start;
520 |
521 | if (!setupComplete) {
522 | if (iters <= 0 && diff > PING_TIMEOUT) {
523 | frozenTabEnvironmentDetected();
524 | util.events.emit('setupComplete');
525 | } else {
526 | timeout = setTimeout(function () {
527 | recursiveTimeout(iters - 1);
528 | }, 5);
529 | }
530 | }
531 | };
532 |
533 | var iterations = 5;
534 | timeout = setTimeout(function () {
535 | recursiveTimeout(5);
536 | }, PING_TIMEOUT - 5 * iterations);
537 | crosstab.broadcastMaster('PING');
538 | } else if (masterTab && masterTab.id === myTab.id) {
539 | util.events.emit('setupComplete');
540 | }
541 | }
542 | }
543 |
544 | function keepaliveLoop() {
545 | if (!crosstab.stopKeepalive) {
546 | keepalive();
547 | window.setTimeout(keepaliveLoop, TAB_KEEPALIVE);
548 | }
549 | }
550 |
551 | // --- Check if crosstab is supported ---
552 | if (!crosstab.supported) {
553 | crosstab.broadcast = notSupported;
554 | } else {
555 | // ---- Setup Storage Listener
556 | window.addEventListener('storage', onStorageEvent, false);
557 | window.addEventListener('beforeunload', beforeUnload, false);
558 |
559 | util.events.on('PING', function (message) {
560 | // only handle direct messages
561 | if (!message.destination || message.destination !== crosstab.id) {
562 | return;
563 | }
564 |
565 | if (util.now() - message.timestamp < PING_TIMEOUT) {
566 | crosstab.broadcast('PONG', null, message.origin);
567 | }
568 | });
569 |
570 | keepaliveLoop();
571 | }
572 |
573 | return crosstab;
574 | })();
575 |
576 |
--------------------------------------------------------------------------------