516 lines
13 KiB
JavaScript
516 lines
13 KiB
JavaScript
/*jshint bitwise: false*/
|
|
(function(global, document, undefined) {
|
|
'use strict';
|
|
var previousMessageBus = global.MessageBus;
|
|
|
|
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
|
|
var callbacks, clientId, failCount, shouldLongPoll, queue, responseCallbacks, uniqueId, baseUrl;
|
|
var me, started, stopped, longPoller, pollTimeout, paused, later, jQuery, interval, chunkedBackoff;
|
|
var delayPollTimeout;
|
|
|
|
var ajaxInProgress = false;
|
|
|
|
uniqueId = function() {
|
|
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r, v;
|
|
r = Math.random() * 16 | 0;
|
|
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
};
|
|
|
|
clientId = uniqueId();
|
|
responseCallbacks = {};
|
|
callbacks = [];
|
|
queue = [];
|
|
interval = null;
|
|
failCount = 0;
|
|
baseUrl = "/";
|
|
paused = false;
|
|
later = [];
|
|
chunkedBackoff = 0;
|
|
jQuery = global.jQuery;
|
|
var hiddenProperty;
|
|
|
|
(function(){
|
|
var prefixes = ["","webkit","ms","moz"];
|
|
for(var i=0; i<prefixes.length; i++) {
|
|
var prefix = prefixes[i];
|
|
var check = prefix + (prefix === "" ? "hidden" : "Hidden");
|
|
if(document[check] !== undefined ){
|
|
hiddenProperty = check;
|
|
}
|
|
}
|
|
})();
|
|
|
|
var isHidden = function() {
|
|
if (hiddenProperty !== undefined){
|
|
return document[hiddenProperty];
|
|
} else {
|
|
return !document.hasFocus;
|
|
}
|
|
};
|
|
|
|
var hasLocalStorage = (function() {
|
|
try {
|
|
localStorage.setItem("mbTestLocalStorage", Date.now());
|
|
localStorage.removeItem("mbTestLocalStorage");
|
|
return true;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
var updateLastAjax = function() {
|
|
if (hasLocalStorage) {
|
|
localStorage.setItem("__mbLastAjax", Date.now());
|
|
}
|
|
}
|
|
|
|
var hiddenTabShouldWait = function() {
|
|
if (hasLocalStorage && isHidden()) {
|
|
var lastAjaxCall = parseInt(localStorage.getItem("__mbLastAjax"), 10);
|
|
var deltaAjax = Date.now() - lastAjaxCall;
|
|
|
|
return deltaAjax >= 0 && deltaAjax < me.minHiddenPollInterval;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var hasonprogress = (new XMLHttpRequest()).onprogress === null;
|
|
var allowChunked = function(){
|
|
return me.enableChunkedEncoding && hasonprogress;
|
|
};
|
|
|
|
shouldLongPoll = function() {
|
|
return me.alwaysLongPoll || !isHidden();
|
|
};
|
|
|
|
var totalAjaxFailures = 0;
|
|
var totalAjaxCalls = 0;
|
|
var lastAjax;
|
|
|
|
var processMessages = function(messages) {
|
|
var gotData = false;
|
|
if (!messages) return false; // server unexpectedly closed connection
|
|
|
|
for (var i=0; i<messages.length; i++) {
|
|
var message = messages[i];
|
|
gotData = true;
|
|
for (var j=0; j<callbacks.length; j++) {
|
|
var callback = callbacks[j];
|
|
if (callback.channel === message.channel) {
|
|
callback.last_id = message.message_id;
|
|
try {
|
|
callback.func(message.data, message.global_id, message.message_id);
|
|
}
|
|
catch(e){
|
|
if(console.log) {
|
|
console.log("MESSAGE BUS FAIL: callback " + callback.channel + " caused exception " + e.message);
|
|
}
|
|
}
|
|
}
|
|
if (message.channel === "/__status") {
|
|
if (message.data[callback.channel] !== undefined) {
|
|
callback.last_id = message.data[callback.channel];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return gotData;
|
|
};
|
|
|
|
var reqSuccess = function(messages) {
|
|
failCount = 0;
|
|
if (paused) {
|
|
if (messages) {
|
|
for (var i=0; i<messages.length; i++) {
|
|
later.push(messages[i]);
|
|
}
|
|
}
|
|
} else {
|
|
return processMessages(messages);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
longPoller = function(poll, data) {
|
|
|
|
if (ajaxInProgress) {
|
|
// never allow concurrent ajax reqs
|
|
return;
|
|
}
|
|
|
|
var gotData = false;
|
|
var aborted = false;
|
|
lastAjax = new Date();
|
|
totalAjaxCalls += 1;
|
|
data.__seq = totalAjaxCalls;
|
|
|
|
var longPoll = shouldLongPoll() && me.enableLongPolling;
|
|
var chunked = longPoll && allowChunked();
|
|
if (chunkedBackoff > 0) {
|
|
chunkedBackoff--;
|
|
chunked = false;
|
|
}
|
|
|
|
var headers = {
|
|
'X-SILENCE-LOGGER': 'true'
|
|
};
|
|
for (var name in me.headers){
|
|
headers[name] = me.headers[name];
|
|
}
|
|
|
|
if (!chunked){
|
|
headers["Dont-Chunk"] = 'true';
|
|
}
|
|
|
|
var dataType = chunked ? "text" : "json";
|
|
|
|
var handle_progress = function(payload, position) {
|
|
|
|
var separator = "\r\n|\r\n";
|
|
var endChunk = payload.indexOf(separator, position);
|
|
|
|
if (endChunk === -1) {
|
|
return position;
|
|
}
|
|
|
|
var chunk = payload.substring(position, endChunk);
|
|
chunk = chunk.replace(/\r\n\|\|\r\n/g, separator);
|
|
|
|
try {
|
|
reqSuccess(JSON.parse(chunk));
|
|
} catch(e) {
|
|
if (console.log) {
|
|
console.log("FAILED TO PARSE CHUNKED REPLY");
|
|
console.log(data);
|
|
}
|
|
}
|
|
|
|
return handle_progress(payload, endChunk + separator.length);
|
|
}
|
|
|
|
var disableChunked = function(){
|
|
if (me.longPoll) {
|
|
me.longPoll.abort();
|
|
chunkedBackoff = 30;
|
|
}
|
|
};
|
|
|
|
var setOnProgressListener = function(xhr) {
|
|
var position = 0;
|
|
// if it takes longer than 3000 ms to get first chunk, we have some proxy
|
|
// this is messing with us, so just backoff from using chunked for now
|
|
var chunkedTimeout = setTimeout(disableChunked,3000);
|
|
xhr.onprogress = function () {
|
|
clearTimeout(chunkedTimeout);
|
|
if(xhr.getResponseHeader('Content-Type') === 'application/json; charset=utf-8') {
|
|
// not chunked we are sending json back
|
|
chunked = false;
|
|
return;
|
|
}
|
|
position = handle_progress(xhr.responseText, position);
|
|
}
|
|
};
|
|
if (!me.ajax){
|
|
throw new Error("Either jQuery or the ajax adapter must be loaded");
|
|
}
|
|
|
|
updateLastAjax();
|
|
|
|
ajaxInProgress = true;
|
|
var req = me.ajax({
|
|
url: me.baseUrl + "message-bus/" + me.clientId + "/poll" + (!longPoll ? "?dlp=t" : ""),
|
|
data: data,
|
|
cache: false,
|
|
async: true,
|
|
dataType: dataType,
|
|
type: 'POST',
|
|
headers: headers,
|
|
messageBus: {
|
|
chunked: chunked,
|
|
onProgressListener: function(xhr) {
|
|
var position = 0;
|
|
// if it takes longer than 3000 ms to get first chunk, we have some proxy
|
|
// this is messing with us, so just backoff from using chunked for now
|
|
var chunkedTimeout = setTimeout(disableChunked,3000);
|
|
return xhr.onprogress = function () {
|
|
clearTimeout(chunkedTimeout);
|
|
if(xhr.getResponseHeader('Content-Type') === 'application/json; charset=utf-8') {
|
|
chunked = false; // not chunked, we are sending json back
|
|
} else {
|
|
position = handle_progress(xhr.responseText, position);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
xhr: function() {
|
|
var xhr = jQuery.ajaxSettings.xhr();
|
|
if (!chunked) {
|
|
return xhr;
|
|
}
|
|
this.messageBus.onProgressListener(xhr);
|
|
return xhr;
|
|
},
|
|
success: function(messages) {
|
|
if (!chunked) {
|
|
// we may have requested text so jQuery will not parse
|
|
if (typeof(messages) === "string") {
|
|
messages = JSON.parse(messages);
|
|
}
|
|
gotData = reqSuccess(messages);
|
|
}
|
|
},
|
|
error: function(xhr, textStatus, err) {
|
|
if(textStatus === "abort") {
|
|
aborted = true;
|
|
} else {
|
|
failCount += 1;
|
|
totalAjaxFailures += 1;
|
|
}
|
|
},
|
|
complete: function() {
|
|
|
|
ajaxInProgress = false;
|
|
|
|
var interval;
|
|
try {
|
|
if (gotData || aborted) {
|
|
interval = me.minPollInterval;
|
|
} else {
|
|
interval = me.callbackInterval;
|
|
if (failCount > 2) {
|
|
interval = interval * failCount;
|
|
} else if (!shouldLongPoll()) {
|
|
interval = me.backgroundCallbackInterval;
|
|
}
|
|
if (interval > me.maxPollInterval) {
|
|
interval = me.maxPollInterval;
|
|
}
|
|
|
|
interval -= (new Date() - lastAjax);
|
|
|
|
if (interval < 100) {
|
|
interval = 100;
|
|
}
|
|
}
|
|
} catch(e) {
|
|
if(console.log && e.message) {
|
|
console.log("MESSAGE BUS FAIL: " + e.message);
|
|
}
|
|
}
|
|
|
|
if (pollTimeout) {
|
|
clearTimeout(pollTimeout);
|
|
pollTimeout = null;
|
|
}
|
|
|
|
if (started) {
|
|
pollTimeout = setTimeout(function(){
|
|
pollTimeout = null;
|
|
poll();
|
|
}, interval);
|
|
}
|
|
|
|
me.longPoll = null;
|
|
}
|
|
});
|
|
|
|
return req;
|
|
};
|
|
|
|
me = {
|
|
/* shared between all tabs */
|
|
minHiddenPollInterval: 1500,
|
|
enableChunkedEncoding: true,
|
|
enableLongPolling: true,
|
|
callbackInterval: 15000,
|
|
backgroundCallbackInterval: 60000,
|
|
minPollInterval: 100,
|
|
maxPollInterval: 3 * 60 * 1000,
|
|
callbacks: callbacks,
|
|
clientId: clientId,
|
|
alwaysLongPoll: false,
|
|
baseUrl: baseUrl,
|
|
headers: {},
|
|
ajax: (jQuery && jQuery.ajax),
|
|
noConflict: function(){
|
|
global.MessageBus = global.MessageBus.previousMessageBus;
|
|
return this;
|
|
},
|
|
diagnostics: function(){
|
|
console.log("Stopped: " + stopped + " Started: " + started);
|
|
console.log("Current callbacks");
|
|
console.log(callbacks);
|
|
console.log("Total ajax calls: " + totalAjaxCalls + " Recent failure count: " + failCount + " Total failures: " + totalAjaxFailures);
|
|
console.log("Last ajax call: " + (new Date() - lastAjax) / 1000 + " seconds ago") ;
|
|
},
|
|
|
|
pause: function() {
|
|
paused = true;
|
|
},
|
|
|
|
resume: function() {
|
|
paused = false;
|
|
processMessages(later);
|
|
later = [];
|
|
},
|
|
|
|
stop: function() {
|
|
stopped = true;
|
|
started = false;
|
|
if (delayPollTimeout) {
|
|
clearTimeout(delayPollTimeout);
|
|
delayPollTimeout = null;
|
|
}
|
|
if (me.longPoll) {
|
|
me.longPoll.abort();
|
|
}
|
|
},
|
|
|
|
// Start polling
|
|
start: function() {
|
|
var poll;
|
|
|
|
if (started) return;
|
|
started = true;
|
|
stopped = false;
|
|
|
|
poll = function() {
|
|
var data;
|
|
|
|
if(stopped) {
|
|
return;
|
|
}
|
|
|
|
if (callbacks.length === 0 || hiddenTabShouldWait()) {
|
|
if(!delayPollTimeout) {
|
|
delayPollTimeout = setTimeout(function() {
|
|
delayPollTimeout = null;
|
|
poll();
|
|
}, parseInt(500 + Math.random() * 500));
|
|
}
|
|
return;
|
|
}
|
|
|
|
data = {};
|
|
for (var i=0;i<callbacks.length;i++) {
|
|
data[callbacks[i].channel] = callbacks[i].last_id;
|
|
}
|
|
|
|
// could possibly already be started
|
|
// notice the delay timeout above
|
|
if (!me.longPoll) {
|
|
me.longPoll = longPoller(poll, data);
|
|
}
|
|
};
|
|
|
|
|
|
// monitor visibility, issue a new long poll when the page shows
|
|
if(document.addEventListener && 'hidden' in document){
|
|
me.visibilityEvent = global.document.addEventListener('visibilitychange', function(){
|
|
if(!document.hidden && !me.longPoll && pollTimeout){
|
|
|
|
clearTimeout(pollTimeout);
|
|
clearTimeout(delayPollTimeout);
|
|
|
|
delayPollTimeout = null;
|
|
pollTimeout = null;
|
|
poll();
|
|
}
|
|
});
|
|
}
|
|
|
|
poll();
|
|
},
|
|
|
|
"status": function() {
|
|
if (paused) {
|
|
return "paused";
|
|
} else if (started) {
|
|
return "started";
|
|
} else if (stopped) {
|
|
return "stopped";
|
|
} else {
|
|
throw "Cannot determine current status";
|
|
}
|
|
},
|
|
|
|
// Subscribe to a channel
|
|
// if lastId is 0 or larger, it will recieve messages AFTER that id
|
|
// if lastId is negative it will perform lookbehind
|
|
// -1 will subscribe to all new messages
|
|
// -2 will recieve last message + all new messages
|
|
// -3 will recieve last 2 messages + all new messages
|
|
subscribe: function(channel, func, lastId) {
|
|
|
|
if(!started && !stopped){
|
|
me.start();
|
|
}
|
|
|
|
if (typeof(lastId) !== "number") {
|
|
lastId = -1;
|
|
}
|
|
|
|
if (typeof(channel) !== "string") {
|
|
throw "Channel name must be a string!";
|
|
}
|
|
|
|
callbacks.push({
|
|
channel: channel,
|
|
func: func,
|
|
last_id: lastId
|
|
});
|
|
if (me.longPoll) {
|
|
me.longPoll.abort();
|
|
}
|
|
|
|
return func;
|
|
},
|
|
|
|
// Unsubscribe from a channel
|
|
unsubscribe: function(channel, func) {
|
|
// TODO allow for globbing in the middle of a channel name
|
|
// like /something/*/something
|
|
// at the moment we only support globbing /something/*
|
|
var glob;
|
|
if (channel.indexOf("*", channel.length - 1) !== -1) {
|
|
channel = channel.substr(0, channel.length - 1);
|
|
glob = true;
|
|
}
|
|
|
|
var removed = false;
|
|
|
|
for (var i=callbacks.length-1; i>=0; i--) {
|
|
|
|
var callback = callbacks[i];
|
|
var keep;
|
|
|
|
if (glob) {
|
|
keep = callback.channel.substr(0, channel.length) !== channel;
|
|
} else {
|
|
keep = callback.channel !== channel;
|
|
}
|
|
|
|
if(!keep && func && callback.func !== func){
|
|
keep = true;
|
|
}
|
|
|
|
if (!keep) {
|
|
callbacks.splice(i,1);
|
|
removed = true;
|
|
}
|
|
}
|
|
|
|
if (removed && me.longPoll) {
|
|
me.longPoll.abort();
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
};
|
|
global.MessageBus = me;
|
|
})(window, document);
|