Skip to content

Instantly share code, notes, and snippets.

@mattsnider
Created April 12, 2013 01:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattsnider/5368474 to your computer and use it in GitHub Desktop.
Save mattsnider/5368474 to your computer and use it in GitHub Desktop.
JavaScript only messaging system for communicating between windows/tabs
(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