Created
April 12, 2013 01:05
-
-
Save mattsnider/5368474 to your computer and use it in GitHub Desktop.
JavaScript only messaging system for communicating between windows/tabs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(w, d) { | |
"use strict"; | |
// simple cookie writer | |
function createCookie(sName, sValue, sPath, sDomain, iMillis) { | |
var aCookie = [encodeURI(sName) + "=" + encodeURI(sValue)], | |
expires, oDate; | |
if (iMillis) { | |
oDate = new Date(); | |
oDate.setTime(oDate.getTime() + iMillis); | |
aCookie.push("expires=" + oDate.toGMTString()); | |
} | |
if (sPath) { | |
aCookie.push("path=" + sPath); | |
} | |
// single word domains, like msnider, do not work with | |
// JS cookies in Chrome for some reason | |
if (sDomain && -1 < sDomain.indexOf('.')) { | |
aCookie.push("domain=" + sDomain); | |
} | |
if (~w.location.protocol.indexOf('s')) { | |
aCookie.push("secure"); | |
} | |
d.cookie = aCookie.join(';'); | |
} | |
// simple cookie reader | |
function readCookie(sName) { | |
var cookies = d.cookie.split(/;\s*/), | |
i = cookies.length - 1, | |
a; | |
// reading from end-to-front, because some browsers (chrome) keep empty | |
// key/value pairs for replaced cookies | |
while (i >= 0) { | |
a = cookies[i--].split('='); | |
if (decodeURI(a[0]) === sName) { | |
return decodeURI(a[1]); | |
} | |
} | |
return ''; | |
} | |
// simple delay system | |
function callDelayed(fn, args, iTimeout) { | |
setTimeout(function() { | |
fn.apply(this, args); | |
}, iTimeout); | |
} | |
// ensure console.log exists | |
function fnLog(s) { | |
if (w.console) { | |
fnLog = function (s) { | |
console.log(KEY_NAME + ': ' + s); | |
} | |
} | |
else { | |
fnLog = function(s) {}; | |
} | |
fnLog(s); | |
} | |
var FORWARD_SLASH = '/', | |
KEY_NAME = 'WindowMessenger', | |
COOKIE_EXPIRES = 60000, // 60 seconds | |
CYCLE_TIMEOUT = window.WindowMessengerCycleTimeout || 5000, // 5 seconds | |
NUM_CYCLES_TO_PERSIST_MESSAGE = window.WindowMessengerNumCycles || 2, | |
aSubscribers = [], | |
iCycleCount = 0, | |
sLastReceivedMessage, | |
sLastSentMessage, | |
WindowMessenger = { | |
reader: function(sKey) {alert('WindowMessenger is not initialized');}, | |
writer: function(sKey, sValue) { | |
alert('WindowMessenger is not initialized');}, | |
/** | |
* Initializes the Window Messenger. | |
*/ | |
init: function() { | |
// 1) start the interval timer (called a cycle) to begin polling | |
// the message string | |
setInterval(WindowMessenger.readMessage, CYCLE_TIMEOUT); | |
// 2) decide which engine to use for communication | |
// WindowMessengerUseCookie can be set to true to force cookie use | |
if (w.localStorage && !w.WindowMessengerUseCookie) { | |
// 2a) using HTML5 local storage. session storage was not | |
// used because a new session is created in each tab | |
this.reader = function(sName) { | |
return localStorage.getItem(sName) || ''; | |
}; | |
this.writer = function(sName, sValue) { | |
return localStorage.setItem(sName, sValue); | |
}; | |
} | |
else { | |
// 2b) using cookie fallback | |
this.reader = function(sName) { | |
return readCookie(sName); | |
}; | |
this.writer = function(sName, sValue) { | |
return createCookie(sName, sValue, FORWARD_SLASH, | |
w.location.hostname, COOKIE_EXPIRES); | |
}; | |
} | |
// 3) if a message is already in the system, I'm going to take ownership | |
// of it. If I am not the sender, the sender may clear it before me. | |
sLastSentMessage = WindowMessenger.reader(KEY_NAME); | |
iCycleCount = 1; | |
}, | |
/** | |
* Send a message through the window messenger. In order to reduce | |
* race conditions, we only allow one message at a time, and let the | |
* others queue up in their respective windows. | |
*/ | |
postMessage: function(sMessage) { | |
// 1) current message | |
var sCurrentMessage = WindowMessenger.reader(KEY_NAME); | |
// 2) is this message being spammed? (should I send it at all) | |
if (sLastSentMessage === sMessage || sCurrentMessage === sMessage) { | |
fnLog('Not posting "' + sMessage +'", as it was recently sent.'); | |
} | |
else { | |
// 3) is there still a message | |
if (sCurrentMessage) { | |
// 3a) try again in a little while | |
fnLog('Queueing message "' + sMessage + '"'); | |
callDelayed(WindowMessenger.postMessage, [sMessage], CYCLE_TIMEOUT); | |
} | |
else { | |
// 3b) no messages, add this one | |
fnLog('Sending post "' + sMessage + '"'); | |
WindowMessenger.writer(KEY_NAME, sMessage); | |
// 4) code against race | |
// ensure race condition did not occur and message was written | |
setTimeout(function() { | |
if (sMessage === WindowMessenger.reader(KEY_NAME)) { | |
// 4a) record the sent message | |
sLastSentMessage = sMessage; | |
} | |
else { | |
fnLog('Race-Condition, re-posting'); | |
// 4b) revert and re-post | |
iCycleCount = 0; | |
callDelayed(WindowMessenger.postMessage, | |
[sMessage], CYCLE_TIMEOUT); | |
} | |
}, 500); | |
iCycleCount = 1; | |
} | |
} | |
}, | |
/** | |
* Read messages from the window messenger. | |
*/ | |
readMessage: function() { | |
// 1) read the window message | |
var sCurrentMessage = WindowMessenger.reader(KEY_NAME), | |
i, sClosureMessage; | |
fnLog('Read window message, contains - ' + sCurrentMessage); | |
// 2) is there a message, if not clear last received message | |
if (!sCurrentMessage) { | |
sLastReceivedMessage = ''; | |
} | |
// 3) is current message equal to the last message | |
else if (sCurrentMessage === sLastSentMessage) { | |
// 3a) increment the message process counter | |
iCycleCount++; | |
fnLog('Processing message "' + sCurrentMessage + '" (' + | |
iCycleCount + ')'); | |
// 3b) when max cycles exceeded, remove message key | |
if (iCycleCount > NUM_CYCLES_TO_PERSIST_MESSAGE) { | |
// refresh the window message to reduce chance of race | |
WindowMessenger.writer(KEY_NAME, ''); | |
sClosureMessage = '' + sLastSentMessage; | |
fnLog('Removing message - "' + sCurrentMessage + '"'); | |
/* | |
3c) code against race | |
*/ | |
setTimeout(function() { | |
// 3a) if message key is still in window message, | |
// then race occurred | |
if (sClosureMessage === WindowMessenger.reader(KEY_NAME)) { | |
sClosureMessage = ''; | |
iCycleCount = 0; | |
} | |
}, 500); | |
} | |
} | |
// 4) is current message not equal to the last message received, | |
// then process it. | |
else if (sCurrentMessage !== sLastReceivedMessage) { | |
sLastReceivedMessage = sCurrentMessage; | |
i = aSubscribers.length - 1; | |
fnLog('Passing message to subscribers - ' + sCurrentMessage); | |
while (i >= 0) { | |
aSubscribers[i--](sCurrentMessage); | |
} | |
} | |
}, | |
/** | |
* Adds the callback function to the subscriber list. The callback | |
* function will need to determine if it should process the passed | |
* message or not. | |
*/ | |
subscribe: function(fnCallback) { | |
aSubscribers.push(fnCallback); | |
} | |
}; | |
WindowMessenger.init(); | |
w.WindowMessenger = WindowMessenger; | |
}(window, document)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment