diff --git a/CHANGELOG.md b/CHANGELOG.md index dab03eda..4fdd80c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +1.3.0 +===== + +**Client API completely refactored in this release**. You can still use previous versions +to communicate with Centrifugo server from browser environment but new implementation much +more comfortable to use in our opinion and will be supported in future releases so consider +upgrading! + +Highlights of this release: + +* automatic resubscribe, no need to subscribe manually in `connect` event handler +* more opaque error handling +* drop support for SockJS < 1.0.0 (but if you still use SockJS 0.3.4 then feel free to open + issue and we will return its support to client) + +Please, read [new documentation](https://fzambia.gitbooks.io/centrifugal/content/clients/javascript.html) +for Javascript browser client. + +Also, DOM plugin was removed from repository as new client API design solves most of problems +that DOM plugin existed for - i.e. abstracting subscribe on many channels and automatically +resubscribe on them. With new client you can have one global connection to Centrifugo and +subscribe on channels at any moment from any part of your javascript code. + +If you are searching for old API docs (`centrifuge-js` <= 1.2.0) - [you can find it here](https://github.com/centrifugal/documentation/tree/c69ca51f21c028a6b9bd582afdbf0a5c13331957/client) + 1.2.0 ===== diff --git a/centrifuge.js b/centrifuge.js index 9de36188..59eadb59 100644 --- a/centrifuge.js +++ b/centrifuge.js @@ -1,15 +1,30 @@ ;(function () { 'use strict'; + /*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 3.0.2 + */ + (function(){"use strict";function lib$es6$promise$utils$$objectOrFunction(x){return typeof x==="function"||typeof x==="object"&&x!==null}function lib$es6$promise$utils$$isFunction(x){return typeof x==="function"}function lib$es6$promise$utils$$isMaybeThenable(x){return typeof x==="object"&&x!==null}var lib$es6$promise$utils$$_isArray;if(!Array.isArray){lib$es6$promise$utils$$_isArray=function(x){return Object.prototype.toString.call(x)==="[object Array]"}}else{lib$es6$promise$utils$$_isArray=Array.isArray}var lib$es6$promise$utils$$isArray=lib$es6$promise$utils$$_isArray;var lib$es6$promise$asap$$len=0;var lib$es6$promise$asap$$toString={}.toString;var lib$es6$promise$asap$$vertxNext;var lib$es6$promise$asap$$customSchedulerFn;var lib$es6$promise$asap$$asap=function asap(callback,arg){lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len]=callback;lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len+1]=arg;lib$es6$promise$asap$$len+=2;if(lib$es6$promise$asap$$len===2){if(lib$es6$promise$asap$$customSchedulerFn){lib$es6$promise$asap$$customSchedulerFn(lib$es6$promise$asap$$flush)}else{lib$es6$promise$asap$$scheduleFlush()}}};function lib$es6$promise$asap$$setScheduler(scheduleFn){lib$es6$promise$asap$$customSchedulerFn=scheduleFn}function lib$es6$promise$asap$$setAsap(asapFn){lib$es6$promise$asap$$asap=asapFn}var lib$es6$promise$asap$$browserWindow=typeof window!=="undefined"?window:undefined;var lib$es6$promise$asap$$browserGlobal=lib$es6$promise$asap$$browserWindow||{};var lib$es6$promise$asap$$BrowserMutationObserver=lib$es6$promise$asap$$browserGlobal.MutationObserver||lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver;var lib$es6$promise$asap$$isNode=typeof process!=="undefined"&&{}.toString.call(process)==="[object process]";var lib$es6$promise$asap$$isWorker=typeof Uint8ClampedArray!=="undefined"&&typeof importScripts!=="undefined"&&typeof MessageChannel!=="undefined";function lib$es6$promise$asap$$useNextTick(){return function(){process.nextTick(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useVertxTimer(){return function(){lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useMutationObserver(){var iterations=0;var observer=new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush);var node=document.createTextNode("");observer.observe(node,{characterData:true});return function(){node.data=iterations=++iterations%2}}function lib$es6$promise$asap$$useMessageChannel(){var channel=new MessageChannel;channel.port1.onmessage=lib$es6$promise$asap$$flush;return function(){channel.port2.postMessage(0)}}function lib$es6$promise$asap$$useSetTimeout(){return function(){setTimeout(lib$es6$promise$asap$$flush,1)}}var lib$es6$promise$asap$$queue=new Array(1e3);function lib$es6$promise$asap$$flush(){for(var i=0;i>> 0; - - if (len === 0) { - return -1; - } - n = 0; - if (arguments.length > 1) { - n = Number(arguments[1]); - if (n != n) { // shortcut for verifying if it's NaN - n = 0; - } else if (n != 0 && n != Infinity && n != -Infinity) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - for (k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - }; - } - function extend(destination, source) { destination.prototype = Object.create(source.prototype); destination.prototype.constructor = destination; return source.prototype; } - /** - * EventEmitter v4.2.3 - git.io/ee - * Oliver Caldwell - * MIT license - * @preserve - */ - - /** - * Class for managing events. - * Can be extended to provide event functionality in other classes. - * - * @class EventEmitter Manages event registering and emitting. - */ - function EventEmitter() {} - - // Shortcuts to improve speed and size - - // Easy access to the prototype - var proto = EventEmitter.prototype; - - /** - * Finds the index of the listener for the event in it's storage array. - * - * @param {Function[]} listeners Array of listeners to search through. - * @param {Function} listener Method to look for. - * @return {Number} Index of the specified listener, -1 if not found - * @api private - */ - function indexOfListener(listeners, listener) { - var i = listeners.length; - while (i--) { - if (listeners[i].listener === listener) { - return i; - } - } - - return -1; + if (!Array.prototype.indexOf) { + Array.prototype.indexOf=function(r){if(null==this)throw new TypeError;var t,e,n=Object(this),a=n.length>>>0;if(0===a)return-1;if(t=0,arguments.length>1&&(t=Number(arguments[1]),t!=t?t=0:0!=t&&1/0!=t&&t!=-1/0&&(t=(t>0||-1)*Math.floor(Math.abs(t)))),t>=a)return-1;for(e=t>=0?t:Math.max(a-Math.abs(t),0);a>e;e++)if(e in n&&n[e]===r)return e;return-1}; } - /** - * Alias a method while keeping the context correct, to allow for overwriting of target method. - * - * @param {String} name The name of the target method. - * @return {Function} The aliased method - * @api private - */ - function alias(name) { - return function aliasClosure() { - return this[name].apply(this, arguments); - }; + function fieldValue(object, name) { + try {return object[name];} catch (x) {return undefined;} } - /** - * Returns the listener array for the specified event. - * Will initialise the event object and listener arrays if required. - * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. - * Each property in the object response is an array of listener functions. - * - * @param {String|RegExp} evt Name of the event to return the listeners from. - * @return {Function[]|Object} All listener functions for the event. - */ - proto.getListeners = function getListeners(evt) { - var events = this._getEvents(); - var response; - var key; - - // Return a concatenated array of all matching events if - // the selector is a regular expression. - if (typeof evt === 'object') { - response = {}; - for (key in events) { - if (events.hasOwnProperty(key) && evt.test(key)) { - response[key] = events[key]; - } - } - } - else { - response = events[evt] || (events[evt] = []); - } - - return response; - }; - - /** - * Takes a list of listener objects and flattens it into a list of listener functions. - * - * @param {Object[]} listeners Raw listener objects. - * @return {Function[]} Just the listener functions. - */ - proto.flattenListeners = function flattenListeners(listeners) { - var flatListeners = []; - var i; - - for (i = 0; i < listeners.length; i += 1) { - flatListeners.push(listeners[i].listener); - } - - return flatListeners; - }; - - /** - * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. - * - * @param {String|RegExp} evt Name of the event to return the listeners from. - * @return {Object} All listener functions for an event in an object. - */ - proto.getListenersAsObject = function getListenersAsObject(evt) { - var listeners = this.getListeners(evt); - var response; - - if (listeners instanceof Array) { - response = {}; - response[evt] = listeners; - } - - return response || listeners; - }; - - /** - * Adds a listener function to the specified event. - * The listener will not be added if it is a duplicate. - * If the listener returns true then it will be removed after it is called. - * If you pass a regular expression as the event name then the listener will be added to all events that match it. - * - * @param {String|RegExp} evt Name of the event to attach the listener to. - * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addListener = function addListener(evt, listener) { - var listeners = this.getListenersAsObject(evt); - var listenerIsWrapped = typeof listener === 'object'; - var key; - - for (key in listeners) { - if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) { - listeners[key].push(listenerIsWrapped ? listener : { - listener: listener, - once: false - }); - } - } - - return this; - }; - - /** - * Alias of addListener - */ - proto.on = alias('addListener'); - - /** - * Semi-alias of addListener. It will add a listener that will be - * automatically removed after it's first execution. - * - * @param {String|RegExp} evt Name of the event to attach the listener to. - * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addOnceListener = function addOnceListener(evt, listener) { - //noinspection JSValidateTypes - return this.addListener(evt, { - listener: listener, - once: true - }); - }; - - /** - * Alias of addOnceListener. - */ - proto.once = alias('addOnceListener'); - - /** - * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. - * You need to tell it what event names should be matched by a regex. - * - * @param {String} evt Name of the event to create. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.defineEvent = function defineEvent(evt) { - this.getListeners(evt); - return this; - }; - - /** - * Uses defineEvent to define multiple events. - * - * @param {String[]} evts An array of event names to define. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.defineEvents = function defineEvents(evts) { - for (var i = 0; i < evts.length; i += 1) { - this.defineEvent(evts[i]); - } - return this; - }; - - /** - * Removes a listener function from the specified event. - * When passed a regular expression as the event name, it will remove the listener from all events that match it. - * - * @param {String|RegExp} evt Name of the event to remove the listener from. - * @param {Function} listener Method to remove from the event. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeListener = function removeListener(evt, listener) { - var listeners = this.getListenersAsObject(evt); - var index; - var key; - - for (key in listeners) { - if (listeners.hasOwnProperty(key)) { - index = indexOfListener(listeners[key], listener); - - if (index !== -1) { - listeners[key].splice(index, 1); - } - } - } - - return this; - }; - - /** - * Alias of removeListener - */ - proto.off = alias('removeListener'); - - /** - * Adds listeners in bulk using the manipulateListeners method. - * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. - * You can also pass it a regular expression to add the array of listeners to all events that match it. - * Yeah, this function does quite a bit. That's probably a bad thing. - * - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to add. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addListeners = function addListeners(evt, listeners) { - // Pass through to manipulateListeners - return this.manipulateListeners(false, evt, listeners); - }; - - /** - * Removes listeners in bulk using the manipulateListeners method. - * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. - * You can also pass it an event name and an array of listeners to be removed. - * You can also pass it a regular expression to remove the listeners from all events that match it. - * - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to remove. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeListeners = function removeListeners(evt, listeners) { - // Pass through to manipulateListeners - return this.manipulateListeners(true, evt, listeners); - }; - - /** - * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. - * The first argument will determine if the listeners are removed (true) or added (false). - * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. - * You can also pass it an event name and an array of listeners to be added/removed. - * You can also pass it a regular expression to manipulate the listeners of all events that match it. - * - * @param {Boolean} remove True if you want to remove listeners, false if you want to add. - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to add/remove. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) { - var i; - var value; - var single = remove ? this.removeListener : this.addListener; - var multiple = remove ? this.removeListeners : this.addListeners; - - // If evt is an object then pass each of it's properties to this method - if (typeof evt === 'object' && !(evt instanceof RegExp)) { - for (i in evt) { - if (evt.hasOwnProperty(i) && (value = evt[i])) { - // Pass the single listener straight through to the singular method - if (typeof value === 'function') { - single.call(this, i, value); - } - else { - // Otherwise pass back to the multiple function - multiple.call(this, i, value); - } - } - } - } - else { - // So evt must be a string - // And listeners must be an array of listeners - // Loop over it and pass each one to the multiple method - i = listeners.length; - while (i--) { - single.call(this, evt, listeners[i]); - } - } - - return this; - }; - - /** - * Removes all listeners from a specified event. - * If you do not specify an event then all listeners will be removed. - * That means every event will be emptied. - * You can also pass a regex to remove all events that match it. - * - * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeEvent = function removeEvent(evt) { - var type = typeof evt; - var events = this._getEvents(); - var key; - - // Remove different things depending on the state of evt - if (type === 'string') { - // Remove all listeners for the specified event - delete events[evt]; - } - else if (type === 'object') { - // Remove all events matching the regex. - for (key in events) { - //noinspection JSUnresolvedFunction - if (events.hasOwnProperty(key) && evt.test(key)) { - delete events[key]; - } - } - } - else { - // Remove all listeners in all events - delete this._events; - } - - return this; - }; - - /** - * Emits an event of your choice. - * When emitted, every listener attached to that event will be executed. - * If you pass the optional argument array then those arguments will be passed to every listener upon execution. - * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. - * So they will not arrive within the array on the other side, they will be separate. - * You can also pass a regular expression to emit to all events that match it. - * - * @param {String|RegExp} evt Name of the event to emit and execute listeners for. - * @param {Array} [args] Optional array of arguments to be passed to each listener. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.emitEvent = function emitEvent(evt, args) { - var listeners = this.getListenersAsObject(evt); - var listener; - var i; - var key; - var response; - - for (key in listeners) { - if (listeners.hasOwnProperty(key)) { - i = listeners[key].length; - - while (i--) { - // If the listener returns true then it shall be removed from the event - // The function is executed either with a basic call or an apply if there is an args array - listener = listeners[key][i]; - - if (listener.once === true) { - this.removeListener(evt, listener.listener); - } - - response = listener.listener.apply(this, args || []); - - if (response === this._getOnceReturnValue()) { - this.removeListener(evt, listener.listener); - } - } - } - } - - return this; - }; - - /** - * Alias of emitEvent - */ - proto.trigger = alias('emitEvent'); - - //noinspection JSValidateJSDoc,JSCommentMatchesSignature - /** - * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. - * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. - * - * @param {String|RegExp} evt Name of the event to emit and execute listeners for. - * @param {...*} Optional additional arguments to be passed to each listener. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.emit = function emit(evt) { - var args = Array.prototype.slice.call(arguments, 1); - return this.emitEvent(evt, args); - }; - - /** - * Sets the current value to check against when executing listeners. If a - * listeners return value matches the one set here then it will be removed - * after execution. This value defaults to true. - * - * @param {*} value The new value to check for when executing listeners. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.setOnceReturnValue = function setOnceReturnValue(value) { - this._onceReturnValue = value; - return this; - }; - - /** - * Fetches the current value to check against when executing listeners. If - * the listeners return value matches this one then it should be removed - * automatically. It will return true by default. - * - * @return {*|Boolean} The current value to check for or the default, true. - * @api private - */ - proto._getOnceReturnValue = function _getOnceReturnValue() { - if (this.hasOwnProperty('_onceReturnValue')) { - return this._onceReturnValue; - } - else { - return true; - } - }; - - /** - * Fetches the events object and creates one if required. - * - * @return {Object} The events storage object. - * @api private - */ - proto._getEvents = function _getEvents() { - return this._events || (this._events = {}); - }; - /** * Mixes in the given objects into the target object by copying the properties. * @param deep if the copy must be deep @@ -509,57 +57,35 @@ */ function mixin(deep, target, objects) { var result = target || {}; - - // Skip first 2 parameters (deep and target), and loop over the others - for (var i = 2; i < arguments.length; ++i) { + for (var i = 2; i < arguments.length; ++i) { // Skip first 2 parameters (deep and target), and loop over the others var object = arguments[i]; - if (object === undefined || object === null) { continue; } - for (var propName in object) { - //noinspection JSUnfilteredForInLoop var prop = fieldValue(object, propName); - //noinspection JSUnfilteredForInLoop var targ = fieldValue(result, propName); - - // Avoid infinite loops if (prop === target) { - continue; + continue; // Avoid infinite loops } - // Do not mixin undefined values if (prop === undefined) { - continue; + continue; // Do not mixin undefined values } - if (deep && typeof prop === 'object' && prop !== null) { if (prop instanceof Array) { - //noinspection JSUnfilteredForInLoop result[propName] = mixin(deep, targ instanceof Array ? targ : [], prop); } else { var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {}; - //noinspection JSUnfilteredForInLoop result[propName] = mixin(deep, source, prop); } } else { - //noinspection JSUnfilteredForInLoop result[propName] = prop; } } } - return result; } - function fieldValue(object, name) { - try { - return object[name]; - } catch (x) { - return undefined; - } - } - function endsWith(value, suffix) { return value.indexOf(suffix, value.length - suffix.length) !== -1; } @@ -613,15 +139,14 @@ function Centrifuge(options) { this._sockjs = false; - this._sockjsVersion = null; this._status = 'disconnected'; this._reconnect = true; + this._reconnecting = false; this._transport = null; - this._latency = null; - this._latencyStart = null; + this._transportName = null; this._messageId = 0; - this._clientId = null; - this._subscriptions = {}; + this._clientID = null; + this._subs = {}; this._lastMessageID = {}; this._messages = []; this._isBatching = false; @@ -629,26 +154,17 @@ this._authChannels = {}; this._refreshTimeout = null; this._retries = 0; + this._callbacks = {}; this._config = { retry: 1000, maxRetry: 20000, + timeout: 5000, info: "", - resubscribe: false, + resubscribe: true, debug: false, insecure: false, server: null, privateChannelPrefix: "$", - protocols_whitelist: [ - 'websocket', - 'xdr-streaming', - 'xhr-streaming', - 'iframe-eventsource', - 'iframe-htmlfile', - 'xdr-polling', - 'xhr-polling', - 'iframe-xhr-polling', - 'jsonp-polling' - ], transports: [ 'websocket', 'xdr-streaming', @@ -661,11 +177,11 @@ 'iframe-xhr-polling', 'jsonp-polling' ], - refreshEndpoint: "/centrifuge/refresh", + refreshEndpoint: "/centrifuge/refresh/", refreshHeaders: {}, refreshParams: {}, refreshTransport: "ajax", - authEndpoint: "/centrifuge/auth", + authEndpoint: "/centrifuge/auth/", authHeaders: {}, authParams: {}, authTransport: "ajax" @@ -729,11 +245,9 @@ } query += encodeURIComponent(i) + "=" + encodeURIComponent(params[i]); } - if (query.length > 0) { query = "?" + query; } - xhr.open("POST", url + query, true); // add request headers @@ -752,14 +266,14 @@ data = JSON.parse(xhr.responseText); parsed = true; } catch (e) { - callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); + callback(true, 'JSON returned was invalid, yet status code was 200. Data was: ' + xhr.responseText); } if (parsed) { // prevents double execution. callback(false, data); } } else { - self._log("Couldn't get auth info from your webapp", xhr.status); + self._log("Couldn't get auth info from application", xhr.status); callback(true, xhr.status); } } @@ -828,7 +342,6 @@ throw 'include SockJS client library before Centrifuge javascript client library or use raw Websocket connection endpoint'; } this._sockjs = true; - this._sockjsVersion = SockJS.version; } else if (endsWith(this._config.url, 'connection/websocket')) { this._debug("client will connect to raw Websocket endpoint"); this._config.url = this._config.url.replace("http://", "ws://"); @@ -844,7 +357,6 @@ this._debug("SockJS found, client will connect to SockJS endpoint"); this._config.url += "/connection"; this._sockjs = true; - this._sockjsVersion = SockJS.version; } } }; @@ -857,25 +369,21 @@ }; centrifugeProto._isDisconnected = function () { - return this._isConnected() === false; + return this._status === 'disconnected'; }; - centrifugeProto._isConnected = function () { - return this._status === 'connected'; + centrifugeProto._isConnecting = function() { + return this._status === 'connecting'; }; - centrifugeProto._isConnecting = function () { - return this._status === 'connecting'; + centrifugeProto._isConnected = function () { + return this._status === 'connected'; }; centrifugeProto._nextMessageId = function () { return ++this._messageId; }; - centrifugeProto._clearSubscriptions = function () { - this._subscriptions = {}; - }; - centrifugeProto._resetRetry = function() { this._debug("reset retries count to 0"); this._retries = 0; @@ -887,18 +395,43 @@ return interval; }; + centrifugeProto._clearConnectedState = function (reconnect) { + self._clientID = null; + + // fire errbacks of registered calls. + for (var uid in this._callbacks) { + var callbacks = this._callbacks[uid]; + var errback = callbacks["errback"]; + if (!errback) { + continue; + } + errback(this._createErrorObject("disconnected", "retry")); + } + this._callbacks = {}; + + // fire unsubscribe events + for (var channel in this._subs) { + var sub = this._subs[channel]; + if (reconnect) { + if (sub._isSuccess()) { + sub._triggerUnsubscribe(); + } + sub._setSubscribing(); + } else { + sub._setUnsubscribed(); + } + } + + if (!this._config.resubscribe || !this._reconnect) { + // completely clear connected state + this._subs = {}; + } + }; + centrifugeProto._send = function (messages) { - // We must be sure that the messages have a clientId. - // This is not guaranteed since the handshake may take time to return - // (and hence the clientId is not known yet) and the application - // may create other messages. if (messages.length === 0) { return; } - for (var i = 0; i < messages.length; ++i) { - var message = messages[i]; - message.uid = '' + this._nextMessageId(); - } this._debug('Send', messages); this._transport.send(JSON.stringify(messages)); }; @@ -906,18 +439,13 @@ centrifugeProto._connect = function (callback) { if (this.isConnected()) { + this._debug("connect called when already connected"); return; } - this._clientId = null; - - this._reconnect = true; - - if (!this._config.resubscribe) { - this._clearSubscriptions(); - } - this._setStatus('connecting'); + this._clientID = null; + this._reconnect = true; var self = this; @@ -925,15 +453,11 @@ this.on('connect', callback); } + // detect transport to use - SockJS or raw Websocket if (this._sockjs === true) { - //noinspection JSUnresolvedFunction - var sockjsOptions = {}; - if (startsWith(this._sockjsVersion, "1.")) { - sockjsOptions["transports"] = this._config.transports; - } else { - this._log("SockJS <= 0.3.4 is deprecated, use SockJS >= 1.0.0 instead"); - sockjsOptions["protocols_whitelist"] = this._config.protocols_whitelist; - } + var sockjsOptions = { + "transports": this._config.transports + }; if (this._config.server !== null) { sockjsOptions['server'] = this._config.server; } @@ -942,20 +466,26 @@ this._transport = new WebSocket(this._config.url); } - this._setStatus('connecting'); - this._transport.onopen = function () { + self._reconnecting = false; + + if (self._sockjs) { + self._transportName = self._transport._transport.transportName; + } else { + self._transportName = "raw-websocket"; + } + self._resetRetry(); if (!isString(self._config.user)) { - self._debug("user expected to be string"); + self._log("user expected to be string"); } if (!isString(self._config.info)) { - self._debug("info expected to be string"); + self._log("info expected to be string"); } - var centrifugeMessage = { + var msg = { 'method': 'connect', 'params': { 'user': self._config.user, @@ -964,35 +494,25 @@ }; if (!self._config.insecure) { - centrifugeMessage["params"]["timestamp"] = self._config.timestamp; - centrifugeMessage["params"]["token"] = self._config.token; + // in insecure client mode we don't need timestamp and token. + msg["params"]["timestamp"] = self._config.timestamp; + msg["params"]["token"] = self._config.token; if (!isString(self._config.timestamp)) { - self._debug("timestamp expected to be string"); + self._log("timestamp expected to be string"); } if (!isString(self._config.token)) { - self._debug("token expected to be string"); + self._log("token expected to be string"); } } - self.send(centrifugeMessage); - self._latencyStart = new Date(); + self._addMessage(msg); }; this._transport.onerror = function (error) { - self._debug(error); + self._debug("transport level error", error); }; this._transport.onclose = function () { - self._setStatus('disconnected'); - self.trigger('disconnect'); - if (self._reconnect === true) { - var interval = self._getRetryInterval(); - self._debug("reconnect after " + interval + " milliseconds"); - window.setTimeout(function () { - if (self._reconnect === true) { - self._connect.call(self); - } - }, interval); - } + self._disconnect("connection closed", true, false); }; this._transport.onmessage = function (event) { @@ -1003,89 +523,211 @@ }; }; - centrifugeProto._disconnect = function (shouldReconnect) { + centrifugeProto._disconnect = function (reason, shouldReconnect, closeTransport) { + this._debug("disconnected:", reason, shouldReconnect); var reconnect = shouldReconnect || false; - this._clientId = null; - this._setStatus('disconnected'); if (reconnect === false) { - this._subscriptions = {}; this._reconnect = false; } - this._transport.close(); - }; - centrifugeProto._getSubscription = function (channel) { - var subscription; - subscription = this._subscriptions[channel]; - if (!subscription) { - return null; + this._clearConnectedState(shouldReconnect); + + if (!this.isDisconnected()) { + this._setStatus('disconnected'); + var disconnectContext = { + "reason": reason, + "reconnect": reconnect + }; + if (this._reconnecting === false) { + this.trigger('disconnect', [disconnectContext]); + } } - return subscription; - }; - centrifugeProto._removeSubscription = function (channel) { - try { - delete this._subscriptions[channel]; - } catch (e) { - this._debug('nothing to delete for channel ', channel); + if (closeTransport) { + this._transport.close(); } - try { - delete this._authChannels[channel]; - } catch (e) { - this._debug('nothing to delete from authChannels for channel ', channel); + + var self = this; + if (shouldReconnect === true && self._reconnect === true) { + self._reconnecting = true; + var interval = self._getRetryInterval(); + self._debug("reconnect after " + interval + " milliseconds"); + window.setTimeout(function () { + if (self._reconnect === true) { + self._connect.call(self); + } + }, interval); } }; - centrifugeProto._connectResponse = function (message) { - - if (this._latencyStart !== null) { - var latencyEnd = new Date(); - this._latency = latencyEnd.getTime() - this._latencyStart.getTime(); - this._latencyStart = null; - } + centrifugeProto._refresh = function () { + // ask web app for connection parameters - user ID, + // timestamp, info and token + var self = this; + this._debug('refresh credentials'); - if (this.isConnected()) { - return; - } - if (!errorExists(message)) { - if (!message.body) { + var cb = function(error, data) { + if (error === true) { + // 403 or 500 - does not matter - if connection check activated then Centrifugo + // will disconnect client eventually + self._debug("error getting connect parameters", data); + if (self._refreshTimeout) { + window.clearTimeout(self._refreshTimeout); + } + self._refreshTimeout = window.setTimeout(function(){ + self._refresh.call(self); + }, 3000); + return; + } + self._config.user = data.user; + self._config.timestamp = data.timestamp; + self._config.info = data.info; + self._config.token = data.token; + if (self.isDisconnected()) { + self._debug("credentials refreshed, connect from scratch"); + self._connect(); + } else { + self._debug("send refreshed credentials"); + var msg = { + "method": "refresh", + "params": { + 'user': self._config.user, + 'timestamp': self._config.timestamp, + 'info': self._config.info, + 'token': self._config.token + } + }; + self._addMessage(msg); + } + }; + + var transport = this._config.refreshTransport.toLowerCase(); + if (transport === "ajax") { + this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); + } else if (transport === "jsonp") { + this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); + } else { + throw 'Unknown refresh transport ' + transport; + } + }; + + centrifugeProto._subscribe = function(sub) { + + var channel = sub.channel; + + if (!(channel in this._subs)) { + this._subs[channel] = sub; + } + + if (!this.isConnected()) { + // subscribe will be called later + sub._setNew(); + return; + } + + sub._setSubscribing(); + + var msg = { + "method": "subscribe", + "params": { + "channel": channel + } + }; + + // If channel name does not start with privateChannelPrefix - then we + // can just send subscription message to Centrifuge. If channel name + // starts with privateChannelPrefix - then this is a private channel + // and we should ask web application backend for permission first. + if (startsWith(channel, this._config.privateChannelPrefix)) { + // private channel + if (this._isAuthBatching) { + this._authChannels[channel] = true; + } else { + this.startAuthBatching(); + this._subscribe(sub); + this.stopAuthBatching(); + } + } else { + var recover = this._recover(channel); + if (recover === true) { + msg["params"]["recover"] = true; + msg["params"]["last"] = this._getLastID(channel); + } + this._addMessage(msg); + } + }; + + centrifugeProto._unsubscribe = function(sub) { + if (this.isConnected()) { + // No need to unsubscribe in disconnected state - i.e. client already unsubscribed. + var msg = { + "method": "unsubscribe", + "params": { + "channel": sub.channel + } + }; + this._addMessage(msg); + } + }; + + centrifugeProto._getSub = function(channel) { + var sub = this._subs[channel]; + if (!sub) { + return null; + } + return sub; + }; + + centrifugeProto._connectResponse = function (message) { + + if (this.isConnected()) { + return; + } + + if (!errorExists(message)) { + if (!message.body) { return; } if (message.body.expires) { var isExpired = message.body.expired; if (isExpired) { - this.refresh(); + this._refresh(); return; } } - this._clientId = message.body.client; + this._clientID = message.body.client; this._setStatus('connected'); - this.trigger('connect', [message]); + if (this._refreshTimeout) { window.clearTimeout(this._refreshTimeout); } if (message.body.expires) { var self = this; this._refreshTimeout = window.setTimeout(function() { - self.refresh.call(self); + self._refresh.call(self); }, message.body.ttl * 1000); } - } else { - this.trigger('error', [message]); - this.trigger('connect:error', [message]); - } - if (this._config.resubscribe) { - this.startBatching(); - this.startAuthBatching(); - for (var i in this._subscriptions) { - var sub = this._subscriptions[i]; - sub.subscribe(); + if (this._config.resubscribe) { + this.startBatching(); + this.startAuthBatching(); + for (var channel in this._subs) { + console.log(channel); + var sub = this._subs[channel]; + this._subscribe(sub); + } + this.stopAuthBatching(); + this.stopBatching(true); } - this.stopAuthBatching(); - this.stopBatching(true); - } + var connectContext = { + "client": message.body.client, + "transport": this._transportName + }; + this.trigger('connect', [connectContext]); + } else { + this.trigger('error', [{"message": message}]); + } }; centrifugeProto._disconnectResponse = function (message) { @@ -1094,168 +736,216 @@ if ("reconnect" in message.body) { shouldReconnect = message.body["reconnect"]; } - this.disconnect(shouldReconnect); + var reason = ""; if ("reason" in message.body) { - this._debug("disconnected:", message.body["reason"]); + reason = message.body["reason"]; } + this._disconnect(reason, shouldReconnect, true); } else { - this.trigger('error', [message]); - this.trigger('disconnect:error', [message.error]); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._subscribeResponse = function (message) { - if (errorExists(message)) { - this.trigger('error', [message]); - } var body = message.body; if (body === null) { return; } var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } + + if (!sub._isSubscribing()) { + return; + } + if (!errorExists(message)) { - subscription.trigger('subscribe:success', [body]); - subscription.trigger('ready', [body]); + sub._setSubscribeSuccess(); var messages = body["messages"]; if (messages && messages.length > 0) { + // handle missed messages for (var i in messages.reverse()) { this._messageResponse({body: messages[i]}); } } else { if ("last" in body) { + // no missed messages found so set last message id from body. this._lastMessageID[channel] = body["last"]; } } } else { - subscription.trigger('subscribe:error', [message.error]); - subscription.trigger('error', [message]); + this.trigger('error', [{"message": message}]); + sub._setSubscribeError(this._errorObjectFromMessage(message)); } }; centrifugeProto._unsubscribeResponse = function (message) { + var uid = message.uid; var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } + if (!errorExists(message)) { - subscription.trigger('unsubscribe', [body]); - this._removeSubscription(channel); + if (!uid) { + // unsubscribe command from server – unsubscribe all current subs + sub._setUnsubscribed(); + } + // ignore client initiated successful unsubscribe responses as we + // already unsubscribed on client level. + } else { + this.trigger('error', [{"message": message}]); } }; centrifugeProto._publishResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('publish:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('publish:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._presenceResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('presence', [body]); - subscription.trigger('presence:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('presence:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._historyResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('history', [body]); - subscription.trigger('history:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('history:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._joinResponse = function(message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } - subscription.trigger('join', [body]); + sub.trigger('join', [body]); }; centrifugeProto._leaveResponse = function(message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } - subscription.trigger('leave', [body]); + sub.trigger('leave', [body]); }; centrifugeProto._messageResponse = function (message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (subscription === null) { - return; - } + // keep last uid received from channel. this._lastMessageID[channel] = body["uid"]; - subscription.trigger('message', [body]); + + var sub = this._getSub(channel); + if (!sub) { + return; + } + sub.trigger('message', [body]); }; centrifugeProto._refreshResponse = function (message) { if (this._refreshTimeout) { window.clearTimeout(this._refreshTimeout); } - if (message.body.expires) { - var self = this; - var isExpired = message.body.expired; - if (isExpired) { - self._refreshTimeout = window.setTimeout(function(){ - self.refresh.call(self); - }, 3000 + Math.round(Math.random() * 1000)); - return; + if (!errorExists(message)) { + if (message.body.expires) { + var self = this; + var isExpired = message.body.expired; + if (isExpired) { + self._refreshTimeout = window.setTimeout(function () { + self._refresh.call(self); + }, 3000 + Math.round(Math.random() * 1000)); + return; + } + this._clientID = message.body.client; + self._refreshTimeout = window.setTimeout(function () { + self._refresh.call(self); + }, message.body.ttl * 1000); } - this._clientId = message.body.client; - self._refreshTimeout = window.setTimeout(function () { - self.refresh.call(self); - }, message.body.ttl * 1000); + } else { + this.trigger('error', [{"message": message}]); } }; centrifugeProto._dispatchMessage = function(message) { if (message === undefined || message === null) { + this._debug("dispatch: got undefined or null message"); return; } var method = message.method; if (!method) { + this._debug("dispatch: got message with empty method"); return; } @@ -1296,12 +986,14 @@ this._messageResponse(message); break; default: + this._debug("dispatch: got message with unknown method" + method); break; } }; centrifugeProto._receive = function (data) { if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) { + // array of responses received for (var i in data) { if (data.hasOwnProperty(i)) { var msg = data[i]; @@ -1309,6 +1001,7 @@ } } } else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) { + // one response received this._dispatchMessage(data); } }; @@ -1320,11 +1013,11 @@ }; centrifugeProto._ping = function () { - var centrifugeMessage = { + var msg = { "method": "ping", "params": {} }; - this.send(centrifugeMessage); + this._addMessage(msg); }; centrifugeProto._recover = function(channel) { @@ -1342,16 +1035,53 @@ } }; - /* PUBLIC API */ + centrifugeProto._createErrorObject = function(err, advice) { + var errObject = { + "error": err + }; + if (advice) { + errObject["advice"] = advice; + } + return errObject; + }; + + centrifugeProto._errorObjectFromMessage = function(message) { + var err = message.error; + var advice = message["advice"]; + return this._createErrorObject(err, advice); + }; + + centrifugeProto._registerCall = function(uid, callback, errback) { + var self = this; + this._callbacks[uid] = { + "callback": callback, + "errback": errback + }; + setTimeout(function() { + delete self._callbacks[uid]; + if (isFunction(errback)) { + errback(self._createErrorObject("timeout", "retry")); + } + }, this._config.timeout); + }; + + centrifugeProto._addMessage = function (message) { + var uid = '' + this._nextMessageId(); + message.uid = uid; + if (this._isBatching === true) { + this._messages.push(message); + } else { + this._send([message]); + } + return uid; + }; centrifugeProto.getClientId = function () { - return this._clientId; + return this._clientID; }; centrifugeProto.isConnected = centrifugeProto._isConnected; - centrifugeProto.isConnecting = centrifugeProto._isConnecting; - centrifugeProto.isDisconnected = centrifugeProto._isDisconnected; centrifugeProto.configure = function (configuration) { @@ -1360,20 +1090,12 @@ centrifugeProto.connect = centrifugeProto._connect; - centrifugeProto.disconnect = centrifugeProto._disconnect; - - centrifugeProto.getSubscription = centrifugeProto._getSubscription; + centrifugeProto.disconnect = function() { + this._disconnect("client", false, true); + }; centrifugeProto.ping = centrifugeProto._ping; - centrifugeProto.send = function (message) { - if (this._isBatching === true) { - this._messages.push(message); - } else { - this._send([message]); - } - }; - centrifugeProto.startBatching = function () { // start collecting messages without sending them to Centrifuge until flush // method called @@ -1400,7 +1122,7 @@ this._isAuthBatching = true; }; - centrifugeProto.stopAuthBatching = function(callback) { + centrifugeProto.stopAuthBatching = function() { // create request to authEndpoint with collected private channels // to ask if this client can subscribe on each channel this._isAuthBatching = false; @@ -1409,17 +1131,14 @@ var channels = []; for (var channel in authChannels) { - var subscription = this.getSubscription(channel); - if (!subscription) { + var sub = this._getSub(channel); + if (!sub) { continue; } channels.push(channel); } if (channels.length == 0) { - if (callback) { - callback(); - } return; } @@ -1437,23 +1156,30 @@ var channel = channels[i]; self._subscribeResponse({ "error": "authorization request failed", + "advice": "fix", "body": { "channel": channel } }); } - if (callback) { - callback(); - } return; } + + // try to send all subscriptions in one request. + var batch = false; + if (!self._isBatching) { + self.startBatching(); + batch = true; + } + for (var i in channels) { var channel = channels[i]; var channelResponse = data[channel]; if (!channelResponse) { // subscription:error self._subscribeResponse({ - "error": 404, + "error": "channel not found in authorization response", + "advice": "fix", "body": { "channel": channel } @@ -1461,7 +1187,7 @@ continue; } if (!channelResponse.status || channelResponse.status === 200) { - var centrifugeMessage = { + var msg = { "method": "subscribe", "params": { "channel": channel, @@ -1472,10 +1198,10 @@ }; var recover = self._recover(channel); if (recover === true) { - centrifugeMessage["params"]["recover"] = true; - centrifugeMessage["params"]["last"] = self._getLastID(channel); + msg["params"]["recover"] = true; + msg["params"]["last"] = self._getLastID(channel); } - self.send(centrifugeMessage); + self._addMessage(msg); } else { self._subscribeResponse({ "error": channelResponse.status, @@ -1485,9 +1211,11 @@ }); } } - if (callback) { - callback(); + + if (batch) { + self.stopBatching(true); } + }; var transport = this._config.authTransport.toLowerCase(); @@ -1500,239 +1228,274 @@ } }; - centrifugeProto.subscribe = function (channel, callback) { - + centrifugeProto.subscribe = function (channel, events) { if (arguments.length < 1) { throw 'Illegal arguments number: required 1, got ' + arguments.length; } if (!isString(channel)) { throw 'Illegal argument type: channel must be a string'; } - if (!this._config.resubscribe && this.isDisconnected()) { - throw 'Can not subscribe in disconnected state'; + if (!this._config.resubscribe && !this.isConnected()) { + throw 'Can not only subscribe in connected state when resubscribe option is off'; } - var current_subscription = this.getSubscription(channel); + var currentSub = this._getSub(channel); - if (current_subscription !== null) { - return current_subscription; + if (currentSub !== null) { + currentSub._setEvents(events); + return currentSub; } else { - var subscription = new Subscription(this, channel, callback); - this._subscriptions[channel] = subscription; - subscription.subscribe(); - return subscription; + var sub = new Sub(this, channel, events); + this._subs[channel] = sub; + sub.subscribe(); + return sub; } }; - centrifugeProto.unsubscribe = function (channel) { - if (arguments.length < 1) { - throw 'Illegal arguments number: required 1, got ' + arguments.length; + var _STATE_NEW = 0; + var _STATE_SUBSCRIBING = 1; + var _STATE_SUCCESS = 2; + var _STATE_ERROR = 3; + var _STATE_UNSUBSCRIBED = 4; + + function Sub(centrifuge, channel, events) { + this._status = _STATE_NEW; + this._error = null; + this._centrifuge = centrifuge; + this.channel = channel; + this._setEvents(events); + this._isResubscribe = false; + this._ready = false; + this._promise = null; + this._initializePromise(); + } + + extend(Sub, EventEmitter); + + var subProto = Sub.prototype; + + subProto._initializePromise = function() { + this._ready = false; + var self = this; + this._promise = new Promise(function(resolve, reject) { + self._resolve = function(value) { + self._ready = true; + resolve(value); + }; + self._reject = function(err) { + self._ready = true; + reject(err); + }; + }); + }; + + subProto._setEvents = function(events) { + if (!events) { + return; } - if (!isString(channel)) { - throw 'Illegal argument type: channel must be a string'; + if (isFunction(events)) { + this.on("message", events); + } else if (Object.prototype.toString.call(events) === Object.prototype.toString.call({})) { + var knownEvents = [ + "message", "join", "leave", "unsubscribe", + "subscribe", "error" + ]; + for (var i in knownEvents) { + var ev = knownEvents[i]; + if (ev in events) { + this.on(ev, events[ev]); + } + } } + }; - var subscription = this.getSubscription(channel); - if (subscription !== null) { - subscription.unsubscribe(); - } + subProto._isNew = function() { + return this._status === _STATE_NEW; }; - centrifugeProto.publish = function (channel, data, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.publish(data, callback); - return subscription; + subProto._isUnsubscribed = function() { + return this._status === _STATE_UNSUBSCRIBED; }; - centrifugeProto.presence = function (channel, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.presence(callback); - return subscription; + subProto._isSubscribing = function() { + return this._status === _STATE_SUBSCRIBING; }; - centrifugeProto.history = function (channel, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.history(callback); - return subscription; + subProto._isReady = function() { + return this._status === _STATE_SUCCESS || this._status === _STATE_ERROR; }; - centrifugeProto.refresh = function () { - // ask web app for connection parameters - user ID, - // timestamp, info and token - var self = this; - this._debug('refresh credentials'); + subProto._isSuccess = function() { + return this._status === _STATE_SUCCESS; + }; - var cb = function(error, data) { - if (error === true) { - // 403 or 500 - does not matter - if connection check activated then Centrifugo - // will disconnect client eventually - self._debug("error getting connect parameters", data); - if (self._refreshTimeout) { - window.clearTimeout(self._refreshTimeout); - } - self._refreshTimeout = window.setTimeout(function(){ - self.refresh.call(self); - }, 3000); - return; - } - self._config.user = data.user; - self._config.timestamp = data.timestamp; - self._config.info = data.info; - self._config.token = data.token; - if (self.isDisconnected()) { - self._debug("credentials refreshed, connect from scratch"); - self._connect(); - } else { - self._debug("send refreshed credentials"); - var centrifugeMessage = { - "method": "refresh", - "params": { - 'user': self._config.user, - 'timestamp': self._config.timestamp, - 'info': self._config.info, - 'token': self._config.token - } - }; - self.send(centrifugeMessage); - } - }; + subProto._isError = function() { + return this._status === _STATE_ERROR; + }; - var transport = this._config.refreshTransport.toLowerCase(); - if (transport === "ajax") { - this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); - } else if (transport === "jsonp") { - this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); - } else { - throw 'Unknown refresh transport ' + transport; - } + subProto._setNew = function() { + this._status = _STATE_NEW; }; - function Subscription(centrifuge, channel, callback) { - /** - * The constructor for a centrifuge object, identified by an optional name. - * The default name is the string 'default'. - * @param name the optional name of this centrifuge object - */ - this._centrifuge = centrifuge; - this.channel = channel; - this.callback = callback; - if (this.callback) { - this.on('message', this.callback); + subProto._setSubscribing = function() { + if (this._ready === true) { + // new promise for this subscription + this._initializePromise(); + this._isResubscribe = true; } - } - - extend(Subscription, EventEmitter); + this._status = _STATE_SUBSCRIBING; + }; - var subscriptionProto = Subscription.prototype; + subProto._setSubscribeSuccess = function() { + if (this._status == _STATE_SUCCESS) { + return; + } + this._status = _STATE_SUCCESS; + var successContext = this._getSubscribeSuccessContext(); + this.trigger("subscribe", [successContext]); + this._resolve(successContext); + }; - subscriptionProto.getChannel = function () { - return this.channel; + subProto._setSubscribeError = function(err) { + if (this._status == _STATE_ERROR) { + return; + } + this._status = _STATE_ERROR; + this._error = err; + var errContext = this._getSubscribeErrorContext(); + this.trigger("error", [errContext]); + this._reject(errContext); }; - subscriptionProto.getCentrifuge = function () { - return this._centrifuge; + subProto._triggerUnsubscribe = function() { + var unsubscribeContext = { + "channel": this.channel + }; + this.trigger("unsubscribe", [unsubscribeContext]); }; - subscriptionProto.subscribe = function () { - /* - If channel name does not start with privateChannelPrefix - then we - can just send subscription message to Centrifuge. If channel name - starts with privateChannelPrefix - then this is a private channel - and we should ask web application backend for permission first. - */ - if (!this._centrifuge.isConnected()) { + subProto._setUnsubscribed = function() { + if (this._status == _STATE_UNSUBSCRIBED) { return; } - - var centrifugeMessage = { - "method": "subscribe", - "params": { - "channel": this.channel - } + this._status = _STATE_UNSUBSCRIBED; + this._triggerUnsubscribe(); + }; + + subProto._getSubscribeSuccessContext = function() { + return { + "channel": this.channel, + "isResubscribe": this._isResubscribe }; + }; - if (startsWith(this.channel, this._centrifuge._config.privateChannelPrefix)) { - // private channel - if (this._centrifuge._isAuthBatching) { - this._centrifuge._authChannels[this.channel] = true; + subProto._getSubscribeErrorContext = function() { + var subscribeErrorContext = this._error; + subscribeErrorContext["channel"] = this.channel; + subscribeErrorContext["isResubscribe"] = this._isResubscribe; + return subscribeErrorContext; + }; + + subProto.ready = function(callback, errback) { + if (this._ready) { + if (this._isSuccess()) { + callback(this._getSubscribeSuccessContext()); } else { - this._centrifuge.startAuthBatching(); - this.subscribe(); - this._centrifuge.stopAuthBatching(); + errback(this._getSubscribeErrorContext()); } - } else { - var recover = this._centrifuge._recover(this.channel); - if (recover === true) { - centrifugeMessage["params"]["recover"] = true; - centrifugeMessage["params"]["last"] = this._centrifuge._getLastID(this.channel); - } - this._centrifuge.send(centrifugeMessage); } }; - subscriptionProto.unsubscribe = function () { - this._centrifuge._removeSubscription(this.channel); - if (this._centrifuge.isConnected()) { - var centrifugeMessage = { - "method": "unsubscribe", - "params": { - "channel": this.channel - } - }; - this._centrifuge.send(centrifugeMessage); + subProto.subscribe = function() { + if (this._status == _STATE_SUCCESS) { + return; } + this._centrifuge._subscribe(this); + return this; }; - subscriptionProto.publish = function (data, callback) { - var centrifugeMessage = { - "method": "publish", - "params": { - "channel": this.channel, - "data": data + subProto.unsubscribe = function () { + this._setUnsubscribed(); + this._centrifuge._unsubscribe(this); + }; + + subProto.publish = function (data) { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('publish:success', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "publish", + "params": { + "channel": self.channel, + "data": data + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; - subscriptionProto.presence = function (callback) { - var centrifugeMessage = { - "method": "presence", - "params": { - "channel": this.channel + subProto.presence = function() { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('presence', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "presence", + "params": { + "channel": self.channel + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; - subscriptionProto.history = function (callback) { - var centrifugeMessage = { - "method": "history", - "params": { - "channel": this.channel + subProto.history = function() { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('history', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "history", + "params": { + "channel": self.channel + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; // Expose the class either via AMD, CommonJS or the global object diff --git a/centrifuge.min.js b/centrifuge.min.js index e987b06f..8f2c5151 100644 --- a/centrifuge.min.js +++ b/centrifuge.min.js @@ -1 +1 @@ -(function(){"use strict";function e(e,t){return e.prototype=Object.create(t.prototype),e.prototype.constructor=e,t.prototype}function t(){}function n(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function i(e){return function(){return this[e].apply(this,arguments)}}function s(e,t){for(var n=t||{},i=2;in&&(s=n),Math.floor((1-i)*s)}function g(e){return"error"in e&&null!==e.error&&""!==e.error}function _(e){this._sockjs=!1,this._sockjsVersion=null,this._status="disconnected",this._reconnect=!0,this._transport=null,this._latency=null,this._latencyStart=null,this._messageId=0,this._clientId=null,this._subscriptions={},this._lastMessageID={},this._messages=[],this._isBatching=!1,this._isAuthBatching=!1,this._authChannels={},this._refreshTimeout=null,this._retries=0,this._config={retry:1e3,maxRetry:2e4,info:"",resubscribe:!1,debug:!1,insecure:!1,server:null,privateChannelPrefix:"$",protocols_whitelist:["websocket","xdr-streaming","xhr-streaming","iframe-eventsource","iframe-htmlfile","xdr-polling","xhr-polling","iframe-xhr-polling","jsonp-polling"],transports:["websocket","xdr-streaming","xhr-streaming","eventsource","iframe-eventsource","iframe-htmlfile","xdr-polling","xhr-polling","iframe-xhr-polling","jsonp-polling"],refreshEndpoint:"/centrifuge/refresh",refreshHeaders:{},refreshParams:{},refreshTransport:"ajax",authEndpoint:"/centrifuge/auth",authHeaders:{},authParams:{},authTransport:"ajax"},e&&this.configure(e)}function d(e,t,n){this._centrifuge=e,this.channel=t,this.callback=n,this.callback&&this.on("message",this.callback)}Object.create||(Object.create=function(){function e(){}return function(t){if(1!=arguments.length)throw new Error("Object.create implementation only accepts one parameter.");return e.prototype=t,new e}}()),Array.prototype.indexOf||(Array.prototype.indexOf=function(e){if(null==this)throw new TypeError;var t,n,i=Object(this),s=i.length>>>0;if(0===s)return-1;if(t=0,arguments.length>1&&(t=Number(arguments[1]),t!=t?t=0:0!=t&&1/0!=t&&t!=-1/0&&(t=(t>0||-1)*Math.floor(Math.abs(t)))),t>=s)return-1;for(n=t>=0?t:Math.max(s-Math.abs(t),0);s>n;n++)if(n in i&&i[n]===e)return n;return-1});var p=t.prototype;p.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},p.flattenListeners=function(e){var t,n=[];for(t=0;t0&&this._log("Only AJAX request allows to send custom headers, it's not possible with JSONP."),self._debug("sending JSONP request to",e);var r=_._nextAuthCallbackID.toString();_._nextAuthCallbackID++;var o=window.document,c=o.createElement("script");_._authCallbacks[r]=function(e){s(!1,e),delete _[r]};var a="";for(var u in t)a.length>0&&(a+="&"),a+=encodeURIComponent(u)+"="+encodeURIComponent(t[u]);var h="Centrifuge._authCallbacks['"+r+"']";c.src=this._config.authEndpoint+"?callback="+encodeURIComponent(h)+"&data="+encodeURIComponent(JSON.stringify(i))+"&"+a;var f=o.getElementsByTagName("head")[0]||o.documentElement;f.insertBefore(c,f.firstChild)},b._ajax=function(e,t,n,i,s){var r=this;r._debug("sending AJAX request to",e);var o=window.XMLHttpRequest?new window.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),c="";for(var a in t)c.length>0&&(c+="&"),c+=encodeURIComponent(a)+"="+encodeURIComponent(t[a]);c.length>0&&(c="?"+c),o.open("POST",e+c,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","application/json");for(var u in n)o.setRequestHeader(u,n[u]);return o.onreadystatechange=function(){if(4===o.readyState)if(200===o.status){var e,t=!1;try{e=JSON.parse(o.responseText),t=!0}catch(n){s(!0,"JSON returned from webapp was invalid, yet status code was 200. Data was: "+o.responseText)}t&&s(!1,e)}else r._log("Couldn't get auth info from your webapp",o.status),s(!0,o.status)},setTimeout(function(){o.send(JSON.stringify(i))},20),o},b._log=function(){f("info",arguments)},b._debug=function(){this._config.debug===!0&&f("debug",arguments)},b._configure=function(e){if(this._debug("Configuring centrifuge object with",e),e||(e={}),this._config=s(!1,this._config,e),!this._config.url)throw"Missing required configuration parameter 'url' specifying server URL";if(!this._config.user&&""!==this._config.user){if(!this._config.insecure)throw"Missing required configuration parameter 'user' specifying user's unique ID in your application";this._debug("user not found but this is OK for insecure mode - anonymous access will be used"),this._config.user=""}if(!this._config.timestamp){if(!this._config.insecure)throw"Missing required configuration parameter 'timestamp'";this._debug("token not found but this is OK for insecure mode")}if(!this._config.token){if(!this._config.insecure)throw"Missing required configuration parameter 'token' specifying the sign of authorization request";this._debug("timestamp not found but this is OK for insecure mode")}if(this._config.url=a(this._config.url),o(this._config.url,"connection")){if(this._debug("client will connect to SockJS endpoint"),"undefined"==typeof SockJS)throw"include SockJS client library before Centrifuge javascript client library or use raw Websocket connection endpoint";this._sockjs=!0,this._sockjsVersion=SockJS.version}else o(this._config.url,"connection/websocket")?(this._debug("client will connect to raw Websocket endpoint"),this._config.url=this._config.url.replace("http://","ws://"),this._config.url=this._config.url.replace("https://","wss://")):(this._debug("client will detect connection endpoint itself"),"undefined"==typeof SockJS?(this._debug("no SockJS found, client will connect to raw Websocket endpoint"),this._config.url+="/connection/websocket",this._config.url=this._config.url.replace("http://","ws://"),this._config.url=this._config.url.replace("https://","wss://")):(this._debug("SockJS found, client will connect to SockJS endpoint"),this._config.url+="/connection",this._sockjs=!0,this._sockjsVersion=SockJS.version))},b._setStatus=function(e){this._status!==e&&(this._debug("Status",this._status,"->",e),this._status=e)},b._isDisconnected=function(){return this._isConnected()===!1},b._isConnected=function(){return"connected"===this._status},b._isConnecting=function(){return"connecting"===this._status},b._nextMessageId=function(){return++this._messageId},b._clearSubscriptions=function(){this._subscriptions={}},b._resetRetry=function(){this._debug("reset retries count to 0"),this._retries=0},b._getRetryInterval=function(){var e=l(this._retries,this._config.retry,this._config.maxRetry);return this._retries+=1,e},b._send=function(e){if(0!==e.length){for(var t=0;t= 1.0.0 instead"),n.protocols_whitelist=this._config.protocols_whitelist),null!==this._config.server&&(n.server=this._config.server),this._transport=new SockJS(this._config.url,null,n)}else this._transport=new WebSocket(this._config.url);this._setStatus("connecting"),this._transport.onopen=function(){t._resetRetry(),u(t._config.user)||t._debug("user expected to be string"),u(t._config.info)||t._debug("info expected to be string");var e={method:"connect",params:{user:t._config.user,info:t._config.info}};t._config.insecure||(e.params.timestamp=t._config.timestamp,e.params.token=t._config.token,u(t._config.timestamp)||t._debug("timestamp expected to be string"),u(t._config.token)||t._debug("token expected to be string")),t.send(e),t._latencyStart=new Date},this._transport.onerror=function(e){t._debug(e)},this._transport.onclose=function(){if(t._setStatus("disconnected"),t.trigger("disconnect"),t._reconnect===!0){var e=t._getRetryInterval();t._debug("reconnect after "+e+" milliseconds"),window.setTimeout(function(){t._reconnect===!0&&t._connect.call(t)},e)}},this._transport.onmessage=function(e){var n;n=JSON.parse(e.data),t._debug("Received",n),t._receive(n)}}},b._disconnect=function(e){var t=e||!1;this._clientId=null,this._setStatus("disconnected"),t===!1&&(this._subscriptions={},this._reconnect=!1),this._transport.close()},b._getSubscription=function(e){var t;return t=this._subscriptions[e],t?t:null},b._removeSubscription=function(e){try{delete this._subscriptions[e]}catch(t){this._debug("nothing to delete for channel ",e)}try{delete this._authChannels[e]}catch(t){this._debug("nothing to delete from authChannels for channel ",e)}},b._connectResponse=function(e){if(null!==this._latencyStart){var t=new Date;this._latency=t.getTime()-this._latencyStart.getTime(),this._latencyStart=null}if(!this.isConnected()){if(g(e))this.trigger("error",[e]),this.trigger("connect:error",[e]);else{if(!e.body)return;if(e.body.expires){var n=e.body.expired;if(n)return void this.refresh()}if(this._clientId=e.body.client,this._setStatus("connected"),this.trigger("connect",[e]),this._refreshTimeout&&window.clearTimeout(this._refreshTimeout),e.body.expires){var i=this;this._refreshTimeout=window.setTimeout(function(){i.refresh.call(i)},1e3*e.body.ttl)}}if(this._config.resubscribe){this.startBatching(),this.startAuthBatching();for(var s in this._subscriptions){var r=this._subscriptions[s];r.subscribe()}this.stopAuthBatching(),this.stopBatching(!0)}}},b._disconnectResponse=function(e){if(g(e))this.trigger("error",[e]),this.trigger("disconnect:error",[e.error]);else{var t=!1;"reconnect"in e.body&&(t=e.body.reconnect),this.disconnect(t),"reason"in e.body&&this._debug("disconnected:",e.body.reason)}},b._subscribeResponse=function(e){g(e)&&this.trigger("error",[e]);var t=e.body;if(null!==t){var n=t.channel,i=this.getSubscription(n);if(i)if(g(e))i.trigger("subscribe:error",[e.error]),i.trigger("error",[e]);else{i.trigger("subscribe:success",[t]),i.trigger("ready",[t]);var s=t.messages;if(s&&s.length>0)for(var r in s.reverse())this._messageResponse({body:s[r]});else"last"in t&&(this._lastMessageID[n]=t.last)}}},b._unsubscribeResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&(g(e)||(i.trigger("unsubscribe",[t]),this._removeSubscription(n)))},b._publishResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&(g(e)?(i.trigger("publish:error",[e.error]),this.trigger("error",[e])):i.trigger("publish:success",[t]))},b._presenceResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&(g(e)?(i.trigger("presence:error",[e.error]),this.trigger("error",[e])):(i.trigger("presence",[t]),i.trigger("presence:success",[t])))},b._historyResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&(g(e)?(i.trigger("history:error",[e.error]),this.trigger("error",[e])):(i.trigger("history",[t]),i.trigger("history:success",[t])))},b._joinResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&i.trigger("join",[t])},b._leaveResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);i&&i.trigger("leave",[t])},b._messageResponse=function(e){var t=e.body,n=t.channel,i=this.getSubscription(n);null!==i&&(this._lastMessageID[n]=t.uid,i.trigger("message",[t]))},b._refreshResponse=function(e){if(this._refreshTimeout&&window.clearTimeout(this._refreshTimeout),e.body.expires){var t=this,n=e.body.expired;if(n)return void(t._refreshTimeout=window.setTimeout(function(){t.refresh.call(t)},3e3+Math.round(1e3*Math.random())));this._clientId=e.body.client,t._refreshTimeout=window.setTimeout(function(){t.refresh.call(t)},1e3*e.body.ttl)}},b._dispatchMessage=function(e){if(void 0!==e&&null!==e){var t=e.method;if(t)switch(t){case"connect":this._connectResponse(e);break;case"disconnect":this._disconnectResponse(e);break;case"subscribe":this._subscribeResponse(e);break;case"unsubscribe":this._unsubscribeResponse(e);break;case"publish":this._publishResponse(e);break;case"presence":this._presenceResponse(e);break;case"history":this._historyResponse(e);break;case"join":this._joinResponse(e);break;case"leave":this._leaveResponse(e);break;case"ping":break;case"refresh":this._refreshResponse(e);break;case"message":this._messageResponse(e)}}},b._receive=function(e){if(Object.prototype.toString.call(e)===Object.prototype.toString.call([])){for(var t in e)if(e.hasOwnProperty(t)){var n=e[t];this._dispatchMessage(n)}}else Object.prototype.toString.call(e)===Object.prototype.toString.call({})&&this._dispatchMessage(e)},b._flush=function(){var e=this._messages.slice(0);this._messages=[],this._send(e)},b._ping=function(){var e={method:"ping",params:{}};this.send(e)},b._recover=function(e){return e in this._lastMessageID},b._getLastID=function(e){var t=this._lastMessageID[e];return t?(this._debug("last uid found and sent for channel",e),t):(this._debug("no last uid found for channel",e),"")},b.getClientId=function(){return this._clientId},b.isConnected=b._isConnected,b.isConnecting=b._isConnecting,b.isDisconnected=b._isDisconnected,b.configure=function(e){this._configure.call(this,e)},b.connect=b._connect,b.disconnect=b._disconnect,b.getSubscription=b._getSubscription,b.ping=b._ping,b.send=function(e){this._isBatching===!0?this._messages.push(e):this._send([e])},b.startBatching=function(){this._isBatching=!0},b.stopBatching=function(e){e=e||!1,this._isBatching=!1,e===!0&&this.flush()},b.flush=function(){this._flush()},b.startAuthBatching=function(){this._isAuthBatching=!0},b.stopAuthBatching=function(e){this._isAuthBatching=!1;var t=this._authChannels;this._authChannels={};var n=[];for(var i in t){var s=this.getSubscription(i);s&&n.push(i)}if(0==n.length)return void(e&&e());var r={client:this.getClientId(),channels:n},o=this,c=function(t,i){if(t===!0){o._debug("authorization request failed");for(var s in n){var r=n[s];o._subscribeResponse({error:"authorization request failed",body:{channel:r}})}return void(e&&e())}for(var s in n){var r=n[s],c=i[r];if(c)if(c.status&&200!==c.status)o._subscribeResponse({error:c.status,body:{channel:r}});else{var a={method:"subscribe",params:{channel:r,client:o.getClientId(),info:c.info,sign:c.sign}},u=o._recover(r);u===!0&&(a.params.recover=!0,a.params.last=o._getLastID(r)),o.send(a)}else o._subscribeResponse({error:404,body:{channel:r}})}e&&e()},a=this._config.authTransport.toLowerCase();if("ajax"===a)this._ajax(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,r,c);else{if("jsonp"!==a)throw"Unknown auth transport "+a;this._jsonp(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,r,c)}},b.subscribe=function(e,t){if(arguments.length<1)throw"Illegal arguments number: required 1, got "+arguments.length;if(!u(e))throw"Illegal argument type: channel must be a string";if(!this._config.resubscribe&&this.isDisconnected())throw"Can not subscribe in disconnected state";var n=this.getSubscription(e);if(null!==n)return n;var i=new d(this,e,t);return this._subscriptions[e]=i,i.subscribe(),i},b.unsubscribe=function(e){if(arguments.length<1)throw"Illegal arguments number: required 1, got "+arguments.length;if(!u(e))throw"Illegal argument type: channel must be a string";var t=this.getSubscription(e);null!==t&&t.unsubscribe()},b.publish=function(e,t,n){var i=this.getSubscription(e);return null===i?(this._debug("subscription not found for channel "+e),null):(i.publish(t,n),i)},b.presence=function(e,t){var n=this.getSubscription(e);return null===n?(this._debug("subscription not found for channel "+e),null):(n.presence(t),n)},b.history=function(e,t){var n=this.getSubscription(e);return null===n?(this._debug("subscription not found for channel "+e),null):(n.history(t),n)},b.refresh=function(){var e=this;this._debug("refresh credentials");var t=function(t,n){if(t===!0)return e._debug("error getting connect parameters",n),e._refreshTimeout&&window.clearTimeout(e._refreshTimeout),void(e._refreshTimeout=window.setTimeout(function(){e.refresh.call(e)},3e3));if(e._config.user=n.user,e._config.timestamp=n.timestamp,e._config.info=n.info,e._config.token=n.token,e.isDisconnected())e._debug("credentials refreshed, connect from scratch"),e._connect();else{e._debug("send refreshed credentials");var i={method:"refresh",params:{user:e._config.user,timestamp:e._config.timestamp,info:e._config.info,token:e._config.token}};e.send(i)}},n=this._config.refreshTransport.toLowerCase();if("ajax"===n)this._ajax(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,{},t);else{if("jsonp"!==n)throw"Unknown refresh transport "+n;this._jsonp(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,{},t)}},e(d,t);var m=d.prototype;m.getChannel=function(){return this.channel},m.getCentrifuge=function(){return this._centrifuge},m.subscribe=function(){if(this._centrifuge.isConnected()){var e={method:"subscribe",params:{channel:this.channel}};if(c(this.channel,this._centrifuge._config.privateChannelPrefix))this._centrifuge._isAuthBatching?this._centrifuge._authChannels[this.channel]=!0:(this._centrifuge.startAuthBatching(),this.subscribe(),this._centrifuge.stopAuthBatching());else{var t=this._centrifuge._recover(this.channel);t===!0&&(e.params.recover=!0,e.params.last=this._centrifuge._getLastID(this.channel)),this._centrifuge.send(e)}}},m.unsubscribe=function(){if(this._centrifuge._removeSubscription(this.channel),this._centrifuge.isConnected()){var e={method:"unsubscribe",params:{channel:this.channel}};this._centrifuge.send(e)}},m.publish=function(e,t){var n={method:"publish",params:{channel:this.channel,data:e}};t&&this.on("publish:success",t),this._centrifuge.send(n)},m.presence=function(e){var t={method:"presence",params:{channel:this.channel}};e&&this.on("presence",e),this._centrifuge.send(t)},m.history=function(e){var t={method:"history",params:{channel:this.channel}};e&&this.on("history",e),this._centrifuge.send(t)},"function"==typeof define&&define.amd?define(function(){return _}):"object"==typeof module&&module.exports?module.exports=_:this.Centrifuge=_}).call(this); \ No newline at end of file +(function(){"use strict";function e(e,t){return e.prototype=Object.create(t.prototype),e.prototype.constructor=e,t.prototype}function t(e,t){try{return e[t]}catch(n){return void 0}}function n(e,i){for(var r=i||{},s=2;sn&&(r=n),Math.floor((1-i)*r)}function h(e){return"error"in e&&null!==e.error&&""!==e.error}function f(e){this._sockjs=!1,this._status="disconnected",this._reconnect=!0,this._reconnecting=!1,this._transport=null,this._transportName=null,this._messageId=0,this._clientID=null,this._subs={},this._lastMessageID={},this._messages=[],this._isBatching=!1,this._isAuthBatching=!1,this._authChannels={},this._refreshTimeout=null,this._retries=0,this._callbacks={},this._config={retry:1e3,maxRetry:2e4,timeout:5e3,info:"",resubscribe:!0,debug:!1,insecure:!1,server:null,privateChannelPrefix:"$",transports:["websocket","xdr-streaming","xhr-streaming","eventsource","iframe-eventsource","iframe-htmlfile","xdr-polling","xhr-polling","iframe-xhr-polling","jsonp-polling"],refreshEndpoint:"/centrifuge/refresh/",refreshHeaders:{},refreshParams:{},refreshTransport:"ajax",authEndpoint:"/centrifuge/auth/",authHeaders:{},authParams:{},authTransport:"ajax"},e&&this.configure(e)}function l(e,t,n){this._status=g,this._error=null,this._centrifuge=e,this.channel=t,this._setEvents(n),this._isResubscribe=!1,this._ready=!1,this._promise=null,this._initializePromise()}(function(){function e(e){return"function"==typeof e||"object"==typeof e&&null!==e}function t(e){return"function"==typeof e}function n(e){return"object"==typeof e&&null!==e}function i(e){N=e}function r(e){F=e}function s(){return function(){process.nextTick(h)}}function o(){return function(){J(h)}}function c(){var e=0,t=new K(h),n=document.createTextNode("");return t.observe(n,{characterData:!0}),function(){n.data=e=++e%2}}function a(){var e=new MessageChannel;return e.port1.onmessage=h,function(){e.port2.postMessage(0)}}function u(){return function(){setTimeout(h,1)}}function h(){for(var e=0;z>e;e+=2){var t=G[e],n=G[e+1];t(n),G[e]=void 0,G[e+1]=void 0}z=0}function f(){try{var e=require,t=e("vertx");return J=t.runOnLoop||t.runOnContext,o()}catch(n){return u()}}function l(){}function _(){return new TypeError("You cannot resolve a promise with itself")}function g(){return new TypeError("A promises callback cannot return that same promise.")}function d(e){try{return e.then}catch(t){return tt.error=t,tt}}function b(e,t,n,i){try{e.call(t,n,i)}catch(r){return r}}function p(e,t,n){F(function(e){var i=!1,r=b(n,t,function(n){i||(i=!0,t!==n?y(e,n):S(e,n))},function(t){i||(i=!0,k(e,t))},"Settle: "+(e._label||" unknown promise"));!i&&r&&(i=!0,k(e,r))},e)}function v(e,t){t._state===Z?S(e,t._result):t._state===et?k(e,t._result):j(t,void 0,function(t){y(e,t)},function(t){k(e,t)})}function m(e,n){if(n.constructor===e.constructor)v(e,n);else{var i=d(n);i===tt?k(e,tt.error):void 0===i?S(e,n):t(i)?p(e,n,i):S(e,n)}}function y(t,n){t===n?k(t,_()):e(n)?m(t,n):S(t,n)}function w(e){e._onerror&&e._onerror(e._result),E(e)}function S(e,t){e._state===Q&&(e._result=t,e._state=Z,0!==e._subscribers.length&&F(E,e))}function k(e,t){e._state===Q&&(e._state=et,e._result=t,F(w,e))}function j(e,t,n,i){var r=e._subscribers,s=r.length;e._onerror=null,r[s]=t,r[s+Z]=n,r[s+et]=i,0===s&&e._state&&F(E,e)}function E(e){var t=e._subscribers,n=e._state;if(0!==t.length){for(var i,r,s=e._result,o=0;oo;o++)j(i.resolve(e[o]),void 0,t,n);return r}function I(e){var t=this;if(e&&"object"==typeof e&&e.constructor===t)return e;var n=new t(l);return y(n,e),n}function L(e){var t=this,n=new t(l);return k(n,e),n}function P(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function D(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function B(e){this._id=at++,this._state=void 0,this._result=void 0,this._subscribers=[],l!==e&&(t(e)||P(),this instanceof B||D(),x(this,e))}function q(){var e;if("undefined"!=typeof global)e=global;else if("undefined"!=typeof self)e=self;else try{e=Function("return this")()}catch(t){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=e.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(e.Promise=ut)}var U;U=Array.isArray?Array.isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)};var J,N,H,X=U,z=0,F=({}.toString,function(e,t){G[z]=e,G[z+1]=t,z+=2,2===z&&(N?N(h):H())}),V="undefined"!=typeof window?window:void 0,W=V||{},K=W.MutationObserver||W.WebKitMutationObserver,Y="undefined"!=typeof process&&"[object process]"==={}.toString.call(process),$="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,G=new Array(1e3);H=Y?s():K?c():$?a():void 0===V&&"function"==typeof require?f():u();var Q=void 0,Z=1,et=2,tt=new O,nt=new O;M.prototype._validateInput=function(e){return X(e)},M.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},M.prototype._init=function(){this._result=new Array(this.length)};var it=M;M.prototype._enumerate=function(){for(var e=this,t=e.length,n=e.promise,i=e._input,r=0;n._state===Q&&t>r;r++)e._eachEntry(i[r],r)},M.prototype._eachEntry=function(e,t){var i=this,r=i._instanceConstructor;n(e)?e.constructor===r&&e._state!==Q?(e._onerror=null,i._settledAt(e._state,t,e._result)):i._willSettleAt(r.resolve(e),t):(i._remaining--,i._result[t]=e)},M.prototype._settledAt=function(e,t,n){var i=this,r=i.promise;r._state===Q&&(i._remaining--,e===et?k(r,n):i._result[t]=n),0===i._remaining&&S(r,i._result)},M.prototype._willSettleAt=function(e,t){var n=this;j(e,void 0,function(e){n._settledAt(Z,t,e)},function(e){n._settledAt(et,t,e)})};var rt=A,st=T,ot=I,ct=L,at=0,ut=B;B.all=rt,B.race=st,B.resolve=ot,B.reject=ct,B._setScheduler=i,B._setAsap=r,B._asap=F,B.prototype={constructor:B,then:function(e,t){var n=this,i=n._state;if(i===Z&&!e||i===et&&!t)return this;var r=new this.constructor(l),s=n._result;if(i){var o=arguments[i-1];F(function(){C(i,r,o,s)})}else j(n,r,e,t);return r},"catch":function(e){return this.then(null,e)}};var ht=q,ft={Promise:ut,polyfill:ht};"function"==typeof define&&define.amd?define(function(){return ft}):"undefined"!=typeof module&&module.exports?module.exports=ft:"undefined"!=typeof this&&(this.ES6Promise=ft),ht()}).call(this),function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,s=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if(e instanceof RegExp){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;t>>0;if(0===r)return-1;if(t=0,arguments.length>1&&(t=Number(arguments[1]),t!=t?t=0:0!=t&&1/0!=t&&t!=-1/0&&(t=(t>0||-1)*Math.floor(Math.abs(t)))),t>=r)return-1;for(n=t>=0?t:Math.max(r-Math.abs(t),0);r>n;n++)if(n in i&&i[n]===e)return n;return-1}),e(f,EventEmitter),f._authCallbacks={},f._nextAuthCallbackID=1;var _=f.prototype;_._jsonp=function(e,t,n,i,r){n.length>0&&this._log("Only AJAX request allows to send custom headers, it's not possible with JSONP."),self._debug("sending JSONP request to",e);var s=f._nextAuthCallbackID.toString();f._nextAuthCallbackID++;var o=window.document,c=o.createElement("script");f._authCallbacks[s]=function(e){r(!1,e),delete f[s]};var a="";for(var u in t)a.length>0&&(a+="&"),a+=encodeURIComponent(u)+"="+encodeURIComponent(t[u]);var h="Centrifuge._authCallbacks['"+s+"']";c.src=this._config.authEndpoint+"?callback="+encodeURIComponent(h)+"&data="+encodeURIComponent(JSON.stringify(i))+"&"+a;var l=o.getElementsByTagName("head")[0]||o.documentElement;l.insertBefore(c,l.firstChild)},_._ajax=function(e,t,n,i,r){var s=this;s._debug("sending AJAX request to",e);var o=window.XMLHttpRequest?new window.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),c="";for(var a in t)c.length>0&&(c+="&"),c+=encodeURIComponent(a)+"="+encodeURIComponent(t[a]);c.length>0&&(c="?"+c),o.open("POST",e+c,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","application/json");for(var u in n)o.setRequestHeader(u,n[u]);return o.onreadystatechange=function(){if(4===o.readyState)if(200===o.status){var e,t=!1;try{e=JSON.parse(o.responseText),t=!0}catch(n){r(!0,"JSON returned was invalid, yet status code was 200. Data was: "+o.responseText)}t&&r(!1,e)}else s._log("Couldn't get auth info from application",o.status),r(!0,o.status)},setTimeout(function(){o.send(JSON.stringify(i))},20),o},_._log=function(){a("info",arguments)},_._debug=function(){this._config.debug===!0&&a("debug",arguments)},_._configure=function(e){if(this._debug("Configuring centrifuge object with",e),e||(e={}),this._config=n(!1,this._config,e),!this._config.url)throw"Missing required configuration parameter 'url' specifying server URL";if(!this._config.user&&""!==this._config.user){if(!this._config.insecure)throw"Missing required configuration parameter 'user' specifying user's unique ID in your application";this._debug("user not found but this is OK for insecure mode - anonymous access will be used"),this._config.user=""}if(!this._config.timestamp){if(!this._config.insecure)throw"Missing required configuration parameter 'timestamp'";this._debug("token not found but this is OK for insecure mode")}if(!this._config.token){if(!this._config.insecure)throw"Missing required configuration parameter 'token' specifying the sign of authorization request";this._debug("timestamp not found but this is OK for insecure mode")}if(this._config.url=s(this._config.url),i(this._config.url,"connection")){if(this._debug("client will connect to SockJS endpoint"),"undefined"==typeof SockJS)throw"include SockJS client library before Centrifuge javascript client library or use raw Websocket connection endpoint";this._sockjs=!0}else i(this._config.url,"connection/websocket")?(this._debug("client will connect to raw Websocket endpoint"),this._config.url=this._config.url.replace("http://","ws://"),this._config.url=this._config.url.replace("https://","wss://")):(this._debug("client will detect connection endpoint itself"),"undefined"==typeof SockJS?(this._debug("no SockJS found, client will connect to raw Websocket endpoint"),this._config.url+="/connection/websocket",this._config.url=this._config.url.replace("http://","ws://"),this._config.url=this._config.url.replace("https://","wss://")):(this._debug("SockJS found, client will connect to SockJS endpoint"),this._config.url+="/connection",this._sockjs=!0))},_._setStatus=function(e){this._status!==e&&(this._debug("Status",this._status,"->",e),this._status=e)},_._isDisconnected=function(){return"disconnected"===this._status},_._isConnecting=function(){return"connecting"===this._status},_._isConnected=function(){return"connected"===this._status},_._nextMessageId=function(){return++this._messageId},_._resetRetry=function(){this._debug("reset retries count to 0"),this._retries=0},_._getRetryInterval=function(){var e=u(this._retries,this._config.retry,this._config.maxRetry);return this._retries+=1,e},_._clearConnectedState=function(e){self._clientID=null;for(var t in this._callbacks){var n=this._callbacks[t],i=n.errback;i&&i(this._createErrorObject("disconnected","retry"))}this._callbacks={};for(var r in this._subs){var s=this._subs[r];e?(s._isSuccess()&&s._triggerUnsubscribe(),s._setSubscribing()):s._setUnsubscribed()}this._config.resubscribe&&this._reconnect||(this._subs={})},_._send=function(e){0!==e.length&&(this._debug("Send",e),this._transport.send(JSON.stringify(e)))},_._connect=function(e){if(this.isConnected())return void this._debug("connect called when already connected");this._setStatus("connecting"),this._clientID=null,this._reconnect=!0;var t=this;if(e&&this.on("connect",e),this._sockjs===!0){var n={transports:this._config.transports};null!==this._config.server&&(n.server=this._config.server),this._transport=new SockJS(this._config.url,null,n)}else this._transport=new WebSocket(this._config.url);this._transport.onopen=function(){t._reconnecting=!1,t._transportName=t._sockjs?t._transport._transport.transportName:"raw-websocket",t._resetRetry(),o(t._config.user)||t._log("user expected to be string"),o(t._config.info)||t._log("info expected to be string");var e={method:"connect",params:{user:t._config.user,info:t._config.info}};t._config.insecure||(e.params.timestamp=t._config.timestamp,e.params.token=t._config.token,o(t._config.timestamp)||t._log("timestamp expected to be string"),o(t._config.token)||t._log("token expected to be string")),t._addMessage(e)},this._transport.onerror=function(e){t._debug("transport level error",e)},this._transport.onclose=function(){t._disconnect("connection closed",!0,!1)},this._transport.onmessage=function(e){var n;n=JSON.parse(e.data),t._debug("Received",n),t._receive(n)}},_._disconnect=function(e,t,n){this._debug("disconnected:",e,t);var i=t||!1;if(i===!1&&(this._reconnect=!1),this._clearConnectedState(t),!this.isDisconnected()){this._setStatus("disconnected");var r={reason:e,reconnect:i};this._reconnecting===!1&&this.trigger("disconnect",[r])}n&&this._transport.close();var s=this;if(t===!0&&s._reconnect===!0){s._reconnecting=!0;var o=s._getRetryInterval();s._debug("reconnect after "+o+" milliseconds"),window.setTimeout(function(){s._reconnect===!0&&s._connect.call(s)},o)}},_._refresh=function(){var e=this;this._debug("refresh credentials");var t=function(t,n){if(t===!0)return e._debug("error getting connect parameters",n),e._refreshTimeout&&window.clearTimeout(e._refreshTimeout),void(e._refreshTimeout=window.setTimeout(function(){e._refresh.call(e)},3e3));if(e._config.user=n.user,e._config.timestamp=n.timestamp,e._config.info=n.info,e._config.token=n.token,e.isDisconnected())e._debug("credentials refreshed, connect from scratch"),e._connect();else{e._debug("send refreshed credentials");var i={method:"refresh",params:{user:e._config.user,timestamp:e._config.timestamp,info:e._config.info,token:e._config.token}};e._addMessage(i)}},n=this._config.refreshTransport.toLowerCase();if("ajax"===n)this._ajax(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,{},t);else{if("jsonp"!==n)throw"Unknown refresh transport "+n;this._jsonp(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,{},t)}},_._subscribe=function(e){var t=e.channel;if(t in this._subs||(this._subs[t]=e),!this.isConnected())return void e._setNew();e._setSubscribing();var n={method:"subscribe",params:{channel:t}};if(r(t,this._config.privateChannelPrefix))this._isAuthBatching?this._authChannels[t]=!0:(this.startAuthBatching(),this._subscribe(e),this.stopAuthBatching());else{var i=this._recover(t);i===!0&&(n.params.recover=!0,n.params.last=this._getLastID(t)),this._addMessage(n)}},_._unsubscribe=function(e){if(this.isConnected()){var t={method:"unsubscribe",params:{channel:e.channel}};this._addMessage(t)}},_._getSub=function(e){var t=this._subs[e];return t?t:null},_._connectResponse=function(e){if(!this.isConnected())if(h(e))this.trigger("error",[{message:e}]);else{if(!e.body)return;if(e.body.expires){var t=e.body.expired;if(t)return void this._refresh()}if(this._clientID=e.body.client,this._setStatus("connected"),this._refreshTimeout&&window.clearTimeout(this._refreshTimeout),e.body.expires){var n=this;this._refreshTimeout=window.setTimeout(function(){n._refresh.call(n)},1e3*e.body.ttl)}if(this._config.resubscribe){this.startBatching(),this.startAuthBatching();for(var i in this._subs){console.log(i);var r=this._subs[i];this._subscribe(r)}this.stopAuthBatching(),this.stopBatching(!0)}var s={client:e.body.client,transport:this._transportName};this.trigger("connect",[s])}},_._disconnectResponse=function(e){if(h(e))this.trigger("error",[{message:e}]);else{var t=!1;"reconnect"in e.body&&(t=e.body.reconnect);var n="";"reason"in e.body&&(n=e.body.reason),this._disconnect(n,t,!0)}},_._subscribeResponse=function(e){var t=e.body;if(null!==t){var n=t.channel,i=this._getSub(n);if(i&&i._isSubscribing())if(h(e))this.trigger("error",[{message:e}]),i._setSubscribeError(this._errorObjectFromMessage(e));else{i._setSubscribeSuccess();var r=t.messages;if(r&&r.length>0)for(var s in r.reverse())this._messageResponse({body:r[s]});else"last"in t&&(this._lastMessageID[n]=t.last)}}},_._unsubscribeResponse=function(e){var t=e.uid,n=e.body,i=n.channel,r=this._getSub(i);r&&(h(e)?this.trigger("error",[{message:e}]):t||r._setUnsubscribed())},_._publishResponse=function(e){var t=e.uid,n=e.body;if(t in this._callbacks){var i=this._callbacks[t];if(delete this._callbacks[t],h(e)){var r=i.errback;if(!r)return;r(this._errorObjectFromMessage(e)),this.trigger("error",[{message:e}])}else{var s=i.callback;if(!s)return;s(n)}}},_._presenceResponse=function(e){var t=e.uid,n=e.body;if(t in this._callbacks){var i=this._callbacks[t];if(delete this._callbacks[t],h(e)){var r=i.errback;if(!r)return;r(this._errorObjectFromMessage(e)),this.trigger("error",[{message:e}])}else{var s=i.callback;if(!s)return;s(n)}}},_._historyResponse=function(e){var t=e.uid,n=e.body;if(t in this._callbacks){var i=this._callbacks[t];if(delete this._callbacks[t],h(e)){var r=i.errback;if(!r)return;r(this._errorObjectFromMessage(e)),this.trigger("error",[{message:e}])}else{var s=i.callback;if(!s)return;s(n)}}},_._joinResponse=function(e){var t=e.body,n=t.channel,i=this._getSub(n);i&&i.trigger("join",[t])},_._leaveResponse=function(e){var t=e.body,n=t.channel,i=this._getSub(n);i&&i.trigger("leave",[t])},_._messageResponse=function(e){var t=e.body,n=t.channel;this._lastMessageID[n]=t.uid;var i=this._getSub(n);i&&i.trigger("message",[t])},_._refreshResponse=function(e){if(this._refreshTimeout&&window.clearTimeout(this._refreshTimeout),h(e))this.trigger("error",[{message:e}]);else if(e.body.expires){var t=this,n=e.body.expired;if(n)return void(t._refreshTimeout=window.setTimeout(function(){t._refresh.call(t)},3e3+Math.round(1e3*Math.random())));this._clientID=e.body.client,t._refreshTimeout=window.setTimeout(function(){t._refresh.call(t)},1e3*e.body.ttl)}},_._dispatchMessage=function(e){if(void 0===e||null===e)return void this._debug("dispatch: got undefined or null message");var t=e.method;if(!t)return void this._debug("dispatch: got message with empty method");switch(t){case"connect":this._connectResponse(e);break;case"disconnect":this._disconnectResponse(e);break;case"subscribe":this._subscribeResponse(e);break;case"unsubscribe":this._unsubscribeResponse(e);break;case"publish":this._publishResponse(e);break;case"presence":this._presenceResponse(e);break;case"history":this._historyResponse(e);break;case"join":this._joinResponse(e);break;case"leave":this._leaveResponse(e);break;case"ping":break;case"refresh":this._refreshResponse(e);break;case"message":this._messageResponse(e);break;default:this._debug("dispatch: got message with unknown method"+t)}},_._receive=function(e){if(Object.prototype.toString.call(e)===Object.prototype.toString.call([])){for(var t in e)if(e.hasOwnProperty(t)){var n=e[t];this._dispatchMessage(n)}}else Object.prototype.toString.call(e)===Object.prototype.toString.call({})&&this._dispatchMessage(e)},_._flush=function(){var e=this._messages.slice(0);this._messages=[],this._send(e)},_._ping=function(){var e={method:"ping",params:{}};this._addMessage(e)},_._recover=function(e){return e in this._lastMessageID},_._getLastID=function(e){var t=this._lastMessageID[e];return t?(this._debug("last uid found and sent for channel",e),t):(this._debug("no last uid found for channel",e),"")},_._createErrorObject=function(e,t){var n={error:e};return t&&(n.advice=t),n},_._errorObjectFromMessage=function(e){var t=e.error,n=e.advice;return this._createErrorObject(t,n)},_._registerCall=function(e,t,n){var i=this;this._callbacks[e]={callback:t,errback:n},setTimeout(function(){delete i._callbacks[e],c(n)&&n(i._createErrorObject("timeout","retry"))},this._config.timeout)},_._addMessage=function(e){var t=""+this._nextMessageId();return e.uid=t,this._isBatching===!0?this._messages.push(e):this._send([e]),t},_.getClientId=function(){return this._clientID},_.isConnected=_._isConnected,_.isDisconnected=_._isDisconnected,_.configure=function(e){this._configure.call(this,e)},_.connect=_._connect,_.disconnect=function(){this._disconnect("client",!1,!0)},_.ping=_._ping,_.startBatching=function(){this._isBatching=!0},_.stopBatching=function(e){e=e||!1,this._isBatching=!1,e===!0&&this.flush()},_.flush=function(){this._flush()},_.startAuthBatching=function(){this._isAuthBatching=!0},_.stopAuthBatching=function(){this._isAuthBatching=!1;var e=this._authChannels;this._authChannels={};var t=[];for(var n in e){var i=this._getSub(n);i&&t.push(n)}if(0!=t.length){var r={client:this.getClientId(),channels:t},s=this,o=function(e,n){if(e!==!0){var i=!1;s._isBatching||(s.startBatching(),i=!0);for(var r in t){var o=t[r],c=n[o];if(c)if(c.status&&200!==c.status)s._subscribeResponse({error:c.status,body:{channel:o}});else{var a={method:"subscribe",params:{channel:o,client:s.getClientId(),info:c.info,sign:c.sign}},u=s._recover(o);u===!0&&(a.params.recover=!0,a.params.last=s._getLastID(o)),s._addMessage(a)}else s._subscribeResponse({error:"channel not found in authorization response",advice:"fix",body:{channel:o}})}i&&s.stopBatching(!0)}else{s._debug("authorization request failed");for(var r in t){var o=t[r];s._subscribeResponse({error:"authorization request failed",advice:"fix",body:{channel:o}})}}},c=this._config.authTransport.toLowerCase();if("ajax"===c)this._ajax(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,r,o);else{if("jsonp"!==c)throw"Unknown auth transport "+c;this._jsonp(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,r,o)}}},_.subscribe=function(e,t){if(arguments.length<1)throw"Illegal arguments number: required 1, got "+arguments.length;if(!o(e))throw"Illegal argument type: channel must be a string";if(!this._config.resubscribe&&!this.isConnected())throw"Can not only subscribe in connected state when resubscribe option is off";var n=this._getSub(e);if(null!==n)return n._setEvents(t),n;var i=new l(this,e,t);return this._subs[e]=i,i.subscribe(),i};var g=0,d=1,b=2,p=3,v=4;e(l,EventEmitter);var m=l.prototype;m._initializePromise=function(){this._ready=!1;var e=this;this._promise=new Promise(function(t,n){e._resolve=function(n){e._ready=!0,t(n)},e._reject=function(t){e._ready=!0,n(t)}})},m._setEvents=function(e){if(e)if(c(e))this.on("message",e);else if(Object.prototype.toString.call(e)===Object.prototype.toString.call({})){var t=["message","join","leave","unsubscribe","subscribe","error"];for(var n in t){var i=t[n];i in e&&this.on(i,e[i])}}},m._isNew=function(){return this._status===g},m._isUnsubscribed=function(){return this._status===v},m._isSubscribing=function(){return this._status===d},m._isReady=function(){return this._status===b||this._status===p},m._isSuccess=function(){return this._status===b},m._isError=function(){return this._status===p},m._setNew=function(){this._status=g},m._setSubscribing=function(){this._ready===!0&&(this._initializePromise(),this._isResubscribe=!0),this._status=d},m._setSubscribeSuccess=function(){if(this._status!=b){this._status=b;var e=this._getSubscribeSuccessContext();this.trigger("subscribe",[e]),this._resolve(e)}},m._setSubscribeError=function(e){if(this._status!=p){this._status=p,this._error=e;var t=this._getSubscribeErrorContext();this.trigger("error",[t]),this._reject(t)}},m._triggerUnsubscribe=function(){var e={channel:this.channel};this.trigger("unsubscribe",[e])},m._setUnsubscribed=function(){this._status!=v&&(this._status=v,this._triggerUnsubscribe())},m._getSubscribeSuccessContext=function(){return{channel:this.channel,isResubscribe:this._isResubscribe}},m._getSubscribeErrorContext=function(){var e=this._error;return e.channel=this.channel,e.isResubscribe=this._isResubscribe,e},m.ready=function(e,t){this._ready&&(this._isSuccess()?e(this._getSubscribeSuccessContext()):t(this._getSubscribeErrorContext()))},m.subscribe=function(){return this._status!=b?(this._centrifuge._subscribe(this),this):void 0},m.unsubscribe=function(){this._setUnsubscribed(),this._centrifuge._unsubscribe(this)},m.publish=function(e){var t=this;return new Promise(function(n,i){return t._isUnsubscribed()?void i(t._centrifuge._createErrorObject("subscription unsubscribed","fix")):void t._promise.then(function(){if(!t._centrifuge.isConnected())return void i(t._centrifuge._createErrorObject("disconnected","retry"));var r={method:"publish",params:{channel:t.channel,data:e}},s=t._centrifuge._addMessage(r);t._centrifuge._registerCall(s,n,i)},function(e){i(e)})})},m.presence=function(){var e=this;return new Promise(function(t,n){return e._isUnsubscribed()?void n(e._centrifuge._createErrorObject("subscription unsubscribed","fix")):void e._promise.then(function(){if(!e._centrifuge.isConnected())return void n(e._centrifuge._createErrorObject("disconnected","retry"));var i={method:"presence",params:{channel:e.channel}},r=e._centrifuge._addMessage(i);e._centrifuge._registerCall(r,t,n)},function(e){n(e)})})},m.history=function(){var e=this;return new Promise(function(t,n){return e._isUnsubscribed()?void n(e._centrifuge._createErrorObject("subscription unsubscribed","fix")):void e._promise.then(function(){if(!e._centrifuge.isConnected())return void n(e._centrifuge._createErrorObject("disconnected","retry"));var i={method:"history",params:{channel:e.channel}},r=e._centrifuge._addMessage(i);e._centrifuge._registerCall(r,t,n)},function(e){n(e)})})},"function"==typeof define&&define.amd?define(function(){return f}):"object"==typeof module&&module.exports?module.exports=f:this.Centrifuge=f}).call(this); \ No newline at end of file diff --git a/plugins/centrifuge-dom/README.rst b/plugins/centrifuge-dom/README.rst deleted file mode 100644 index 0efbb269..00000000 --- a/plugins/centrifuge-dom/README.rst +++ /dev/null @@ -1,105 +0,0 @@ -To make things even more simple ``centrifuge.dom.js`` jQuery plugin can be used. - -In most cases you application does not need all real-time features of Centrifuge. -If your application does not need complicated subscription management, dynamic channels -then ``centrifuge.dom.js`` can help you a lot. - -Many of you heard about AngularJS or KnockoutJS. Those libraries use html attributes -to control application behaviour. When you change attributes and their values you -change your application logic. This is very flexible technique. Why not use this power -to add some real-time on your site. - -First, add ``centrifuge.dom.js`` on your page: - -.. code-block:: html - - - - -Note, that ``centrifuge.dom.js`` requires **jQuery**! - -When enabled that plugin searches for special html-elements on your page, creates a -connection to Centrifuge, subscribes on necessary channels and triggers event on -html-elements when new message from channel received. - -All you need to do in this case is write how your page will react on new messages: - -.. code-block:: javascript - - $('#html-element').on('centrifuge.message', function(event, message) { - console.log(message.data); - }); - - -Let's see how it looks in practice. Consider comments for example. - -The user of your web application writes a new comment, clicks submit button. -Your web application's backend processes new data, validates it, saves as -usually. If everything ok you then must send POST request with comment data -into Centrifuge so that new comment will appear on the screen of all connected -clients. - -Let's make it work in five steps. - -STEP 1) Add all necessary scripts into your web application's main template. -These are ``jQuery``, ``SockJS`` (optional, use can use pure WebSockets), ``centrifuge.js``, ``centrifuge.dom.js`` - -STEP 2) In main template initialize plugin: - -.. code-block:: javascript - - $(function(){ - $.centrifuge_dom({}); - }); - - -STEP 3) Also add html-elements with proper attributes in main template with connection -address, token, user ID values. - -.. code-block:: html - -
-
-
-
- - -Here I use syntax of Django templates. In your case it can look slightly different. -The values of connection address, token, user ID must provide your -web app's backend. - -STEP 4) On the page with comments add the following html-element with channel and namespace -names in attributes - -.. code-block:: html - -
- -STEP 5) On the same page add some javascript - -.. code-block:: javascript - - $(function() { - $("#comments-handler").on("centrifuge.message", function(event, message) { - $("body").append(message.data); - }); - }); - - -That's all, baby! - -Moreover now to to add some new real-time elements on your pages you only need to do -last two steps. - -In some scenarios you need to handle errors and disconnects. This can be done by listening -for ``centrifuge.disconnect`` and ``centrifuge.error`` events on handler elements. - -For example - -.. code-block:: javascript - - $("#comments-handler").on("centrifuge.disconnect", function(event, err) { - console.log("disconnected from Centrifuge"); - }); - - diff --git a/plugins/centrifuge-dom/centrifuge.dom.js b/plugins/centrifuge-dom/centrifuge.dom.js deleted file mode 100644 index 4a7c40e5..00000000 --- a/plugins/centrifuge-dom/centrifuge.dom.js +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Centrifuge javascript DOM jQuery plugin - * v0.5.1 - */ -;(function (jQuery) { - jQuery.extend({ - centrifuge_dom: function (custom_options) { - - var defaults = { - url: null, - selector: '.centrifuge', - urlSelector: '#centrifuge-address', - tokenSelector: '#centrifuge-token', - userSelector: '#centrifuge-user', - timestampSelector: '#centrifuge-timestamp', - infoSelector: '#centrifuge-info', - valueAttrName: 'data-centrifuge-value', - channelAttr: 'data-centrifuge-channel', - messageEventNameAttr: 'data-centrifuge-message', - eventPrefix: 'centrifuge.', - fullMessage: true, - debug: false - }; - - this.centrifuge = null; - - var options = jQuery.extend(defaults, custom_options); - - var compliance = {}; - - var handlers = jQuery(options.selector); - - function debug(message) { - if (options.debug === true) { - console.log(message); - } - } - - function get_object_size(obj) { - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) size++; - } - return size; - } - - function get_message_data(message) { - if (options.fullMessage === false) { - return message.data; - } - return message; - } - - function get_message_body(message) { - if (options.fullMessage === false) { - return message.body; - } - return message; - } - - function get_message_error(message) { - if (options.fullMessage === false) { - return message.error; - } - return message; - } - - function get_handler_subscription(handler, centrifuge) { - var handler_channel = handler.attr(options.channelAttr); - var subscription = centrifuge.getSubscription(handler_channel); - if (subscription === null) { - debug('no subscription found for channel ' + handler_channel); - } - return subscription; - } - - function bind_centrifuge_events(centrifuge) { - - centrifuge.on('connect', function () { - debug("connected to Centrifuge"); - subscribe(centrifuge); - }); - - centrifuge.on('error', function (err) { - debug(err); - }); - - centrifuge.on('disconnect', function () { - debug("disconnected from Centrifuge"); - disconnect(); - }); - - } - - function bind_handler_events(centrifuge) { - - handlers.on(options.eventPrefix + 'publish', function(data) { - // publish data into channel - var handler = $(this); - var subscription = get_handler_subscription(handler, centrifuge); - if (subscription) { - subscription.publish(data); - } - }); - - handlers.on(options.eventPrefix + 'presence', function() { - var handler = $(this); - var subscription = get_handler_subscription(handler, centrifuge); - if (subscription) { - subscription.presence(function(message) { - handler.trigger('centrifuge-presence-message', get_message_data(message)); - }); - } - }); - - handlers.on(options.eventPrefix + 'history', function() { - var handler = $(this); - var subscription = get_handler_subscription(handler, centrifuge); - if (subscription) { - subscription.history(function(message) { - handler.trigger('centrifuge-history-message', get_message_data(message)); - }); - } - }); - - handlers.on(options.eventPrefix + 'unsubscribe', function() { - var handler = $(this); - var subscription = get_handler_subscription(handler, centrifuge); - if (subscription) { - subscription.unsubscribe(); - } - }); - - } - - function handle_subscription(path, centrifuge) { - - var handler = compliance[path]; - - var subscription = centrifuge.subscribe(path, function (message) { - debug(message); - var handler_event = handler.attr(options.messageEventNameAttr); - var message_event_name = options.eventPrefix + (handler_event || 'message'); - handler.trigger(message_event_name, get_message_data(message)); - }); - - subscription.on('subscribe:success', function(message) { - var subscribe_success_event_name = options.eventPrefix + 'subscribe:success'; - handler.trigger(subscribe_success_event_name, get_message_body(message)); - }); - - subscription.on('subscribe:error', function(message) { - debug(message); - var subscribe_success_event_name = options.eventPrefix + 'subscribe:error'; - handler.trigger(subscribe_success_event_name, get_message_error(message)); - }); - - subscription.on('join', function(message) { - debug(message); - var join_event_name = options.eventPrefix + 'join'; - handler.trigger(join_event_name, get_message_data(message)); - }); - - subscription.on('leave', function(message) { - debug(message); - var leave_event_name = options.eventPrefix + 'leave'; - handler.trigger(leave_event_name, get_message_data(message)); - }); - - } - - function handle_disconnect(path) { - var handler = compliance[path]; - handler.trigger(options.eventPrefix + 'disconnect'); - } - - function subscribe(centrifuge) { - for (var subscription_path in compliance) { - //noinspection JSUnfilteredForInLoop - handle_subscription(subscription_path, centrifuge); - } - } - - function disconnect() { - for (var subscription_path in compliance) { - //noinspection JSUnfilteredForInLoop - handle_disconnect(subscription_path); - } - } - - function parse_dom(centrifuge) { - handlers.each(function (index, element) { - var handler = jQuery(element); - var handler_channel = handler.attr(options.channelAttr); - compliance[handler_channel] = handler; - }); - } - - this.init = function() { - - if (!Centrifuge) { - console.log("No Centrifuge javascript client found"); - return; - } - - if (handlers.length === 0) { - debug("No Centrifuge handlers found on this page, nothing to do"); - return; - } - - var token = $(options.tokenSelector).attr(options.valueAttrName); - if (!token) { - console.log("Centrifuge token not found"); - return; - } - - var user = $(options.userSelector).attr(options.valueAttrName); - if (!user) { - console.log("Centrifuge user not found"); - return; - } - - var timestamp = $(options.timestampSelector).attr(options.valueAttrName); - if (!timestamp) { - console.log("Centrifuge timestamp not found"); - return; - } - - var url; - if (options.url === null) { - url = $(options.urlSelector).attr(options.valueAttrName); - } - - if (!url) { - console.log("Centrifuge connection url not found"); - return; - } - - var info = $(options.infoSelector).attr(options.valueAttrName); - if (!info) { - debug("Centrifuge info not used, token must be generated without info part."); - info = null; - } - - //noinspection JSUnresolvedFunction - var centrifuge = new Centrifuge({ - url: url, - token: token, - user: user, - timestamp: timestamp, - info: info - }); - - parse_dom(centrifuge); - - if (get_object_size(compliance) === 0) { - debug("No valid handlers found on page, no need in connection to Centrifuge"); - return; - } - - bind_centrifuge_events(centrifuge); - bind_handler_events(centrifuge); - centrifuge.connect(); - this.centrifuge = centrifuge; - return this; - }; - - return this.init(); - - } - }); -})(jQuery); diff --git a/plugins/centrifuge-dom/centrifuge.dom.min.js b/plugins/centrifuge-dom/centrifuge.dom.min.js deleted file mode 100644 index ba2a4e98..00000000 --- a/plugins/centrifuge-dom/centrifuge.dom.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){e.extend({centrifuge_dom:function(n){function t(e){m.debug===!0&&console.log(e)}function r(e){var n,t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t}function i(e){return m.fullMessage===!1?e.data:e}function o(e){return m.fullMessage===!1?e.body:e}function u(e){return m.fullMessage===!1?e.error:e}function c(e,n){var r=e.attr(m.channelAttr),i=n.getSubscription(r);return null===i&&t("no subscription found for channel "+r),i}function f(e){e.on("connect",function(){t("connected to Centrifuge"),g(e)}),e.on("error",function(e){t(e)}),e.on("disconnect",function(){t("disconnected from Centrifuge"),v()})}function s(e){p.on(m.eventPrefix+"publish",function(n){var t=$(this),r=c(t,e);r&&r.publish(n)}),p.on(m.eventPrefix+"presence",function(){var n=$(this),t=c(n,e);t&&t.presence(function(e){n.trigger("centrifuge-presence-message",i(e))})}),p.on(m.eventPrefix+"history",function(){var n=$(this),t=c(n,e);t&&t.history(function(e){n.trigger("centrifuge-history-message",i(e))})}),p.on(m.eventPrefix+"unsubscribe",function(){var n=$(this),t=c(n,e);t&&t.unsubscribe()})}function a(e,n){var r=b[e],c=n.subscribe(e,function(e){t(e);var n=r.attr(m.messageEventNameAttr),o=m.eventPrefix+(n||"message");r.trigger(o,i(e))});c.on("subscribe:success",function(e){var n=m.eventPrefix+"subscribe:success";r.trigger(n,o(e))}),c.on("subscribe:error",function(e){t(e);var n=m.eventPrefix+"subscribe:error";r.trigger(n,u(e))}),c.on("join",function(e){t(e);var n=m.eventPrefix+"join";r.trigger(n,i(e))}),c.on("leave",function(e){t(e);var n=m.eventPrefix+"leave";r.trigger(n,i(e))})}function l(e){var n=b[e];n.trigger(m.eventPrefix+"disconnect")}function g(e){for(var n in b)a(n,e)}function v(){for(var e in b)l(e)}function d(){p.each(function(n,t){var r=e(t),i=r.attr(m.channelAttr);b[i]=r})}var h={url:null,selector:".centrifuge",urlSelector:"#centrifuge-address",tokenSelector:"#centrifuge-token",userSelector:"#centrifuge-user",timestampSelector:"#centrifuge-timestamp",infoSelector:"#centrifuge-info",valueAttrName:"data-centrifuge-value",channelAttr:"data-centrifuge-channel",messageEventNameAttr:"data-centrifuge-message",eventPrefix:"centrifuge.",fullMessage:!0,debug:!1};this.centrifuge=null;var m=e.extend(h,n),b={},p=e(m.selector);return this.init=function(){if(!Centrifuge)return void console.log("No Centrifuge javascript client found");if(0===p.length)return void t("No Centrifuge handlers found on this page, nothing to do");var e=$(m.tokenSelector).attr(m.valueAttrName);if(!e)return void console.log("Centrifuge token not found");var n=$(m.userSelector).attr(m.valueAttrName);if(!n)return void console.log("Centrifuge user not found");var i=$(m.timestampSelector).attr(m.valueAttrName);if(!i)return void console.log("Centrifuge timestamp not found");var o;if(null===m.url&&(o=$(m.urlSelector).attr(m.valueAttrName)),!o)return void console.log("Centrifuge connection url not found");var u=$(m.infoSelector).attr(m.valueAttrName);u||(t("Centrifuge info not used, token must be generated without info part."),u=null);var c=new Centrifuge({url:o,token:e,user:n,timestamp:i,info:u});return d(c),0===r(b)?void t("No valid handlers found on page, no need in connection to Centrifuge"):(f(c),s(c),c.connect(),this.centrifuge=c,this)},this.init()}})}(jQuery); \ No newline at end of file diff --git a/src/centrifuge.js b/src/centrifuge.js index 9de36188..59eadb59 100644 --- a/src/centrifuge.js +++ b/src/centrifuge.js @@ -1,15 +1,30 @@ ;(function () { 'use strict'; + /*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 3.0.2 + */ + (function(){"use strict";function lib$es6$promise$utils$$objectOrFunction(x){return typeof x==="function"||typeof x==="object"&&x!==null}function lib$es6$promise$utils$$isFunction(x){return typeof x==="function"}function lib$es6$promise$utils$$isMaybeThenable(x){return typeof x==="object"&&x!==null}var lib$es6$promise$utils$$_isArray;if(!Array.isArray){lib$es6$promise$utils$$_isArray=function(x){return Object.prototype.toString.call(x)==="[object Array]"}}else{lib$es6$promise$utils$$_isArray=Array.isArray}var lib$es6$promise$utils$$isArray=lib$es6$promise$utils$$_isArray;var lib$es6$promise$asap$$len=0;var lib$es6$promise$asap$$toString={}.toString;var lib$es6$promise$asap$$vertxNext;var lib$es6$promise$asap$$customSchedulerFn;var lib$es6$promise$asap$$asap=function asap(callback,arg){lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len]=callback;lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len+1]=arg;lib$es6$promise$asap$$len+=2;if(lib$es6$promise$asap$$len===2){if(lib$es6$promise$asap$$customSchedulerFn){lib$es6$promise$asap$$customSchedulerFn(lib$es6$promise$asap$$flush)}else{lib$es6$promise$asap$$scheduleFlush()}}};function lib$es6$promise$asap$$setScheduler(scheduleFn){lib$es6$promise$asap$$customSchedulerFn=scheduleFn}function lib$es6$promise$asap$$setAsap(asapFn){lib$es6$promise$asap$$asap=asapFn}var lib$es6$promise$asap$$browserWindow=typeof window!=="undefined"?window:undefined;var lib$es6$promise$asap$$browserGlobal=lib$es6$promise$asap$$browserWindow||{};var lib$es6$promise$asap$$BrowserMutationObserver=lib$es6$promise$asap$$browserGlobal.MutationObserver||lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver;var lib$es6$promise$asap$$isNode=typeof process!=="undefined"&&{}.toString.call(process)==="[object process]";var lib$es6$promise$asap$$isWorker=typeof Uint8ClampedArray!=="undefined"&&typeof importScripts!=="undefined"&&typeof MessageChannel!=="undefined";function lib$es6$promise$asap$$useNextTick(){return function(){process.nextTick(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useVertxTimer(){return function(){lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useMutationObserver(){var iterations=0;var observer=new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush);var node=document.createTextNode("");observer.observe(node,{characterData:true});return function(){node.data=iterations=++iterations%2}}function lib$es6$promise$asap$$useMessageChannel(){var channel=new MessageChannel;channel.port1.onmessage=lib$es6$promise$asap$$flush;return function(){channel.port2.postMessage(0)}}function lib$es6$promise$asap$$useSetTimeout(){return function(){setTimeout(lib$es6$promise$asap$$flush,1)}}var lib$es6$promise$asap$$queue=new Array(1e3);function lib$es6$promise$asap$$flush(){for(var i=0;i>> 0; - - if (len === 0) { - return -1; - } - n = 0; - if (arguments.length > 1) { - n = Number(arguments[1]); - if (n != n) { // shortcut for verifying if it's NaN - n = 0; - } else if (n != 0 && n != Infinity && n != -Infinity) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - for (k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - }; - } - function extend(destination, source) { destination.prototype = Object.create(source.prototype); destination.prototype.constructor = destination; return source.prototype; } - /** - * EventEmitter v4.2.3 - git.io/ee - * Oliver Caldwell - * MIT license - * @preserve - */ - - /** - * Class for managing events. - * Can be extended to provide event functionality in other classes. - * - * @class EventEmitter Manages event registering and emitting. - */ - function EventEmitter() {} - - // Shortcuts to improve speed and size - - // Easy access to the prototype - var proto = EventEmitter.prototype; - - /** - * Finds the index of the listener for the event in it's storage array. - * - * @param {Function[]} listeners Array of listeners to search through. - * @param {Function} listener Method to look for. - * @return {Number} Index of the specified listener, -1 if not found - * @api private - */ - function indexOfListener(listeners, listener) { - var i = listeners.length; - while (i--) { - if (listeners[i].listener === listener) { - return i; - } - } - - return -1; + if (!Array.prototype.indexOf) { + Array.prototype.indexOf=function(r){if(null==this)throw new TypeError;var t,e,n=Object(this),a=n.length>>>0;if(0===a)return-1;if(t=0,arguments.length>1&&(t=Number(arguments[1]),t!=t?t=0:0!=t&&1/0!=t&&t!=-1/0&&(t=(t>0||-1)*Math.floor(Math.abs(t)))),t>=a)return-1;for(e=t>=0?t:Math.max(a-Math.abs(t),0);a>e;e++)if(e in n&&n[e]===r)return e;return-1}; } - /** - * Alias a method while keeping the context correct, to allow for overwriting of target method. - * - * @param {String} name The name of the target method. - * @return {Function} The aliased method - * @api private - */ - function alias(name) { - return function aliasClosure() { - return this[name].apply(this, arguments); - }; + function fieldValue(object, name) { + try {return object[name];} catch (x) {return undefined;} } - /** - * Returns the listener array for the specified event. - * Will initialise the event object and listener arrays if required. - * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. - * Each property in the object response is an array of listener functions. - * - * @param {String|RegExp} evt Name of the event to return the listeners from. - * @return {Function[]|Object} All listener functions for the event. - */ - proto.getListeners = function getListeners(evt) { - var events = this._getEvents(); - var response; - var key; - - // Return a concatenated array of all matching events if - // the selector is a regular expression. - if (typeof evt === 'object') { - response = {}; - for (key in events) { - if (events.hasOwnProperty(key) && evt.test(key)) { - response[key] = events[key]; - } - } - } - else { - response = events[evt] || (events[evt] = []); - } - - return response; - }; - - /** - * Takes a list of listener objects and flattens it into a list of listener functions. - * - * @param {Object[]} listeners Raw listener objects. - * @return {Function[]} Just the listener functions. - */ - proto.flattenListeners = function flattenListeners(listeners) { - var flatListeners = []; - var i; - - for (i = 0; i < listeners.length; i += 1) { - flatListeners.push(listeners[i].listener); - } - - return flatListeners; - }; - - /** - * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. - * - * @param {String|RegExp} evt Name of the event to return the listeners from. - * @return {Object} All listener functions for an event in an object. - */ - proto.getListenersAsObject = function getListenersAsObject(evt) { - var listeners = this.getListeners(evt); - var response; - - if (listeners instanceof Array) { - response = {}; - response[evt] = listeners; - } - - return response || listeners; - }; - - /** - * Adds a listener function to the specified event. - * The listener will not be added if it is a duplicate. - * If the listener returns true then it will be removed after it is called. - * If you pass a regular expression as the event name then the listener will be added to all events that match it. - * - * @param {String|RegExp} evt Name of the event to attach the listener to. - * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addListener = function addListener(evt, listener) { - var listeners = this.getListenersAsObject(evt); - var listenerIsWrapped = typeof listener === 'object'; - var key; - - for (key in listeners) { - if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) { - listeners[key].push(listenerIsWrapped ? listener : { - listener: listener, - once: false - }); - } - } - - return this; - }; - - /** - * Alias of addListener - */ - proto.on = alias('addListener'); - - /** - * Semi-alias of addListener. It will add a listener that will be - * automatically removed after it's first execution. - * - * @param {String|RegExp} evt Name of the event to attach the listener to. - * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addOnceListener = function addOnceListener(evt, listener) { - //noinspection JSValidateTypes - return this.addListener(evt, { - listener: listener, - once: true - }); - }; - - /** - * Alias of addOnceListener. - */ - proto.once = alias('addOnceListener'); - - /** - * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. - * You need to tell it what event names should be matched by a regex. - * - * @param {String} evt Name of the event to create. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.defineEvent = function defineEvent(evt) { - this.getListeners(evt); - return this; - }; - - /** - * Uses defineEvent to define multiple events. - * - * @param {String[]} evts An array of event names to define. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.defineEvents = function defineEvents(evts) { - for (var i = 0; i < evts.length; i += 1) { - this.defineEvent(evts[i]); - } - return this; - }; - - /** - * Removes a listener function from the specified event. - * When passed a regular expression as the event name, it will remove the listener from all events that match it. - * - * @param {String|RegExp} evt Name of the event to remove the listener from. - * @param {Function} listener Method to remove from the event. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeListener = function removeListener(evt, listener) { - var listeners = this.getListenersAsObject(evt); - var index; - var key; - - for (key in listeners) { - if (listeners.hasOwnProperty(key)) { - index = indexOfListener(listeners[key], listener); - - if (index !== -1) { - listeners[key].splice(index, 1); - } - } - } - - return this; - }; - - /** - * Alias of removeListener - */ - proto.off = alias('removeListener'); - - /** - * Adds listeners in bulk using the manipulateListeners method. - * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. - * You can also pass it a regular expression to add the array of listeners to all events that match it. - * Yeah, this function does quite a bit. That's probably a bad thing. - * - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to add. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.addListeners = function addListeners(evt, listeners) { - // Pass through to manipulateListeners - return this.manipulateListeners(false, evt, listeners); - }; - - /** - * Removes listeners in bulk using the manipulateListeners method. - * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. - * You can also pass it an event name and an array of listeners to be removed. - * You can also pass it a regular expression to remove the listeners from all events that match it. - * - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to remove. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeListeners = function removeListeners(evt, listeners) { - // Pass through to manipulateListeners - return this.manipulateListeners(true, evt, listeners); - }; - - /** - * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. - * The first argument will determine if the listeners are removed (true) or added (false). - * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. - * You can also pass it an event name and an array of listeners to be added/removed. - * You can also pass it a regular expression to manipulate the listeners of all events that match it. - * - * @param {Boolean} remove True if you want to remove listeners, false if you want to add. - * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. - * @param {Function[]} [listeners] An optional array of listener functions to add/remove. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) { - var i; - var value; - var single = remove ? this.removeListener : this.addListener; - var multiple = remove ? this.removeListeners : this.addListeners; - - // If evt is an object then pass each of it's properties to this method - if (typeof evt === 'object' && !(evt instanceof RegExp)) { - for (i in evt) { - if (evt.hasOwnProperty(i) && (value = evt[i])) { - // Pass the single listener straight through to the singular method - if (typeof value === 'function') { - single.call(this, i, value); - } - else { - // Otherwise pass back to the multiple function - multiple.call(this, i, value); - } - } - } - } - else { - // So evt must be a string - // And listeners must be an array of listeners - // Loop over it and pass each one to the multiple method - i = listeners.length; - while (i--) { - single.call(this, evt, listeners[i]); - } - } - - return this; - }; - - /** - * Removes all listeners from a specified event. - * If you do not specify an event then all listeners will be removed. - * That means every event will be emptied. - * You can also pass a regex to remove all events that match it. - * - * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.removeEvent = function removeEvent(evt) { - var type = typeof evt; - var events = this._getEvents(); - var key; - - // Remove different things depending on the state of evt - if (type === 'string') { - // Remove all listeners for the specified event - delete events[evt]; - } - else if (type === 'object') { - // Remove all events matching the regex. - for (key in events) { - //noinspection JSUnresolvedFunction - if (events.hasOwnProperty(key) && evt.test(key)) { - delete events[key]; - } - } - } - else { - // Remove all listeners in all events - delete this._events; - } - - return this; - }; - - /** - * Emits an event of your choice. - * When emitted, every listener attached to that event will be executed. - * If you pass the optional argument array then those arguments will be passed to every listener upon execution. - * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. - * So they will not arrive within the array on the other side, they will be separate. - * You can also pass a regular expression to emit to all events that match it. - * - * @param {String|RegExp} evt Name of the event to emit and execute listeners for. - * @param {Array} [args] Optional array of arguments to be passed to each listener. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.emitEvent = function emitEvent(evt, args) { - var listeners = this.getListenersAsObject(evt); - var listener; - var i; - var key; - var response; - - for (key in listeners) { - if (listeners.hasOwnProperty(key)) { - i = listeners[key].length; - - while (i--) { - // If the listener returns true then it shall be removed from the event - // The function is executed either with a basic call or an apply if there is an args array - listener = listeners[key][i]; - - if (listener.once === true) { - this.removeListener(evt, listener.listener); - } - - response = listener.listener.apply(this, args || []); - - if (response === this._getOnceReturnValue()) { - this.removeListener(evt, listener.listener); - } - } - } - } - - return this; - }; - - /** - * Alias of emitEvent - */ - proto.trigger = alias('emitEvent'); - - //noinspection JSValidateJSDoc,JSCommentMatchesSignature - /** - * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. - * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. - * - * @param {String|RegExp} evt Name of the event to emit and execute listeners for. - * @param {...*} Optional additional arguments to be passed to each listener. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.emit = function emit(evt) { - var args = Array.prototype.slice.call(arguments, 1); - return this.emitEvent(evt, args); - }; - - /** - * Sets the current value to check against when executing listeners. If a - * listeners return value matches the one set here then it will be removed - * after execution. This value defaults to true. - * - * @param {*} value The new value to check for when executing listeners. - * @return {Object} Current instance of EventEmitter for chaining. - */ - proto.setOnceReturnValue = function setOnceReturnValue(value) { - this._onceReturnValue = value; - return this; - }; - - /** - * Fetches the current value to check against when executing listeners. If - * the listeners return value matches this one then it should be removed - * automatically. It will return true by default. - * - * @return {*|Boolean} The current value to check for or the default, true. - * @api private - */ - proto._getOnceReturnValue = function _getOnceReturnValue() { - if (this.hasOwnProperty('_onceReturnValue')) { - return this._onceReturnValue; - } - else { - return true; - } - }; - - /** - * Fetches the events object and creates one if required. - * - * @return {Object} The events storage object. - * @api private - */ - proto._getEvents = function _getEvents() { - return this._events || (this._events = {}); - }; - /** * Mixes in the given objects into the target object by copying the properties. * @param deep if the copy must be deep @@ -509,57 +57,35 @@ */ function mixin(deep, target, objects) { var result = target || {}; - - // Skip first 2 parameters (deep and target), and loop over the others - for (var i = 2; i < arguments.length; ++i) { + for (var i = 2; i < arguments.length; ++i) { // Skip first 2 parameters (deep and target), and loop over the others var object = arguments[i]; - if (object === undefined || object === null) { continue; } - for (var propName in object) { - //noinspection JSUnfilteredForInLoop var prop = fieldValue(object, propName); - //noinspection JSUnfilteredForInLoop var targ = fieldValue(result, propName); - - // Avoid infinite loops if (prop === target) { - continue; + continue; // Avoid infinite loops } - // Do not mixin undefined values if (prop === undefined) { - continue; + continue; // Do not mixin undefined values } - if (deep && typeof prop === 'object' && prop !== null) { if (prop instanceof Array) { - //noinspection JSUnfilteredForInLoop result[propName] = mixin(deep, targ instanceof Array ? targ : [], prop); } else { var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {}; - //noinspection JSUnfilteredForInLoop result[propName] = mixin(deep, source, prop); } } else { - //noinspection JSUnfilteredForInLoop result[propName] = prop; } } } - return result; } - function fieldValue(object, name) { - try { - return object[name]; - } catch (x) { - return undefined; - } - } - function endsWith(value, suffix) { return value.indexOf(suffix, value.length - suffix.length) !== -1; } @@ -613,15 +139,14 @@ function Centrifuge(options) { this._sockjs = false; - this._sockjsVersion = null; this._status = 'disconnected'; this._reconnect = true; + this._reconnecting = false; this._transport = null; - this._latency = null; - this._latencyStart = null; + this._transportName = null; this._messageId = 0; - this._clientId = null; - this._subscriptions = {}; + this._clientID = null; + this._subs = {}; this._lastMessageID = {}; this._messages = []; this._isBatching = false; @@ -629,26 +154,17 @@ this._authChannels = {}; this._refreshTimeout = null; this._retries = 0; + this._callbacks = {}; this._config = { retry: 1000, maxRetry: 20000, + timeout: 5000, info: "", - resubscribe: false, + resubscribe: true, debug: false, insecure: false, server: null, privateChannelPrefix: "$", - protocols_whitelist: [ - 'websocket', - 'xdr-streaming', - 'xhr-streaming', - 'iframe-eventsource', - 'iframe-htmlfile', - 'xdr-polling', - 'xhr-polling', - 'iframe-xhr-polling', - 'jsonp-polling' - ], transports: [ 'websocket', 'xdr-streaming', @@ -661,11 +177,11 @@ 'iframe-xhr-polling', 'jsonp-polling' ], - refreshEndpoint: "/centrifuge/refresh", + refreshEndpoint: "/centrifuge/refresh/", refreshHeaders: {}, refreshParams: {}, refreshTransport: "ajax", - authEndpoint: "/centrifuge/auth", + authEndpoint: "/centrifuge/auth/", authHeaders: {}, authParams: {}, authTransport: "ajax" @@ -729,11 +245,9 @@ } query += encodeURIComponent(i) + "=" + encodeURIComponent(params[i]); } - if (query.length > 0) { query = "?" + query; } - xhr.open("POST", url + query, true); // add request headers @@ -752,14 +266,14 @@ data = JSON.parse(xhr.responseText); parsed = true; } catch (e) { - callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); + callback(true, 'JSON returned was invalid, yet status code was 200. Data was: ' + xhr.responseText); } if (parsed) { // prevents double execution. callback(false, data); } } else { - self._log("Couldn't get auth info from your webapp", xhr.status); + self._log("Couldn't get auth info from application", xhr.status); callback(true, xhr.status); } } @@ -828,7 +342,6 @@ throw 'include SockJS client library before Centrifuge javascript client library or use raw Websocket connection endpoint'; } this._sockjs = true; - this._sockjsVersion = SockJS.version; } else if (endsWith(this._config.url, 'connection/websocket')) { this._debug("client will connect to raw Websocket endpoint"); this._config.url = this._config.url.replace("http://", "ws://"); @@ -844,7 +357,6 @@ this._debug("SockJS found, client will connect to SockJS endpoint"); this._config.url += "/connection"; this._sockjs = true; - this._sockjsVersion = SockJS.version; } } }; @@ -857,25 +369,21 @@ }; centrifugeProto._isDisconnected = function () { - return this._isConnected() === false; + return this._status === 'disconnected'; }; - centrifugeProto._isConnected = function () { - return this._status === 'connected'; + centrifugeProto._isConnecting = function() { + return this._status === 'connecting'; }; - centrifugeProto._isConnecting = function () { - return this._status === 'connecting'; + centrifugeProto._isConnected = function () { + return this._status === 'connected'; }; centrifugeProto._nextMessageId = function () { return ++this._messageId; }; - centrifugeProto._clearSubscriptions = function () { - this._subscriptions = {}; - }; - centrifugeProto._resetRetry = function() { this._debug("reset retries count to 0"); this._retries = 0; @@ -887,18 +395,43 @@ return interval; }; + centrifugeProto._clearConnectedState = function (reconnect) { + self._clientID = null; + + // fire errbacks of registered calls. + for (var uid in this._callbacks) { + var callbacks = this._callbacks[uid]; + var errback = callbacks["errback"]; + if (!errback) { + continue; + } + errback(this._createErrorObject("disconnected", "retry")); + } + this._callbacks = {}; + + // fire unsubscribe events + for (var channel in this._subs) { + var sub = this._subs[channel]; + if (reconnect) { + if (sub._isSuccess()) { + sub._triggerUnsubscribe(); + } + sub._setSubscribing(); + } else { + sub._setUnsubscribed(); + } + } + + if (!this._config.resubscribe || !this._reconnect) { + // completely clear connected state + this._subs = {}; + } + }; + centrifugeProto._send = function (messages) { - // We must be sure that the messages have a clientId. - // This is not guaranteed since the handshake may take time to return - // (and hence the clientId is not known yet) and the application - // may create other messages. if (messages.length === 0) { return; } - for (var i = 0; i < messages.length; ++i) { - var message = messages[i]; - message.uid = '' + this._nextMessageId(); - } this._debug('Send', messages); this._transport.send(JSON.stringify(messages)); }; @@ -906,18 +439,13 @@ centrifugeProto._connect = function (callback) { if (this.isConnected()) { + this._debug("connect called when already connected"); return; } - this._clientId = null; - - this._reconnect = true; - - if (!this._config.resubscribe) { - this._clearSubscriptions(); - } - this._setStatus('connecting'); + this._clientID = null; + this._reconnect = true; var self = this; @@ -925,15 +453,11 @@ this.on('connect', callback); } + // detect transport to use - SockJS or raw Websocket if (this._sockjs === true) { - //noinspection JSUnresolvedFunction - var sockjsOptions = {}; - if (startsWith(this._sockjsVersion, "1.")) { - sockjsOptions["transports"] = this._config.transports; - } else { - this._log("SockJS <= 0.3.4 is deprecated, use SockJS >= 1.0.0 instead"); - sockjsOptions["protocols_whitelist"] = this._config.protocols_whitelist; - } + var sockjsOptions = { + "transports": this._config.transports + }; if (this._config.server !== null) { sockjsOptions['server'] = this._config.server; } @@ -942,20 +466,26 @@ this._transport = new WebSocket(this._config.url); } - this._setStatus('connecting'); - this._transport.onopen = function () { + self._reconnecting = false; + + if (self._sockjs) { + self._transportName = self._transport._transport.transportName; + } else { + self._transportName = "raw-websocket"; + } + self._resetRetry(); if (!isString(self._config.user)) { - self._debug("user expected to be string"); + self._log("user expected to be string"); } if (!isString(self._config.info)) { - self._debug("info expected to be string"); + self._log("info expected to be string"); } - var centrifugeMessage = { + var msg = { 'method': 'connect', 'params': { 'user': self._config.user, @@ -964,35 +494,25 @@ }; if (!self._config.insecure) { - centrifugeMessage["params"]["timestamp"] = self._config.timestamp; - centrifugeMessage["params"]["token"] = self._config.token; + // in insecure client mode we don't need timestamp and token. + msg["params"]["timestamp"] = self._config.timestamp; + msg["params"]["token"] = self._config.token; if (!isString(self._config.timestamp)) { - self._debug("timestamp expected to be string"); + self._log("timestamp expected to be string"); } if (!isString(self._config.token)) { - self._debug("token expected to be string"); + self._log("token expected to be string"); } } - self.send(centrifugeMessage); - self._latencyStart = new Date(); + self._addMessage(msg); }; this._transport.onerror = function (error) { - self._debug(error); + self._debug("transport level error", error); }; this._transport.onclose = function () { - self._setStatus('disconnected'); - self.trigger('disconnect'); - if (self._reconnect === true) { - var interval = self._getRetryInterval(); - self._debug("reconnect after " + interval + " milliseconds"); - window.setTimeout(function () { - if (self._reconnect === true) { - self._connect.call(self); - } - }, interval); - } + self._disconnect("connection closed", true, false); }; this._transport.onmessage = function (event) { @@ -1003,89 +523,211 @@ }; }; - centrifugeProto._disconnect = function (shouldReconnect) { + centrifugeProto._disconnect = function (reason, shouldReconnect, closeTransport) { + this._debug("disconnected:", reason, shouldReconnect); var reconnect = shouldReconnect || false; - this._clientId = null; - this._setStatus('disconnected'); if (reconnect === false) { - this._subscriptions = {}; this._reconnect = false; } - this._transport.close(); - }; - centrifugeProto._getSubscription = function (channel) { - var subscription; - subscription = this._subscriptions[channel]; - if (!subscription) { - return null; + this._clearConnectedState(shouldReconnect); + + if (!this.isDisconnected()) { + this._setStatus('disconnected'); + var disconnectContext = { + "reason": reason, + "reconnect": reconnect + }; + if (this._reconnecting === false) { + this.trigger('disconnect', [disconnectContext]); + } } - return subscription; - }; - centrifugeProto._removeSubscription = function (channel) { - try { - delete this._subscriptions[channel]; - } catch (e) { - this._debug('nothing to delete for channel ', channel); + if (closeTransport) { + this._transport.close(); } - try { - delete this._authChannels[channel]; - } catch (e) { - this._debug('nothing to delete from authChannels for channel ', channel); + + var self = this; + if (shouldReconnect === true && self._reconnect === true) { + self._reconnecting = true; + var interval = self._getRetryInterval(); + self._debug("reconnect after " + interval + " milliseconds"); + window.setTimeout(function () { + if (self._reconnect === true) { + self._connect.call(self); + } + }, interval); } }; - centrifugeProto._connectResponse = function (message) { - - if (this._latencyStart !== null) { - var latencyEnd = new Date(); - this._latency = latencyEnd.getTime() - this._latencyStart.getTime(); - this._latencyStart = null; - } + centrifugeProto._refresh = function () { + // ask web app for connection parameters - user ID, + // timestamp, info and token + var self = this; + this._debug('refresh credentials'); - if (this.isConnected()) { - return; - } - if (!errorExists(message)) { - if (!message.body) { + var cb = function(error, data) { + if (error === true) { + // 403 or 500 - does not matter - if connection check activated then Centrifugo + // will disconnect client eventually + self._debug("error getting connect parameters", data); + if (self._refreshTimeout) { + window.clearTimeout(self._refreshTimeout); + } + self._refreshTimeout = window.setTimeout(function(){ + self._refresh.call(self); + }, 3000); + return; + } + self._config.user = data.user; + self._config.timestamp = data.timestamp; + self._config.info = data.info; + self._config.token = data.token; + if (self.isDisconnected()) { + self._debug("credentials refreshed, connect from scratch"); + self._connect(); + } else { + self._debug("send refreshed credentials"); + var msg = { + "method": "refresh", + "params": { + 'user': self._config.user, + 'timestamp': self._config.timestamp, + 'info': self._config.info, + 'token': self._config.token + } + }; + self._addMessage(msg); + } + }; + + var transport = this._config.refreshTransport.toLowerCase(); + if (transport === "ajax") { + this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); + } else if (transport === "jsonp") { + this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); + } else { + throw 'Unknown refresh transport ' + transport; + } + }; + + centrifugeProto._subscribe = function(sub) { + + var channel = sub.channel; + + if (!(channel in this._subs)) { + this._subs[channel] = sub; + } + + if (!this.isConnected()) { + // subscribe will be called later + sub._setNew(); + return; + } + + sub._setSubscribing(); + + var msg = { + "method": "subscribe", + "params": { + "channel": channel + } + }; + + // If channel name does not start with privateChannelPrefix - then we + // can just send subscription message to Centrifuge. If channel name + // starts with privateChannelPrefix - then this is a private channel + // and we should ask web application backend for permission first. + if (startsWith(channel, this._config.privateChannelPrefix)) { + // private channel + if (this._isAuthBatching) { + this._authChannels[channel] = true; + } else { + this.startAuthBatching(); + this._subscribe(sub); + this.stopAuthBatching(); + } + } else { + var recover = this._recover(channel); + if (recover === true) { + msg["params"]["recover"] = true; + msg["params"]["last"] = this._getLastID(channel); + } + this._addMessage(msg); + } + }; + + centrifugeProto._unsubscribe = function(sub) { + if (this.isConnected()) { + // No need to unsubscribe in disconnected state - i.e. client already unsubscribed. + var msg = { + "method": "unsubscribe", + "params": { + "channel": sub.channel + } + }; + this._addMessage(msg); + } + }; + + centrifugeProto._getSub = function(channel) { + var sub = this._subs[channel]; + if (!sub) { + return null; + } + return sub; + }; + + centrifugeProto._connectResponse = function (message) { + + if (this.isConnected()) { + return; + } + + if (!errorExists(message)) { + if (!message.body) { return; } if (message.body.expires) { var isExpired = message.body.expired; if (isExpired) { - this.refresh(); + this._refresh(); return; } } - this._clientId = message.body.client; + this._clientID = message.body.client; this._setStatus('connected'); - this.trigger('connect', [message]); + if (this._refreshTimeout) { window.clearTimeout(this._refreshTimeout); } if (message.body.expires) { var self = this; this._refreshTimeout = window.setTimeout(function() { - self.refresh.call(self); + self._refresh.call(self); }, message.body.ttl * 1000); } - } else { - this.trigger('error', [message]); - this.trigger('connect:error', [message]); - } - if (this._config.resubscribe) { - this.startBatching(); - this.startAuthBatching(); - for (var i in this._subscriptions) { - var sub = this._subscriptions[i]; - sub.subscribe(); + if (this._config.resubscribe) { + this.startBatching(); + this.startAuthBatching(); + for (var channel in this._subs) { + console.log(channel); + var sub = this._subs[channel]; + this._subscribe(sub); + } + this.stopAuthBatching(); + this.stopBatching(true); } - this.stopAuthBatching(); - this.stopBatching(true); - } + var connectContext = { + "client": message.body.client, + "transport": this._transportName + }; + this.trigger('connect', [connectContext]); + } else { + this.trigger('error', [{"message": message}]); + } }; centrifugeProto._disconnectResponse = function (message) { @@ -1094,168 +736,216 @@ if ("reconnect" in message.body) { shouldReconnect = message.body["reconnect"]; } - this.disconnect(shouldReconnect); + var reason = ""; if ("reason" in message.body) { - this._debug("disconnected:", message.body["reason"]); + reason = message.body["reason"]; } + this._disconnect(reason, shouldReconnect, true); } else { - this.trigger('error', [message]); - this.trigger('disconnect:error', [message.error]); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._subscribeResponse = function (message) { - if (errorExists(message)) { - this.trigger('error', [message]); - } var body = message.body; if (body === null) { return; } var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } + + if (!sub._isSubscribing()) { + return; + } + if (!errorExists(message)) { - subscription.trigger('subscribe:success', [body]); - subscription.trigger('ready', [body]); + sub._setSubscribeSuccess(); var messages = body["messages"]; if (messages && messages.length > 0) { + // handle missed messages for (var i in messages.reverse()) { this._messageResponse({body: messages[i]}); } } else { if ("last" in body) { + // no missed messages found so set last message id from body. this._lastMessageID[channel] = body["last"]; } } } else { - subscription.trigger('subscribe:error', [message.error]); - subscription.trigger('error', [message]); + this.trigger('error', [{"message": message}]); + sub._setSubscribeError(this._errorObjectFromMessage(message)); } }; centrifugeProto._unsubscribeResponse = function (message) { + var uid = message.uid; var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } + if (!errorExists(message)) { - subscription.trigger('unsubscribe', [body]); - this._removeSubscription(channel); + if (!uid) { + // unsubscribe command from server – unsubscribe all current subs + sub._setUnsubscribed(); + } + // ignore client initiated successful unsubscribe responses as we + // already unsubscribed on client level. + } else { + this.trigger('error', [{"message": message}]); } }; centrifugeProto._publishResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('publish:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('publish:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._presenceResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('presence', [body]); - subscription.trigger('presence:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('presence:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._historyResponse = function (message) { + var uid = message.uid; var body = message.body; - var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + if (!(uid in this._callbacks)) { return; } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; if (!errorExists(message)) { - subscription.trigger('history', [body]); - subscription.trigger('history:success', [body]); + var callback = callbacks["callback"]; + if (!callback) { + return; + } + callback(body); } else { - subscription.trigger('history:error', [message.error]); - this.trigger('error', [message]); + var errback = callbacks["errback"]; + if (!errback) { + return; + } + errback(this._errorObjectFromMessage(message)); + this.trigger('error', [{"message": message}]); } }; centrifugeProto._joinResponse = function(message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } - subscription.trigger('join', [body]); + sub.trigger('join', [body]); }; centrifugeProto._leaveResponse = function(message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (!subscription) { + + var sub = this._getSub(channel); + if (!sub) { return; } - subscription.trigger('leave', [body]); + sub.trigger('leave', [body]); }; centrifugeProto._messageResponse = function (message) { var body = message.body; var channel = body.channel; - var subscription = this.getSubscription(channel); - if (subscription === null) { - return; - } + // keep last uid received from channel. this._lastMessageID[channel] = body["uid"]; - subscription.trigger('message', [body]); + + var sub = this._getSub(channel); + if (!sub) { + return; + } + sub.trigger('message', [body]); }; centrifugeProto._refreshResponse = function (message) { if (this._refreshTimeout) { window.clearTimeout(this._refreshTimeout); } - if (message.body.expires) { - var self = this; - var isExpired = message.body.expired; - if (isExpired) { - self._refreshTimeout = window.setTimeout(function(){ - self.refresh.call(self); - }, 3000 + Math.round(Math.random() * 1000)); - return; + if (!errorExists(message)) { + if (message.body.expires) { + var self = this; + var isExpired = message.body.expired; + if (isExpired) { + self._refreshTimeout = window.setTimeout(function () { + self._refresh.call(self); + }, 3000 + Math.round(Math.random() * 1000)); + return; + } + this._clientID = message.body.client; + self._refreshTimeout = window.setTimeout(function () { + self._refresh.call(self); + }, message.body.ttl * 1000); } - this._clientId = message.body.client; - self._refreshTimeout = window.setTimeout(function () { - self.refresh.call(self); - }, message.body.ttl * 1000); + } else { + this.trigger('error', [{"message": message}]); } }; centrifugeProto._dispatchMessage = function(message) { if (message === undefined || message === null) { + this._debug("dispatch: got undefined or null message"); return; } var method = message.method; if (!method) { + this._debug("dispatch: got message with empty method"); return; } @@ -1296,12 +986,14 @@ this._messageResponse(message); break; default: + this._debug("dispatch: got message with unknown method" + method); break; } }; centrifugeProto._receive = function (data) { if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) { + // array of responses received for (var i in data) { if (data.hasOwnProperty(i)) { var msg = data[i]; @@ -1309,6 +1001,7 @@ } } } else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) { + // one response received this._dispatchMessage(data); } }; @@ -1320,11 +1013,11 @@ }; centrifugeProto._ping = function () { - var centrifugeMessage = { + var msg = { "method": "ping", "params": {} }; - this.send(centrifugeMessage); + this._addMessage(msg); }; centrifugeProto._recover = function(channel) { @@ -1342,16 +1035,53 @@ } }; - /* PUBLIC API */ + centrifugeProto._createErrorObject = function(err, advice) { + var errObject = { + "error": err + }; + if (advice) { + errObject["advice"] = advice; + } + return errObject; + }; + + centrifugeProto._errorObjectFromMessage = function(message) { + var err = message.error; + var advice = message["advice"]; + return this._createErrorObject(err, advice); + }; + + centrifugeProto._registerCall = function(uid, callback, errback) { + var self = this; + this._callbacks[uid] = { + "callback": callback, + "errback": errback + }; + setTimeout(function() { + delete self._callbacks[uid]; + if (isFunction(errback)) { + errback(self._createErrorObject("timeout", "retry")); + } + }, this._config.timeout); + }; + + centrifugeProto._addMessage = function (message) { + var uid = '' + this._nextMessageId(); + message.uid = uid; + if (this._isBatching === true) { + this._messages.push(message); + } else { + this._send([message]); + } + return uid; + }; centrifugeProto.getClientId = function () { - return this._clientId; + return this._clientID; }; centrifugeProto.isConnected = centrifugeProto._isConnected; - centrifugeProto.isConnecting = centrifugeProto._isConnecting; - centrifugeProto.isDisconnected = centrifugeProto._isDisconnected; centrifugeProto.configure = function (configuration) { @@ -1360,20 +1090,12 @@ centrifugeProto.connect = centrifugeProto._connect; - centrifugeProto.disconnect = centrifugeProto._disconnect; - - centrifugeProto.getSubscription = centrifugeProto._getSubscription; + centrifugeProto.disconnect = function() { + this._disconnect("client", false, true); + }; centrifugeProto.ping = centrifugeProto._ping; - centrifugeProto.send = function (message) { - if (this._isBatching === true) { - this._messages.push(message); - } else { - this._send([message]); - } - }; - centrifugeProto.startBatching = function () { // start collecting messages without sending them to Centrifuge until flush // method called @@ -1400,7 +1122,7 @@ this._isAuthBatching = true; }; - centrifugeProto.stopAuthBatching = function(callback) { + centrifugeProto.stopAuthBatching = function() { // create request to authEndpoint with collected private channels // to ask if this client can subscribe on each channel this._isAuthBatching = false; @@ -1409,17 +1131,14 @@ var channels = []; for (var channel in authChannels) { - var subscription = this.getSubscription(channel); - if (!subscription) { + var sub = this._getSub(channel); + if (!sub) { continue; } channels.push(channel); } if (channels.length == 0) { - if (callback) { - callback(); - } return; } @@ -1437,23 +1156,30 @@ var channel = channels[i]; self._subscribeResponse({ "error": "authorization request failed", + "advice": "fix", "body": { "channel": channel } }); } - if (callback) { - callback(); - } return; } + + // try to send all subscriptions in one request. + var batch = false; + if (!self._isBatching) { + self.startBatching(); + batch = true; + } + for (var i in channels) { var channel = channels[i]; var channelResponse = data[channel]; if (!channelResponse) { // subscription:error self._subscribeResponse({ - "error": 404, + "error": "channel not found in authorization response", + "advice": "fix", "body": { "channel": channel } @@ -1461,7 +1187,7 @@ continue; } if (!channelResponse.status || channelResponse.status === 200) { - var centrifugeMessage = { + var msg = { "method": "subscribe", "params": { "channel": channel, @@ -1472,10 +1198,10 @@ }; var recover = self._recover(channel); if (recover === true) { - centrifugeMessage["params"]["recover"] = true; - centrifugeMessage["params"]["last"] = self._getLastID(channel); + msg["params"]["recover"] = true; + msg["params"]["last"] = self._getLastID(channel); } - self.send(centrifugeMessage); + self._addMessage(msg); } else { self._subscribeResponse({ "error": channelResponse.status, @@ -1485,9 +1211,11 @@ }); } } - if (callback) { - callback(); + + if (batch) { + self.stopBatching(true); } + }; var transport = this._config.authTransport.toLowerCase(); @@ -1500,239 +1228,274 @@ } }; - centrifugeProto.subscribe = function (channel, callback) { - + centrifugeProto.subscribe = function (channel, events) { if (arguments.length < 1) { throw 'Illegal arguments number: required 1, got ' + arguments.length; } if (!isString(channel)) { throw 'Illegal argument type: channel must be a string'; } - if (!this._config.resubscribe && this.isDisconnected()) { - throw 'Can not subscribe in disconnected state'; + if (!this._config.resubscribe && !this.isConnected()) { + throw 'Can not only subscribe in connected state when resubscribe option is off'; } - var current_subscription = this.getSubscription(channel); + var currentSub = this._getSub(channel); - if (current_subscription !== null) { - return current_subscription; + if (currentSub !== null) { + currentSub._setEvents(events); + return currentSub; } else { - var subscription = new Subscription(this, channel, callback); - this._subscriptions[channel] = subscription; - subscription.subscribe(); - return subscription; + var sub = new Sub(this, channel, events); + this._subs[channel] = sub; + sub.subscribe(); + return sub; } }; - centrifugeProto.unsubscribe = function (channel) { - if (arguments.length < 1) { - throw 'Illegal arguments number: required 1, got ' + arguments.length; + var _STATE_NEW = 0; + var _STATE_SUBSCRIBING = 1; + var _STATE_SUCCESS = 2; + var _STATE_ERROR = 3; + var _STATE_UNSUBSCRIBED = 4; + + function Sub(centrifuge, channel, events) { + this._status = _STATE_NEW; + this._error = null; + this._centrifuge = centrifuge; + this.channel = channel; + this._setEvents(events); + this._isResubscribe = false; + this._ready = false; + this._promise = null; + this._initializePromise(); + } + + extend(Sub, EventEmitter); + + var subProto = Sub.prototype; + + subProto._initializePromise = function() { + this._ready = false; + var self = this; + this._promise = new Promise(function(resolve, reject) { + self._resolve = function(value) { + self._ready = true; + resolve(value); + }; + self._reject = function(err) { + self._ready = true; + reject(err); + }; + }); + }; + + subProto._setEvents = function(events) { + if (!events) { + return; } - if (!isString(channel)) { - throw 'Illegal argument type: channel must be a string'; + if (isFunction(events)) { + this.on("message", events); + } else if (Object.prototype.toString.call(events) === Object.prototype.toString.call({})) { + var knownEvents = [ + "message", "join", "leave", "unsubscribe", + "subscribe", "error" + ]; + for (var i in knownEvents) { + var ev = knownEvents[i]; + if (ev in events) { + this.on(ev, events[ev]); + } + } } + }; - var subscription = this.getSubscription(channel); - if (subscription !== null) { - subscription.unsubscribe(); - } + subProto._isNew = function() { + return this._status === _STATE_NEW; }; - centrifugeProto.publish = function (channel, data, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.publish(data, callback); - return subscription; + subProto._isUnsubscribed = function() { + return this._status === _STATE_UNSUBSCRIBED; }; - centrifugeProto.presence = function (channel, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.presence(callback); - return subscription; + subProto._isSubscribing = function() { + return this._status === _STATE_SUBSCRIBING; }; - centrifugeProto.history = function (channel, callback) { - var subscription = this.getSubscription(channel); - if (subscription === null) { - this._debug("subscription not found for channel " + channel); - return null; - } - subscription.history(callback); - return subscription; + subProto._isReady = function() { + return this._status === _STATE_SUCCESS || this._status === _STATE_ERROR; }; - centrifugeProto.refresh = function () { - // ask web app for connection parameters - user ID, - // timestamp, info and token - var self = this; - this._debug('refresh credentials'); + subProto._isSuccess = function() { + return this._status === _STATE_SUCCESS; + }; - var cb = function(error, data) { - if (error === true) { - // 403 or 500 - does not matter - if connection check activated then Centrifugo - // will disconnect client eventually - self._debug("error getting connect parameters", data); - if (self._refreshTimeout) { - window.clearTimeout(self._refreshTimeout); - } - self._refreshTimeout = window.setTimeout(function(){ - self.refresh.call(self); - }, 3000); - return; - } - self._config.user = data.user; - self._config.timestamp = data.timestamp; - self._config.info = data.info; - self._config.token = data.token; - if (self.isDisconnected()) { - self._debug("credentials refreshed, connect from scratch"); - self._connect(); - } else { - self._debug("send refreshed credentials"); - var centrifugeMessage = { - "method": "refresh", - "params": { - 'user': self._config.user, - 'timestamp': self._config.timestamp, - 'info': self._config.info, - 'token': self._config.token - } - }; - self.send(centrifugeMessage); - } - }; + subProto._isError = function() { + return this._status === _STATE_ERROR; + }; - var transport = this._config.refreshTransport.toLowerCase(); - if (transport === "ajax") { - this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); - } else if (transport === "jsonp") { - this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, {}, cb); - } else { - throw 'Unknown refresh transport ' + transport; - } + subProto._setNew = function() { + this._status = _STATE_NEW; }; - function Subscription(centrifuge, channel, callback) { - /** - * The constructor for a centrifuge object, identified by an optional name. - * The default name is the string 'default'. - * @param name the optional name of this centrifuge object - */ - this._centrifuge = centrifuge; - this.channel = channel; - this.callback = callback; - if (this.callback) { - this.on('message', this.callback); + subProto._setSubscribing = function() { + if (this._ready === true) { + // new promise for this subscription + this._initializePromise(); + this._isResubscribe = true; } - } - - extend(Subscription, EventEmitter); + this._status = _STATE_SUBSCRIBING; + }; - var subscriptionProto = Subscription.prototype; + subProto._setSubscribeSuccess = function() { + if (this._status == _STATE_SUCCESS) { + return; + } + this._status = _STATE_SUCCESS; + var successContext = this._getSubscribeSuccessContext(); + this.trigger("subscribe", [successContext]); + this._resolve(successContext); + }; - subscriptionProto.getChannel = function () { - return this.channel; + subProto._setSubscribeError = function(err) { + if (this._status == _STATE_ERROR) { + return; + } + this._status = _STATE_ERROR; + this._error = err; + var errContext = this._getSubscribeErrorContext(); + this.trigger("error", [errContext]); + this._reject(errContext); }; - subscriptionProto.getCentrifuge = function () { - return this._centrifuge; + subProto._triggerUnsubscribe = function() { + var unsubscribeContext = { + "channel": this.channel + }; + this.trigger("unsubscribe", [unsubscribeContext]); }; - subscriptionProto.subscribe = function () { - /* - If channel name does not start with privateChannelPrefix - then we - can just send subscription message to Centrifuge. If channel name - starts with privateChannelPrefix - then this is a private channel - and we should ask web application backend for permission first. - */ - if (!this._centrifuge.isConnected()) { + subProto._setUnsubscribed = function() { + if (this._status == _STATE_UNSUBSCRIBED) { return; } - - var centrifugeMessage = { - "method": "subscribe", - "params": { - "channel": this.channel - } + this._status = _STATE_UNSUBSCRIBED; + this._triggerUnsubscribe(); + }; + + subProto._getSubscribeSuccessContext = function() { + return { + "channel": this.channel, + "isResubscribe": this._isResubscribe }; + }; - if (startsWith(this.channel, this._centrifuge._config.privateChannelPrefix)) { - // private channel - if (this._centrifuge._isAuthBatching) { - this._centrifuge._authChannels[this.channel] = true; + subProto._getSubscribeErrorContext = function() { + var subscribeErrorContext = this._error; + subscribeErrorContext["channel"] = this.channel; + subscribeErrorContext["isResubscribe"] = this._isResubscribe; + return subscribeErrorContext; + }; + + subProto.ready = function(callback, errback) { + if (this._ready) { + if (this._isSuccess()) { + callback(this._getSubscribeSuccessContext()); } else { - this._centrifuge.startAuthBatching(); - this.subscribe(); - this._centrifuge.stopAuthBatching(); + errback(this._getSubscribeErrorContext()); } - } else { - var recover = this._centrifuge._recover(this.channel); - if (recover === true) { - centrifugeMessage["params"]["recover"] = true; - centrifugeMessage["params"]["last"] = this._centrifuge._getLastID(this.channel); - } - this._centrifuge.send(centrifugeMessage); } }; - subscriptionProto.unsubscribe = function () { - this._centrifuge._removeSubscription(this.channel); - if (this._centrifuge.isConnected()) { - var centrifugeMessage = { - "method": "unsubscribe", - "params": { - "channel": this.channel - } - }; - this._centrifuge.send(centrifugeMessage); + subProto.subscribe = function() { + if (this._status == _STATE_SUCCESS) { + return; } + this._centrifuge._subscribe(this); + return this; }; - subscriptionProto.publish = function (data, callback) { - var centrifugeMessage = { - "method": "publish", - "params": { - "channel": this.channel, - "data": data + subProto.unsubscribe = function () { + this._setUnsubscribed(); + this._centrifuge._unsubscribe(this); + }; + + subProto.publish = function (data) { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('publish:success', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "publish", + "params": { + "channel": self.channel, + "data": data + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; - subscriptionProto.presence = function (callback) { - var centrifugeMessage = { - "method": "presence", - "params": { - "channel": this.channel + subProto.presence = function() { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('presence', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "presence", + "params": { + "channel": self.channel + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; - subscriptionProto.history = function (callback) { - var centrifugeMessage = { - "method": "history", - "params": { - "channel": this.channel + subProto.history = function() { + var self = this; + return new Promise(function(resolve, reject) { + if (self._isUnsubscribed()) { + reject(self._centrifuge._createErrorObject("subscription unsubscribed", "fix")); + return; } - }; - if (callback) { - this.on('history', callback); - } - this._centrifuge.send(centrifugeMessage); + self._promise.then(function(){ + if (!self._centrifuge.isConnected()) { + reject(self._centrifuge._createErrorObject("disconnected", "retry")); + return; + } + var msg = { + "method": "history", + "params": { + "channel": self.channel + } + }; + var uid = self._centrifuge._addMessage(msg); + self._centrifuge._registerCall(uid, resolve, reject); + }, function(err){ + reject(err); + }); + }); }; // Expose the class either via AMD, CommonJS or the global object