From bcb1f88a1b20b22f69fe1d1e81843af1a92abd75 Mon Sep 17 00:00:00 2001 From: "Oleg V. Kuzmin" Date: Wed, 24 Jan 2018 19:33:18 +0300 Subject: [PATCH] First commit --- .editorconfig | 9 + .gitignore | 3 + README.md | 9 + dist/centrifuge.js | 1504 +++++++++++++++++ dist/centrifuge.min.js | 1 + index.d.ts | 5 + package.json | 38 + src/Centrifuge.ts | 1180 +++++++++++++ src/Functions.ts | 49 + src/Observable.ts | 53 + src/Subscription.ts | 223 +++ src/interfaces/ICentrifugeConfig.ts | 35 + src/interfaces/ICentrifugeConnectMessage.ts | 7 + src/interfaces/ICentrifugeConnectResponse.ts | 6 + src/interfaces/ICentrifugeCredentials.ts | 6 + src/interfaces/ICentrifugeError.ts | 7 + src/interfaces/ICentrifugeJoinResponse.ts | 3 + src/interfaces/ICentrifugeLeaveResponse.ts | 4 + src/interfaces/ICentrifugeMessage.ts | 9 + src/interfaces/ICentrifugePingMessage.ts | 4 + src/interfaces/ICentrifugeRefreshMessage.ts | 4 + src/interfaces/ICentrifugeRefreshResponse.ts | 4 + src/interfaces/ICentrifugeSubscribeMessage.ts | 12 + .../ICentrifugeSubscribeResponse.ts | 7 + .../ICentrifugeUnsubscribeMessage.ts | 7 + .../ICentrifugeUnsubscribeResponse.ts | 4 + src/interfaces/ISockJSOptions.ts | 4 + src/interfaces/ISubscriptionError.ts | 6 + src/interfaces/ISubscriptionMessage.ts | 8 + src/interfaces/ISubscriptionSuccess.ts | 5 + src/interfaces/index.ts | 19 + tsconfig.json | 15 + webpack.config.js | 52 + 33 files changed, 3302 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dist/centrifuge.js create mode 100644 dist/centrifuge.min.js create mode 100644 index.d.ts create mode 100644 package.json create mode 100644 src/Centrifuge.ts create mode 100644 src/Functions.ts create mode 100644 src/Observable.ts create mode 100644 src/Subscription.ts create mode 100644 src/interfaces/ICentrifugeConfig.ts create mode 100644 src/interfaces/ICentrifugeConnectMessage.ts create mode 100644 src/interfaces/ICentrifugeConnectResponse.ts create mode 100644 src/interfaces/ICentrifugeCredentials.ts create mode 100644 src/interfaces/ICentrifugeError.ts create mode 100644 src/interfaces/ICentrifugeJoinResponse.ts create mode 100644 src/interfaces/ICentrifugeLeaveResponse.ts create mode 100644 src/interfaces/ICentrifugeMessage.ts create mode 100644 src/interfaces/ICentrifugePingMessage.ts create mode 100644 src/interfaces/ICentrifugeRefreshMessage.ts create mode 100644 src/interfaces/ICentrifugeRefreshResponse.ts create mode 100644 src/interfaces/ICentrifugeSubscribeMessage.ts create mode 100644 src/interfaces/ICentrifugeSubscribeResponse.ts create mode 100644 src/interfaces/ICentrifugeUnsubscribeMessage.ts create mode 100644 src/interfaces/ICentrifugeUnsubscribeResponse.ts create mode 100644 src/interfaces/ISockJSOptions.ts create mode 100644 src/interfaces/ISubscriptionError.ts create mode 100644 src/interfaces/ISubscriptionMessage.ts create mode 100644 src/interfaces/ISubscriptionSuccess.ts create mode 100644 src/interfaces/index.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9c61627 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cc5f6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +node_modules +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..830cd4e --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Browser Javascript client for Centrifugo +======================================== + +See client [documentation](https://fzambia.gitbooks.io/centrifugal/content/clients/javascript.html) + +### Contribute + +If you want to contribute, install dependencies and run `npm run build` command from root repo folder - this results +in creating unminified (`centrifuge.js`) and minified (`centrifuge.min.js`) bundles in `dist` directory. diff --git a/dist/centrifuge.js b/dist/centrifuge.js new file mode 100644 index 0000000..5430a27 --- /dev/null +++ b/dist/centrifuge.js @@ -0,0 +1,1504 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else { + var a = factory(); + for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; + } +})(typeof self !== 'undefined' ? self : this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); +/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Centrifuge", function() { return Centrifuge; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__Functions__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Observable__ = __webpack_require__(2); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Subscription__ = __webpack_require__(4); +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); + + + +var Centrifuge = (function (_super) { + __extends(Centrifuge, _super); + function Centrifuge(config) { + var _this = _super.call(this) || this; + _this._config = {}; + _this._status = 'disconnected'; + _this._isSockJS = false; + _this._transport = null; + _this._transportName = null; + _this._transportClosed = true; + _this._reconnect = true; + _this._reconnecting = false; + _this._numRefreshFailed = 0; + _this._refreshTimeout = null; + _this._pongTimeout = null; + _this._pingInterval = null; + _this._messageId = 0; + _this._messages = []; + _this._isBatching = false; + _this._isAuthBatching = false; + _this._authChannels = {}; + _this._clientID = null; + _this._callbacks = {}; + _this._subs = {}; + _this._retries = 0; + _this._latency = null; + _this._latencyStart = null; + _this._lastMessageID = {}; + _this._configure(config); + return _this; + } + Centrifuge.prototype.connect = function (callback) { + if (this.isConnected()) { + this._debug('connect called when already connected'); + return; + } + if (this._status === 'connecting') { + return; + } + this._debug('start connecting'); + this._setStatus('connecting'); + this._clientID = null; + this._reconnect = true; + if (callback) { + this.on('connect', callback); + } + this._setTransport(); + }; + Centrifuge.prototype.disconnect = function () { + this._disconnect('client', false); + }; + Centrifuge.prototype.isConnected = function () { + return this._status === 'connected'; + }; + Centrifuge.prototype.isDisconnected = function () { + return this._status === 'disconnected'; + }; + Centrifuge.prototype.ping = function () { + this.addMessage({ + method: 'ping' + }, false); + }; + Centrifuge.prototype.startBatching = function () { + this._isBatching = true; + }; + Centrifuge.prototype.stopBatching = function (flush) { + flush = flush || false; + this._isBatching = false; + if (flush === true) { + this.flush(); + } + }; + Centrifuge.prototype.flush = function () { + var messages = this._messages.slice(0); + this._messages = []; + this._send(messages); + }; + Centrifuge.prototype.startAuthBatching = function () { + this._isAuthBatching = true; + }; + Centrifuge.prototype.stopAuthBatching = function () { + var _this = this; + var i; + var channel; + this._isAuthBatching = false; + var authChannels = this._authChannels; + this._authChannels = {}; + var channels = []; + for (channel in authChannels) { + if (authChannels.hasOwnProperty(channel)) { + if (!this._getSub(channel)) { + continue; + } + channels.push(channel); + } + } + if (channels.length === 0) { + return; + } + var cb = function (error, data) { + if (error === true) { + _this._debug('authorization request failed'); + for (i in channels) { + if (channels.hasOwnProperty(i)) { + channel = channels[i]; + _this._subscribeError({ + error: 'authorization request failed', + advice: 'fix', + body: { + channel: channel, + } + }); + } + } + return; + } + var batch = false; + if (!_this._isBatching) { + _this.startBatching(); + batch = true; + } + for (i in channels) { + if (channels.hasOwnProperty(i)) { + channel = channels[i]; + var channelResponse = data[channel]; + if (!channelResponse) { + _this._subscribeError({ + error: 'channel not found in authorization response', + advice: 'fix', + body: { + channel: channel, + } + }); + continue; + } + if (!channelResponse.status || channelResponse.status === 200) { + var msg = { + method: 'subscribe', + params: { + channel: channel, + client: _this.getClientId(), + info: channelResponse.info, + sign: channelResponse.sign + } + }; + if (_this._recover(channel) === true) { + msg.params.recover = true; + msg.params.last = _this._getLastID(channel); + } + _this.addMessage(msg).then(function (response) { + _this._subscribeResponse(response); + }, function (error) { + }); + } + else { + _this._subscribeError({ + error: channelResponse.status, + body: { + channel: channel + } + }); + } + } + } + if (batch) { + _this.stopBatching(true); + } + }; + var data = { + client: this.getClientId(), + channels: channels, + }; + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["c" /* isFunction */])(this._config.onPrivateChannelAuth)) { + this._config.onPrivateChannelAuth({ + data: data, + }, cb); + } + else { + var transport = this._config.authTransport.toLowerCase(); + if (transport === 'ajax') { + this._ajax(this._config.authEndpoint, this._config.authParams, this._config.authHeaders, data, cb); + } + else if (transport === 'jsonp') { + this._jsonp(this._config.authEndpoint, this._config.authParams, this._config.authHeaders, data, cb); + } + else { + throw 'Unknown private channel auth transport ' + transport; + } + } + }; + Centrifuge.prototype.subscribe = function (channel, events) { + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(channel)) { + 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 currentSub = this._getSub(channel); + if (currentSub !== null) { + currentSub.setEvents(events); + if (currentSub.isUnsubscribed()) { + currentSub.subscribe(); + } + return currentSub; + } + else { + var sub = new __WEBPACK_IMPORTED_MODULE_2__Subscription__["a" /* Subscription */](this, channel, events); + this._subs[channel] = sub; + sub.subscribe(); + return sub; + } + }; + Centrifuge.prototype.subscribeSub = function (sub) { + var _this = this; + var channel = sub.channel; + if (!(channel in this._subs)) { + this._subs[channel] = sub; + } + if (!this.isConnected()) { + sub.setNew(); + return; + } + sub.setSubscribing(); + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["h" /* startsWith */])(channel, this._config.privateChannelPrefix)) { + if (this._isAuthBatching) { + this._authChannels[channel] = true; + } + else { + this.startAuthBatching(); + this.subscribeSub(sub); + this.stopAuthBatching(); + } + } + else { + var msg = { + method: 'subscribe', + params: { + channel: channel, + } + }; + if (this._recover(channel) === true) { + msg.params.recover = true; + msg.params.last = this._getLastID(channel); + } + this.addMessage(msg).then(function (response) { + _this._subscribeResponse(response); + }, function (error) { + _this._subscribeError(error); + }); + } + }; + Centrifuge.prototype.unsubscribeSub = function (sub) { + var _this = this; + if (this.isConnected()) { + this.addMessage({ + method: 'unsubscribe', + params: { + channel: sub.channel + } + }).then(function (response) { + _this._unsubscribeResponse(response); + }, function (error) { + }); + } + }; + Centrifuge.prototype.getClientId = function () { + return this._clientID; + }; + Centrifuge.prototype.registerCall = function (uid, callback, errback) { + var _this = this; + this._callbacks[uid] = { + callback: callback, + errback: errback + }; + setTimeout(function () { + delete _this._callbacks[uid]; + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["c" /* isFunction */])(errback)) { + errback(Centrifuge.createErrorObject('timeout', 'retry')); + } + }, this._config.timeout); + }; + Centrifuge.prototype.addMessage = function (message, registerCall) { + var _this = this; + return new Promise(function (resolve, reject) { + var uid = _this._getNextMessageId() + ''; + message.uid = uid; + if (_this._isBatching === true) { + _this._messages.push(message); + } + else { + _this._send([message]); + } + if (registerCall !== false) { + _this.registerCall(uid, resolve, reject); + } + }); + }; + Centrifuge.createErrorObject = function (error, advice) { + var result = { + error: error, + }; + if (advice) { + result.advice = advice; + } + return result; + }; + Centrifuge.log = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["f" /* log */])('info', args); + }; + Centrifuge.prototype._debug = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (this._config.debug === true) { + Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["f" /* log */])('debug', args); + } + }; + Centrifuge.prototype._jsonp = function (url, params, headers, data, callback) { + if (Object.keys(headers).length > 0) { + Centrifuge.log('Only AJAX request allows to send custom headers, it is not possible with JSONP.'); + } + this._debug('sending JSONP request to', url); + var callbackName = 'centrifuge_jsonp_' + Centrifuge.nextJSONPCallbackID.toString(); + Centrifuge.nextJSONPCallbackID++; + var script = document.createElement('script'); + var timeoutTrigger = setTimeout(function () { + Centrifuge.jsonpCallbacks[callbackName] = function () { + }; + callback(true, 'timeout'); + }, 3000); + Centrifuge.jsonpCallbacks[callbackName] = function (callbackData) { + clearTimeout(timeoutTrigger); + callback(false, callbackData); + delete Centrifuge.jsonpCallbacks[callbackName]; + }; + var callback_name = 'Centrifuge._jsonpCallbacks[\'' + callbackName + '\']'; + script.src = this._config.authEndpoint + + '?callback=' + encodeURIComponent(callback_name) + + '&data=' + encodeURIComponent(JSON.stringify(data)) + + '&' + Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["g" /* objectToQuery */])(params); + var head = document.getElementsByTagName('head')[0] || document.documentElement; + head.insertBefore(script, head.firstChild); + }; + Centrifuge.prototype._ajax = function (url, params, headers, data, callback) { + this._debug('sending AJAX request to', url); + var xhr = (XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')); + var query = Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["g" /* objectToQuery */])(params); + if (query.length > 0) { + query = '?' + query; + } + xhr.open('POST', url + query, true); + if ('withCredentials' in xhr) { + xhr.withCredentials = true; + } + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('Content-Type', 'application/json'); + for (var headerName in headers) { + if (headers.hasOwnProperty(headerName)) { + xhr.setRequestHeader(headerName, headers[headerName]); + } + } + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + var data_1, parsed = false; + try { + data_1 = JSON.parse(xhr.responseText); + parsed = true; + } + catch (e) { + callback(true, 'JSON returned was invalid, yet status code was 200. Data was: ' + xhr.responseText); + } + if (parsed) { + callback(false, data_1); + } + } + else { + Centrifuge.log('Could not get auth info from application', xhr.status); + callback(true, xhr.status); + } + } + }; + setTimeout(function () { + xhr.send(JSON.stringify(data)); + }, 20); + return xhr; + }; + Centrifuge.prototype._configure = function (config) { + this._debug('Configuring centrifuge object with', config); + config = Object.assign({ + retry: 1000, + maxRetry: 20000, + timeout: 5000, + info: '', + resubscribe: true, + ping: true, + pingInterval: 30000, + pongWaitTimeout: 5000, + debug: false, + insecure: false, + privateChannelPrefix: '$', + refreshEndpoint: '/centrifuge/refresh/', + refreshHeaders: {}, + refreshParams: {}, + refreshData: {}, + refreshTransport: 'ajax', + refreshAttempts: 0, + refreshInterval: 3000, + authEndpoint: '/centrifuge/auth/', + authHeaders: {}, + authParams: {}, + authTransport: 'ajax', + }, config); + if (!config.url) { + throw 'Missing required configuration parameter \'url\' specifying server URL'; + } + config.url = Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["i" /* stripSlash */])(config.url); + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["a" /* endsWith */])(config.url, 'connection/websocket')) { + this._debug('client will connect to raw Websocket endpoint'); + } + else { + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["a" /* endsWith */])(config.url, 'connection')) { + this._debug('client will connect to SockJS endpoint'); + } + else { + this._debug('client will detect connection endpoint itself'); + } + if (config.sockJS !== null) { + this._debug('SockJS explicitly provided in options'); + } + else { + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["e" /* isUndefined */])(global['SockJS'])) { + throw 'Include SockJS client library before Centrifuge javascript client library or provide SockJS object in options or use raw Websocket connection endpoint'; + } + else { + this._debug('Use globally defined SockJS'); + config.sockJS = global['SockJS']; + } + } + } + if (!config.user) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'user\' specifying user\'s unique ID in your application'; + } + else { + this._debug('user not found but this is OK for insecure mode - anonymous access will be used'); + } + } + else { + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(config.user)) { + Centrifuge.log('Configuration parameter \'user\' expected to be string'); + } + } + if (!config.timestamp) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'timestamp\''; + } + else { + this._debug('Configuration parameter \'v\' not found but this is OK for insecure mode'); + } + } + else { + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(config.timestamp)) { + Centrifuge.log('Configuration parameter \'timestamp\' expected to be string'); + } + } + if (!config.token) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'token\' specifying the sign of authorization request'; + } + else { + this._debug('Configuration parameter \'token\' not found but this is OK for insecure mode'); + } + } + else { + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(config.token)) { + Centrifuge.log('Configuration parameter \'token\' expected to be string'); + } + } + if (config.info && !Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(config.info)) { + Centrifuge.log('Configuration parameter \'info\' expected to be string'); + } + this._config = config; + }; + Centrifuge.prototype._getSockjsEndpoint = function () { + var url = this._config.url; + url = url + .replace('ws://', 'http://') + .replace('wss://', 'https://'); + url = Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["i" /* stripSlash */])(url); + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["a" /* endsWith */])(this._config.url, 'connection')) { + url = url + '/connection'; + } + return url; + }; + Centrifuge.prototype._getWebsocketEndpoint = function () { + var url = this._config.url; + url = url + .replace('http://', 'ws://') + .replace('https://', 'wss://'); + url = Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["i" /* stripSlash */])(url); + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["a" /* endsWith */])(this._config.url, 'connection/websocket')) { + url = url + '/connection/websocket'; + } + return url; + }; + Centrifuge.prototype._recover = function (channel) { + return channel in this._lastMessageID; + }; + Centrifuge.prototype._getLastID = function (channel) { + var lastUID = this._lastMessageID[channel]; + if (lastUID) { + this._debug('Last uid found and sent for channel', channel); + return lastUID; + } + else { + this._debug('No last uid found for channel', channel); + return ''; + } + }; + Centrifuge.prototype._getSub = function (channel) { + return this._subs[channel] || null; + }; + Centrifuge.prototype._clearConnectedState = function (reconnect) { + this._clientID = null; + for (var uid in this._callbacks) { + if (this._callbacks.hasOwnProperty(uid)) { + var callbacks = this._callbacks[uid]; + var errback = callbacks.errback; + if (!errback) { + continue; + } + errback(Centrifuge.createErrorObject('disconnected', 'retry')); + } + } + this._callbacks = {}; + for (var channel in this._subs) { + if (this._subs.hasOwnProperty(channel)) { + var sub = this._getSub(channel); + if (reconnect) { + if (sub.isSuccess()) { + sub.triggerUnsubscribe(); + } + sub.setSubscribing(); + } + else { + sub.setUnsubscribed(); + } + } + } + if (!this._config.resubscribe || !this._reconnect) { + this._subs = {}; + } + }; + Centrifuge.prototype._setStatus = function (newStatus) { + if (this._status !== newStatus) { + this._debug('Status', this._status, '->', newStatus); + this._status = newStatus; + } + }; + Centrifuge.prototype._disconnect = function (reason, shouldReconnect) { + if (this.isDisconnected()) { + return; + } + this._debug('disconnected:', reason, shouldReconnect); + var reconnect = shouldReconnect || false; + if (reconnect === false) { + this._reconnect = false; + } + this._clearConnectedState(reconnect); + if (!this.isDisconnected()) { + this._setStatus('disconnected'); + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (this._reconnecting === false) { + this.trigger('disconnect', [{ + reason: reason, + reconnect: reconnect, + }]); + } + } + if (!this._transportClosed) { + this._transport.close(); + } + }; + Centrifuge.prototype._send = function (messages) { + if (!messages.length) { + return; + } + this._debug('Send', messages); + var encodedMessages = []; + for (var i in messages) { + encodedMessages.push(JSON.stringify(messages[i])); + } + this._transport.send(encodedMessages.join("\n")); + }; + Centrifuge.prototype._getNextMessageId = function () { + return ++this._messageId; + }; + Centrifuge.prototype._stopPing = function () { + if (this._pongTimeout !== null) { + clearTimeout(this._pongTimeout); + } + if (this._pingInterval !== null) { + clearInterval(this._pingInterval); + } + }; + Centrifuge.prototype._startPing = function () { + var _this = this; + if (this._config.ping !== true || this._config.pingInterval <= 0 || !this.isConnected()) { + return; + } + this._pingInterval = setInterval(function () { + if (!_this.isConnected()) { + _this._stopPing(); + return; + } + _this.ping(); + _this._pongTimeout = setTimeout(function () { + this._disconnect('no ping', true); + }, _this._config.pongWaitTimeout); + }, this._config.pingInterval); + }; + Centrifuge.prototype._restartPing = function () { + this._stopPing(); + this._startPing(); + }; + Centrifuge.prototype._resetRetry = function () { + this._debug('reset retries count to 0'); + this._retries = 0; + }; + Centrifuge.prototype._getRetryInterval = function () { + this._retries += 1; + var jitter = 0.5 * Math.random(); + var interval = this._config.retry * Math.pow(2, this._retries + 1); + if (interval > this._config.maxRetry) { + interval = this._config.maxRetry; + } + return Math.floor((1 - jitter) * interval); + }; + Centrifuge.prototype._refreshFailed = function () { + this._numRefreshFailed = 0; + if (!this.isDisconnected()) { + this._disconnect('refresh failed'); + } + if (this._config.refreshFailed) { + this._config.refreshFailed(); + } + }; + Centrifuge.prototype._refresh = function () { + var _this = this; + this._debug('refresh credentials'); + if (this._config.refreshAttempts === 0) { + this._debug('refresh attempts set to 0, do not send refresh request at all'); + this._refreshFailed(); + return; + } + if (this._refreshTimeout !== null) { + clearTimeout(this._refreshTimeout); + } + var cb = function (error, data) { + if (error === true) { + _this._debug('error getting connection credentials from refresh endpoint', data); + _this._numRefreshFailed++; + if (_this._refreshTimeout) { + clearTimeout(_this._refreshTimeout); + } + if (_this._config.refreshAttempts !== null && _this._numRefreshFailed >= _this._config.refreshAttempts) { + _this._refreshFailed(); + return; + } + _this._refreshTimeout = setTimeout(function () { + _this._refresh(); + }, _this._config.refreshInterval + Math.round(Math.random() * 1000)); + return; + } + _this._numRefreshFailed = 0; + _this._config.user = data.user; + _this._config.timestamp = data.timestamp; + _this._config.token = data.token; + if ('info' in data) { + _this._config.info = data.info; + } + else { + data.info = ''; + } + if (_this.isDisconnected()) { + _this._debug('credentials refreshed, connect from scratch'); + _this.connect(); + } + else { + _this._debug('send refreshed credentials'); + _this.addMessage({ + method: 'refresh', + params: data, + }).then(function (response) { + _this._refreshResponse(response); + }, function (error) { + }); + } + }; + if (this._config.onRefresh !== null) { + this._config.onRefresh({}, cb); + } + else { + var transport = this._config.refreshTransport.toLowerCase(); + if (transport === 'ajax') { + this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, this._config.refreshData, cb); + } + else if (transport === 'jsonp') { + this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, this._config.refreshData, cb); + } + else { + throw 'Unknown refresh transport ' + transport; + } + } + }; + Centrifuge.prototype._connectResponse = function (response) { + var _this = this; + if (this.isConnected()) { + return; + } + if (this._latencyStart !== null) { + this._latency = (new Date()).getTime() - this._latencyStart.getTime(); + this._latencyStart = null; + } + if (response.expires) { + if (response.expired) { + this._reconnecting = true; + this._disconnect('expired', true); + this._refresh(); + return; + } + } + this._clientID = response.client; + this._setStatus('connected'); + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (response.expires) { + this._refreshTimeout = setTimeout(function () { + _this._refresh(); + }, response.ttl * 1000); + } + if (this._config.resubscribe) { + this.startBatching(); + this.startAuthBatching(); + for (var channel in this._subs) { + if (this._subs.hasOwnProperty(channel)) { + var sub = this._getSub(channel); + if (sub.shouldResubscribe()) { + this.subscribeSub(sub); + } + } + } + this.stopAuthBatching(); + this.stopBatching(true); + } + this._restartPing(); + this.trigger('connect', [{ + client: response.client, + transport: this._transportName, + latency: this._latency + }]); + }; + Centrifuge.prototype._subscribeResponse = function (response) { + var channel = response.channel; + var sub = this._getSub(channel); + if (!sub || !sub.isSubscribing()) { + return; + } + var messages = response.messages; + if (messages && messages.length > 0) { + messages = messages.reverse(); + for (var i in messages) { + if (messages.hasOwnProperty(i)) { + this._messageResponse({ + body: messages[i] + }); + } + } + } + else { + if ('last' in response) { + this._lastMessageID[channel] = response.last; + } + } + var recovered = false; + if ('recovered' in response) { + recovered = response.recovered; + } + sub.setSubscribeSuccess(recovered); + }; + Centrifuge.prototype._subscribeError = function (error) { + var channel = error.channel; + var sub = this._getSub(channel); + if (!sub || !sub.isSubscribing()) { + return; + } + this.trigger('error', [{ + error: error, + }]); + sub.setSubscribeError(error); + }; + Centrifuge.prototype._unsubscribeResponse = function (response) { + var sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.setUnsubscribed(); + }; + Centrifuge.prototype._joinResponse = function (response) { + var sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.trigger('join', [response]); + }; + Centrifuge.prototype._leaveResponse = function (response) { + var sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.trigger('leave', [response]); + }; + Centrifuge.prototype._refreshResponse = function (response) { + var _this = this; + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (response.expires) { + if (response.expired) { + this._refreshTimeout = setTimeout(function () { + _this._refresh(); + }, this._config.refreshInterval + Math.round(Math.random() * 1000)); + return; + } + this._clientID = response.client; + this._refreshTimeout = setTimeout(function () { + _this._refresh(); + }, response.ttl * 1000); + } + }; + Centrifuge.prototype._messageResponse = function (message) { + var body = message.body; + var channel = body.channel; + this._lastMessageID[channel] = body.uid; + var sub = this._getSub(channel); + if (!sub) { + return; + } + sub.trigger('message', [body]); + }; + Centrifuge.prototype._handleResponse = function (message) { + var uid = message.uid; + if (!(uid in this._callbacks)) { + return; + } + var callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["b" /* errorExists */])(message)) { + var callback = callbacks.callback; + if (!callback) { + return; + } + callback(message.body); + } + else { + var errback = callbacks.errback; + if (!errback) { + return; + } + errback(Centrifuge.createErrorObject(message.error, message.advice)); + this.trigger('error', [{ + message: message, + }]); + } + }; + Centrifuge.prototype._dispatchMessage = function (message) { + if (message === undefined || message === null) { + this._debug('dispatch: got undefined or null message'); + return; + } + switch (message.method) { + case 'join': + this._joinResponse(message); + break; + case 'leave': + this._leaveResponse(message); + break; + case 'message': + this._messageResponse(message); + break; + default: + this._handleResponse(message); + } + }; + Centrifuge.prototype._receive = function (data) { + if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) { + for (var i in data) { + if (data.hasOwnProperty(i)) { + this._dispatchMessage(data[i]); + } + } + } + else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) { + this._dispatchMessage(data); + } + }; + Centrifuge.prototype._setTransport = function () { + var _this = this; + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["c" /* isFunction */])(this._config.sockJS)) { + var sockjsOptions = { + transports: this._config.transports || [ + 'websocket', + 'xdr-streaming', + 'xhr-streaming', + 'eventsource', + 'iframe-eventsource', + 'iframe-htmlfile', + 'xdr-polling', + 'xhr-polling', + 'iframe-xhr-polling', + 'jsonp-polling' + ] + }; + if (this._config.server) { + sockjsOptions.server = this._config.server; + } + this._transport = new this._config.sockJS(this._getSockjsEndpoint(), null, sockjsOptions); + this._isSockJS = true; + } + else { + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["c" /* isFunction */])(WebSocket)) { + throw 'No Websocket support and no SockJS configured, can not connect'; + } + this._transport = new WebSocket(this._getWebsocketEndpoint()); + } + this._transport.onopen = function () { + _this._transportClosed = false; + _this._reconnecting = false; + if (_this._isSockJS) { + _this._transportName = _this._transport.transport; + _this._transport.onheartbeat = function () { + this._restartPing(); + }; + } + else { + _this._transportName = 'raw-websocket'; + } + _this._resetRetry(); + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(_this._config.user)) { + Centrifuge.log('user expected to be string'); + } + if (!Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["d" /* isString */])(_this._config.info)) { + Centrifuge.log('info expected to be string'); + } + var msg = { + method: 'connect', + params: { + user: _this._config.user, + info: _this._config.info, + } + }; + if (!_this._config.insecure) { + msg.params.timestamp = _this._config.timestamp; + msg.params.token = _this._config.token; + } + _this._latencyStart = new Date(); + _this.addMessage(msg).then(function (response) { + _this._connectResponse(response); + }, function (error) { + }); + }; + this._transport.onerror = function (error) { + _this._debug('transport level error', error); + }; + this._transport.onclose = function (event) { + _this._transportClosed = true; + var reason = 'connection closed'; + var reconnect = true; + if (event && 'reason' in event && event.reason) { + try { + var advice = JSON.parse(event.reason); + _this._debug('reason is an advice object', advice); + reason = advice.reason; + reconnect = advice.reconnect; + } + catch (e) { + reason = event.reason; + _this._debug('reason is a plain string', reason); + reconnect = reason !== 'disconnect'; + } + } + if (_this._config.onTransportClose) { + _this._config.onTransportClose({ + event: event, + reason: reason, + reconnect: reconnect, + }); + } + _this._disconnect(reason, reconnect); + if (_this._reconnect === true) { + _this._reconnecting = true; + var interval = _this._getRetryInterval(); + _this._debug('reconnect after ' + interval + ' milliseconds'); + setTimeout(function () { + if (_this._reconnect === true) { + _this.connect(); + } + }, interval); + } + }; + this._transport.onmessage = function (event) { + var replies = event.data.split("\n"); + for (var i in replies) { + if (replies.hasOwnProperty(i) && replies[i]) { + var data = JSON.parse(replies[i]); + _this._debug('Received', data); + _this._receive(data); + } + } + _this._restartPing(); + }; + }; + Centrifuge.jsonpCallbacks = {}; + Centrifuge.nextJSONPCallbackID = 1; + return Centrifuge; +}(__WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */])); + + +/* WEBPACK VAR INJECTION */}.call(__webpack_exports__, __webpack_require__(3))) + +/***/ }), +/* 1 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (immutable) */ __webpack_exports__["e"] = isUndefined; +/* harmony export (immutable) */ __webpack_exports__["c"] = isFunction; +/* harmony export (immutable) */ __webpack_exports__["d"] = isString; +/* harmony export (immutable) */ __webpack_exports__["f"] = log; +/* harmony export (immutable) */ __webpack_exports__["i"] = stripSlash; +/* harmony export (immutable) */ __webpack_exports__["h"] = startsWith; +/* harmony export (immutable) */ __webpack_exports__["a"] = endsWith; +/* harmony export (immutable) */ __webpack_exports__["b"] = errorExists; +/* harmony export (immutable) */ __webpack_exports__["g"] = objectToQuery; +function isUndefined(value) { + return typeof value === 'undefined'; +} +function isFunction(value) { + return typeof value === 'function'; +} +function isString(value) { + return typeof value === 'string'; +} +function log(level) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + if (console) { + if (args.length === 1) { + args = args[0]; + } + var logger = console[level]; + if (isFunction(logger)) { + logger.apply(logger, args); + } + } +} +function stripSlash(value) { + return value.replace(/\/$/, ''); +} +function startsWith(value, prefix) { + return value.lastIndexOf(prefix, 0) === 0; +} +function endsWith(value, suffix) { + return value.indexOf(suffix, value.length - suffix.length) !== -1; +} +function errorExists(data) { + return 'error' in data && data.error !== null && data.error !== ''; +} +function objectToQuery(object) { + var p = []; + for (var i in object) { + if (object.hasOwnProperty(i)) { + p.push(encodeURIComponent(i) + (object[i] ? '=' + encodeURIComponent(object[i]) : '')); + } + } + return p.join('&'); +} + + +/***/ }), +/* 2 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Observable; }); +var Observable = (function () { + function Observable() { + this.__callbacks = {}; + } + Observable.prototype.on = function (events, fn) { + var _this = this; + events.replace(/[^\s]+/g, function (name) { + (_this.__callbacks[name] = _this.__callbacks[name] || []).push(fn); + return ''; + }); + return this; + }; + Observable.prototype.one = function (name, fn) { + fn['one'] = true; + return this.on(name, fn); + }; + Observable.prototype.off = function (events, fn) { + var _this = this; + if (events === '*') { + this.__callbacks = {}; + } + else if (fn) { + var arr = this.__callbacks[events]; + for (var i = 0, cb = void 0; (cb = arr && arr[i]); ++i) { + if (cb === fn) { + arr.splice(i, 1); + } + } + } + else { + events.replace(/[^\s]+/g, function (name) { + _this.__callbacks[name] = []; + return ''; + }); + } + return this; + }; + Observable.prototype.trigger = function (name, args) { + var fns = this.__callbacks[name] || []; + for (var i = 0, fn = void 0; (fn = fns[i]); ++i) { + if (!fn['busy']) { + fn['busy'] = true; + fn.apply(this, args); + if (fn['one']) { + fns.splice(i, 1); + i--; + } + fn['busy'] = false; + } + } + return this; + }; + return Observable; +}()); + + + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +var g; + +// This works in non-strict mode +g = (function() { + return this; +})(); + +try { + // This works if eval is allowed (see CSP) + g = g || Function("return this")() || (1,eval)("this"); +} catch(e) { + // This works if the window reference is available + if(typeof window === "object") + g = window; +} + +// g can still be undefined, but nothing to do about it... +// We return undefined, instead of nothing here, so it's +// easier to handle this case. if(!global) { ...} + +module.exports = g; + + +/***/ }), +/* 4 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subscription; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__Functions__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Observable__ = __webpack_require__(2); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Centrifuge__ = __webpack_require__(0); +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); + + + +var Subscription = (function (_super) { + __extends(Subscription, _super); + function Subscription(centrifuge, channel, events) { + var _this = _super.call(this) || this; + _this.channel = null; + _this._status = Subscription.STATE_NEW; + _this._error = null; + _this._centrifuge = null; + _this._isResubscribe = false; + _this._recovered = false; + _this._ready = false; + _this._promise = null; + _this._noResubscribe = false; + _this._centrifuge = centrifuge; + _this.channel = channel; + _this.setEvents(events); + _this._initializePromise(); + return _this; + } + Subscription.prototype.setEvents = function (events) { + if (!events) { + return; + } + if (Object(__WEBPACK_IMPORTED_MODULE_0__Functions__["c" /* 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 = 0, l = knownEvents.length; i < l; i++) { + var ev = knownEvents[i]; + if (ev in events) { + this.on(ev, events[ev]); + } + } + } + }; + Subscription.prototype.setNew = function () { + this._status = Subscription.STATE_NEW; + }; + Subscription.prototype.setSubscribing = function () { + if (this._ready === true) { + this._initializePromise(); + this._isResubscribe = true; + } + this._status = Subscription.STATE_SUBSCRIBING; + }; + Subscription.prototype.setUnsubscribed = function (noResubscribe) { + if (this._status === Subscription.STATE_UNSUBSCRIBED) { + return; + } + this._status = Subscription.STATE_UNSUBSCRIBED; + if (noResubscribe === true) { + this._noResubscribe = true; + } + this.triggerUnsubscribe(); + }; + Subscription.prototype.setSubscribeSuccess = function (recovered) { + if (this._status === Subscription.STATE_SUCCESS) { + return; + } + this._recovered = recovered; + this._status = Subscription.STATE_SUCCESS; + var successContext = this._getSubscribeSuccess(); + this.trigger('subscribe', [successContext]); + this._resolve(successContext); + }; + Subscription.prototype.setSubscribeError = function (err) { + if (this._status === Subscription.STATE_ERROR) { + return; + } + this._status = Subscription.STATE_ERROR; + this._error = err; + var errContext = this._getSubscribeError(); + this.trigger('error', [errContext]); + this._reject(errContext); + }; + Subscription.prototype.triggerUnsubscribe = function () { + this.trigger('unsubscribe', [{ + channel: this.channel + }]); + }; + Subscription.prototype.isUnsubscribed = function () { + return this._status === Subscription.STATE_UNSUBSCRIBED; + }; + Subscription.prototype.isSuccess = function () { + return this._status === Subscription.STATE_SUCCESS; + }; + Subscription.prototype.isSubscribing = function () { + return this._status === Subscription.STATE_SUBSCRIBING; + }; + Subscription.prototype.shouldResubscribe = function () { + return !this._noResubscribe; + }; + Subscription.prototype.ready = function (callback, errback) { + if (this._ready) { + if (this.isSuccess()) { + callback(this._getSubscribeSuccess()); + } + else { + errback(this._getSubscribeError()); + } + } + }; + Subscription.prototype.subscribe = function () { + if (this._status === Subscription.STATE_SUCCESS) { + return; + } + this._centrifuge.subscribeSub(this); + return this; + }; + Subscription.prototype.unsubscribe = function () { + this.setUnsubscribed(true); + this._centrifuge.unsubscribeSub(this); + }; + Subscription.prototype.publish = function (data) { + return this._request('publish', data); + }; + Subscription.prototype.presence = function () { + return this._request('presence'); + }; + Subscription.prototype.history = function () { + return this._request('history'); + }; + Subscription.prototype._initializePromise = function () { + var _this = this; + this._ready = false; + this._promise = new Promise(function (resolve, reject) { + _this._resolve = function (value) { + _this._ready = true; + resolve(value); + }; + _this._reject = function (err) { + _this._ready = true; + reject(err); + }; + }); + }; + Subscription.prototype._getSubscribeSuccess = function () { + return { + channel: this.channel, + isResubscribe: this._isResubscribe, + recovered: this._recovered + }; + }; + Subscription.prototype._getSubscribeError = function () { + var subscribeError = this._error; + subscribeError.channel = this.channel; + subscribeError.isResubscribe = this._isResubscribe; + return subscribeError; + }; + Subscription.prototype._request = function (method, data) { + var _this = this; + return new Promise(function (resolve, reject) { + if (_this.isUnsubscribed()) { + reject(__WEBPACK_IMPORTED_MODULE_2__Centrifuge__["Centrifuge"].createErrorObject('subscription unsubscribed', 'fix')); + return; + } + _this._promise.then(function () { + if (!_this._centrifuge.isConnected()) { + reject(__WEBPACK_IMPORTED_MODULE_2__Centrifuge__["Centrifuge"].createErrorObject('disconnected', 'retry')); + return; + } + var params = { + channel: _this.channel, + }; + if (data) { + params['data'] = data; + } + _this._centrifuge.addMessage({ + method: method, + params: params, + }).then(function (response) { + resolve(response); + }, function (error) { + reject(error); + }); + }, function (err) { + reject(err); + }); + }); + }; + Subscription.STATE_NEW = 0; + Subscription.STATE_SUBSCRIBING = 1; + Subscription.STATE_SUCCESS = 2; + Subscription.STATE_ERROR = 3; + Subscription.STATE_UNSUBSCRIBED = 4; + return Subscription; +}(__WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */])); + + + +/***/ }) +/******/ ]); +}); \ No newline at end of file diff --git a/dist/centrifuge.min.js b/dist/centrifuge.min.js new file mode 100644 index 0000000..fa66efa --- /dev/null +++ b/dist/centrifuge.min.js @@ -0,0 +1 @@ +(function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n=e();for(var s in n)("object"==typeof exports?exports:t)[s]=n[s]}})("undefined"!=typeof self?self:this,function(){return function(t){function e(s){if(n[s])return n[s].exports;var i=n[s]={i:s,l:!1,exports:{}};return t[s].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.d=function(t,n,s){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:s})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=0)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),function(t){n.d(e,"Centrifuge",function(){return c});var s=n(1),i=n(2),r=n(4),o=this&&this.__extends||function(){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])};return function(e,n){function s(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(s.prototype=n.prototype,new s)}}(),c=function(e){function n(t){var n=e.call(this)||this;return n._config={},n._status="disconnected",n._isSockJS=!1,n._transport=null,n._transportName=null,n._transportClosed=!0,n._reconnect=!0,n._reconnecting=!1,n._numRefreshFailed=0,n._refreshTimeout=null,n._pongTimeout=null,n._pingInterval=null,n._messageId=0,n._messages=[],n._isBatching=!1,n._isAuthBatching=!1,n._authChannels={},n._clientID=null,n._callbacks={},n._subs={},n._retries=0,n._latency=null,n._latencyStart=null,n._lastMessageID={},n._configure(t),n}return o(n,e),n.prototype.connect=function(t){if(this.isConnected())return void this._debug("connect called when already connected");"connecting"!==this._status&&(this._debug("start connecting"),this._setStatus("connecting"),this._clientID=null,this._reconnect=!0,t&&this.on("connect",t),this._setTransport())},n.prototype.disconnect=function(){this._disconnect("client",!1)},n.prototype.isConnected=function(){return"connected"===this._status},n.prototype.isDisconnected=function(){return"disconnected"===this._status},n.prototype.ping=function(){this.addMessage({method:"ping"},!1)},n.prototype.startBatching=function(){this._isBatching=!0},n.prototype.stopBatching=function(t){t=t||!1,this._isBatching=!1,!0===t&&this.flush()},n.prototype.flush=function(){var t=this._messages.slice(0);this._messages=[],this._send(t)},n.prototype.startAuthBatching=function(){this._isAuthBatching=!0},n.prototype.stopAuthBatching=function(){var t,e,n=this;this._isAuthBatching=!1;var i=this._authChannels;this._authChannels={};var r=[];for(e in i)if(i.hasOwnProperty(e)){if(!this._getSub(e))continue;r.push(e)}if(0!==r.length){var o=function(s,i){if(!0!==s){var o=!1;n._isBatching||(n.startBatching(),o=!0);for(t in r)if(r.hasOwnProperty(t)){e=r[t];var c=i[e];if(!c){n._subscribeError({error:"channel not found in authorization response",advice:"fix",body:{channel:e}});continue}if(c.status&&200!==c.status)n._subscribeError({error:c.status,body:{channel:e}});else{var a={method:"subscribe",params:{channel:e,client:n.getClientId(),info:c.info,sign:c.sign}};!0===n._recover(e)&&(a.params.recover=!0,a.params.last=n._getLastID(e)),n.addMessage(a).then(function(t){n._subscribeResponse(t)},function(t){})}}o&&n.stopBatching(!0)}else{n._debug("authorization request failed");for(t in r)r.hasOwnProperty(t)&&(e=r[t],n._subscribeError({error:"authorization request failed",advice:"fix",body:{channel:e}}))}},c={client:this.getClientId(),channels:r};if(Object(s.c)(this._config.onPrivateChannelAuth))this._config.onPrivateChannelAuth({data:c},o);else{var a=this._config.authTransport.toLowerCase();if("ajax"===a)this._ajax(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,c,o);else{if("jsonp"!==a)throw"Unknown private channel auth transport "+a;this._jsonp(this._config.authEndpoint,this._config.authParams,this._config.authHeaders,c,o)}}}},n.prototype.subscribe=function(t,e){if(!Object(s.d)(t))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(t);if(null!==n)return n.setEvents(e),n.isUnsubscribed()&&n.subscribe(),n;var i=new r.a(this,t,e);return this._subs[t]=i,i.subscribe(),i},n.prototype.subscribeSub=function(t){var e=this,n=t.channel;if(n in this._subs||(this._subs[n]=t),!this.isConnected())return void t.setNew();if(t.setSubscribing(),Object(s.h)(n,this._config.privateChannelPrefix))this._isAuthBatching?this._authChannels[n]=!0:(this.startAuthBatching(),this.subscribeSub(t),this.stopAuthBatching());else{var i={method:"subscribe",params:{channel:n}};!0===this._recover(n)&&(i.params.recover=!0,i.params.last=this._getLastID(n)),this.addMessage(i).then(function(t){e._subscribeResponse(t)},function(t){e._subscribeError(t)})}},n.prototype.unsubscribeSub=function(t){var e=this;this.isConnected()&&this.addMessage({method:"unsubscribe",params:{channel:t.channel}}).then(function(t){e._unsubscribeResponse(t)},function(t){})},n.prototype.getClientId=function(){return this._clientID},n.prototype.registerCall=function(t,e,i){var r=this;this._callbacks[t]={callback:e,errback:i},setTimeout(function(){delete r._callbacks[t],Object(s.c)(i)&&i(n.createErrorObject("timeout","retry"))},this._config.timeout)},n.prototype.addMessage=function(t,e){var n=this;return new Promise(function(s,i){var r=n._getNextMessageId()+"";t.uid=r,!0===n._isBatching?n._messages.push(t):n._send([t]),!1!==e&&n.registerCall(r,s,i)})},n.createErrorObject=function(t,e){var n={error:t};return e&&(n.advice=e),n},n.log=function(){for(var t=[],e=0;e0&&n.log("Only AJAX request allows to send custom headers, it is not possible with JSONP."),this._debug("sending JSONP request to",t);var c="centrifuge_jsonp_"+n.nextJSONPCallbackID.toString();n.nextJSONPCallbackID++;var a=document.createElement("script"),u=setTimeout(function(){n.jsonpCallbacks[c]=function(){},o(!0,"timeout")},3e3);n.jsonpCallbacks[c]=function(t){clearTimeout(u),o(!1,t),delete n.jsonpCallbacks[c]};var h="Centrifuge._jsonpCallbacks['"+c+"']";a.src=this._config.authEndpoint+"?callback="+encodeURIComponent(h)+"&data="+encodeURIComponent(JSON.stringify(r))+"&"+Object(s.g)(e);var f=document.getElementsByTagName("head")[0]||document.documentElement;f.insertBefore(a,f.firstChild)},n.prototype._ajax=function(t,e,i,r,o){this._debug("sending AJAX request to",t);var c=XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),a=Object(s.g)(e);a.length>0&&(a="?"+a),c.open("POST",t+a,!0),"withCredentials"in c&&(c.withCredentials=!0),c.setRequestHeader("X-Requested-With","XMLHttpRequest"),c.setRequestHeader("Content-Type","application/json");for(var u in i)i.hasOwnProperty(u)&&c.setRequestHeader(u,i[u]);return c.onreadystatechange=function(){if(4===c.readyState)if(200===c.status){var t,e=!1;try{t=JSON.parse(c.responseText),e=!0}catch(t){o(!0,"JSON returned was invalid, yet status code was 200. Data was: "+c.responseText)}e&&o(!1,t)}else n.log("Could not get auth info from application",c.status),o(!0,c.status)},setTimeout(function(){c.send(JSON.stringify(r))},20),c},n.prototype._configure=function(e){if(this._debug("Configuring centrifuge object with",e),e=Object.assign({retry:1e3,maxRetry:2e4,timeout:5e3,info:"",resubscribe:!0,ping:!0,pingInterval:3e4,pongWaitTimeout:5e3,debug:!1,insecure:!1,privateChannelPrefix:"$",refreshEndpoint:"/centrifuge/refresh/",refreshHeaders:{},refreshParams:{},refreshData:{},refreshTransport:"ajax",refreshAttempts:0,refreshInterval:3e3,authEndpoint:"/centrifuge/auth/",authHeaders:{},authParams:{},authTransport:"ajax"},e),!e.url)throw"Missing required configuration parameter 'url' specifying server URL";if(e.url=Object(s.i)(e.url),Object(s.a)(e.url,"connection/websocket"))this._debug("client will connect to raw Websocket endpoint");else if(Object(s.a)(e.url,"connection")?this._debug("client will connect to SockJS endpoint"):this._debug("client will detect connection endpoint itself"),null!==e.sockJS)this._debug("SockJS explicitly provided in options");else{if(Object(s.e)(t.SockJS))throw"Include SockJS client library before Centrifuge javascript client library or provide SockJS object in options or use raw Websocket connection endpoint";this._debug("Use globally defined SockJS"),e.sockJS=t.SockJS}if(e.user)Object(s.d)(e.user)||n.log("Configuration parameter 'user' expected to be string");else{if(!e.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")}if(e.timestamp)Object(s.d)(e.timestamp)||n.log("Configuration parameter 'timestamp' expected to be string");else{if(!e.insecure)throw"Missing required configuration parameter 'timestamp'";this._debug("Configuration parameter 'v' not found but this is OK for insecure mode")}if(e.token)Object(s.d)(e.token)||n.log("Configuration parameter 'token' expected to be string");else{if(!e.insecure)throw"Missing required configuration parameter 'token' specifying the sign of authorization request";this._debug("Configuration parameter 'token' not found but this is OK for insecure mode")}e.info&&!Object(s.d)(e.info)&&n.log("Configuration parameter 'info' expected to be string"),this._config=e},n.prototype._getSockjsEndpoint=function(){var t=this._config.url;return t=t.replace("ws://","http://").replace("wss://","https://"),t=Object(s.i)(t),Object(s.a)(this._config.url,"connection")||(t+="/connection"),t},n.prototype._getWebsocketEndpoint=function(){var t=this._config.url;return t=t.replace("http://","ws://").replace("https://","wss://"),t=Object(s.i)(t),Object(s.a)(this._config.url,"connection/websocket")||(t+="/connection/websocket"),t},n.prototype._recover=function(t){return t in this._lastMessageID},n.prototype._getLastID=function(t){var e=this._lastMessageID[t];return e?(this._debug("Last uid found and sent for channel",t),e):(this._debug("No last uid found for channel",t),"")},n.prototype._getSub=function(t){return this._subs[t]||null},n.prototype._clearConnectedState=function(t){this._clientID=null;for(var e in this._callbacks)if(this._callbacks.hasOwnProperty(e)){var s=this._callbacks[e],i=s.errback;if(!i)continue;i(n.createErrorObject("disconnected","retry"))}this._callbacks={};for(var r in this._subs)if(this._subs.hasOwnProperty(r)){var o=this._getSub(r);t?(o.isSuccess()&&o.triggerUnsubscribe(),o.setSubscribing()):o.setUnsubscribed()}this._config.resubscribe&&this._reconnect||(this._subs={})},n.prototype._setStatus=function(t){this._status!==t&&(this._debug("Status",this._status,"->",t),this._status=t)},n.prototype._disconnect=function(t,e){if(!this.isDisconnected()){this._debug("disconnected:",t,e);var n=e||!1;!1===n&&(this._reconnect=!1),this._clearConnectedState(n),this.isDisconnected()||(this._setStatus("disconnected"),this._refreshTimeout&&clearTimeout(this._refreshTimeout),!1===this._reconnecting&&this.trigger("disconnect",[{reason:t,reconnect:n}])),this._transportClosed||this._transport.close()}},n.prototype._send=function(t){if(t.length){this._debug("Send",t);var e=[];for(var n in t)e.push(JSON.stringify(t[n]));this._transport.send(e.join("\n"))}},n.prototype._getNextMessageId=function(){return++this._messageId},n.prototype._stopPing=function(){null!==this._pongTimeout&&clearTimeout(this._pongTimeout),null!==this._pingInterval&&clearInterval(this._pingInterval)},n.prototype._startPing=function(){var t=this;!0!==this._config.ping||this._config.pingInterval<=0||!this.isConnected()||(this._pingInterval=setInterval(function(){if(!t.isConnected())return void t._stopPing();t.ping(),t._pongTimeout=setTimeout(function(){this._disconnect("no ping",!0)},t._config.pongWaitTimeout)},this._config.pingInterval))},n.prototype._restartPing=function(){this._stopPing(),this._startPing()},n.prototype._resetRetry=function(){this._debug("reset retries count to 0"),this._retries=0},n.prototype._getRetryInterval=function(){this._retries+=1;var t=.5*Math.random(),e=this._config.retry*Math.pow(2,this._retries+1);return e>this._config.maxRetry&&(e=this._config.maxRetry),Math.floor((1-t)*e)},n.prototype._refreshFailed=function(){this._numRefreshFailed=0,this.isDisconnected()||this._disconnect("refresh failed"),this._config.refreshFailed&&this._config.refreshFailed()},n.prototype._refresh=function(){var t=this;if(this._debug("refresh credentials"),0===this._config.refreshAttempts)return this._debug("refresh attempts set to 0, do not send refresh request at all"),void this._refreshFailed();null!==this._refreshTimeout&&clearTimeout(this._refreshTimeout);var e=function(e,n){if(!0===e)return t._debug("error getting connection credentials from refresh endpoint",n),t._numRefreshFailed++,t._refreshTimeout&&clearTimeout(t._refreshTimeout),null!==t._config.refreshAttempts&&t._numRefreshFailed>=t._config.refreshAttempts?void t._refreshFailed():void(t._refreshTimeout=setTimeout(function(){t._refresh()},t._config.refreshInterval+Math.round(1e3*Math.random())));t._numRefreshFailed=0,t._config.user=n.user,t._config.timestamp=n.timestamp,t._config.token=n.token,"info"in n?t._config.info=n.info:n.info="",t.isDisconnected()?(t._debug("credentials refreshed, connect from scratch"),t.connect()):(t._debug("send refreshed credentials"),t.addMessage({method:"refresh",params:n}).then(function(e){t._refreshResponse(e)},function(t){}))};if(null!==this._config.onRefresh)this._config.onRefresh({},e);else{var n=this._config.refreshTransport.toLowerCase();if("ajax"===n)this._ajax(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,this._config.refreshData,e);else{if("jsonp"!==n)throw"Unknown refresh transport "+n;this._jsonp(this._config.refreshEndpoint,this._config.refreshParams,this._config.refreshHeaders,this._config.refreshData,e)}}},n.prototype._connectResponse=function(t){var e=this;if(!this.isConnected()){if(null!==this._latencyStart&&(this._latency=(new Date).getTime()-this._latencyStart.getTime(),this._latencyStart=null),t.expires&&t.expired)return this._reconnecting=!0,this._disconnect("expired",!0),void this._refresh();if(this._clientID=t.client,this._setStatus("connected"),this._refreshTimeout&&clearTimeout(this._refreshTimeout),t.expires&&(this._refreshTimeout=setTimeout(function(){e._refresh()},1e3*t.ttl)),this._config.resubscribe){this.startBatching(),this.startAuthBatching();for(var n in this._subs)if(this._subs.hasOwnProperty(n)){var s=this._getSub(n);s.shouldResubscribe()&&this.subscribeSub(s)}this.stopAuthBatching(),this.stopBatching(!0)}this._restartPing(),this.trigger("connect",[{client:t.client,transport:this._transportName,latency:this._latency}])}},n.prototype._subscribeResponse=function(t){var e=t.channel,n=this._getSub(e);if(n&&n.isSubscribing()){var s=t.messages;if(s&&s.length>0){s=s.reverse();for(var i in s)s.hasOwnProperty(i)&&this._messageResponse({body:s[i]})}else"last"in t&&(this._lastMessageID[e]=t.last);var r=!1;"recovered"in t&&(r=t.recovered),n.setSubscribeSuccess(r)}},n.prototype._subscribeError=function(t){var e=t.channel,n=this._getSub(e);n&&n.isSubscribing()&&(this.trigger("error",[{error:t}]),n.setSubscribeError(t))},n.prototype._unsubscribeResponse=function(t){var e=this._getSub(t.channel);e&&e.setUnsubscribed()},n.prototype._joinResponse=function(t){var e=this._getSub(t.channel);e&&e.trigger("join",[t])},n.prototype._leaveResponse=function(t){var e=this._getSub(t.channel);e&&e.trigger("leave",[t])},n.prototype._refreshResponse=function(t){var e=this;if(this._refreshTimeout&&clearTimeout(this._refreshTimeout),t.expires){if(t.expired)return void(this._refreshTimeout=setTimeout(function(){e._refresh()},this._config.refreshInterval+Math.round(1e3*Math.random())));this._clientID=t.client,this._refreshTimeout=setTimeout(function(){e._refresh()},1e3*t.ttl)}},n.prototype._messageResponse=function(t){var e=t.body,n=e.channel;this._lastMessageID[n]=e.uid;var s=this._getSub(n);s&&s.trigger("message",[e])},n.prototype._handleResponse=function(t){var e=t.uid;if(e in this._callbacks){var i=this._callbacks[e];if(delete this._callbacks[e],Object(s.b)(t)){var r=i.errback;if(!r)return;r(n.createErrorObject(t.error,t.advice)),this.trigger("error",[{message:t}])}else{var o=i.callback;if(!o)return;o(t.body)}}},n.prototype._dispatchMessage=function(t){if(void 0===t||null===t)return void this._debug("dispatch: got undefined or null message");switch(t.method){case"join":this._joinResponse(t);break;case"leave":this._leaveResponse(t);break;case"message":this._messageResponse(t);break;default:this._handleResponse(t)}},n.prototype._receive=function(t){if(Object.prototype.toString.call(t)===Object.prototype.toString.call([]))for(var e in t)t.hasOwnProperty(e)&&this._dispatchMessage(t[e]);else Object.prototype.toString.call(t)===Object.prototype.toString.call({})&&this._dispatchMessage(t)},n.prototype._setTransport=function(){var t=this;if(Object(s.c)(this._config.sockJS)){var e={transports:this._config.transports||["websocket","xdr-streaming","xhr-streaming","eventsource","iframe-eventsource","iframe-htmlfile","xdr-polling","xhr-polling","iframe-xhr-polling","jsonp-polling"]};this._config.server&&(e.server=this._config.server),this._transport=new this._config.sockJS(this._getSockjsEndpoint(),null,e),this._isSockJS=!0}else{if(!Object(s.c)(WebSocket))throw"No Websocket support and no SockJS configured, can not connect";this._transport=new WebSocket(this._getWebsocketEndpoint())}this._transport.onopen=function(){t._transportClosed=!1,t._reconnecting=!1,t._isSockJS?(t._transportName=t._transport.transport,t._transport.onheartbeat=function(){this._restartPing()}):t._transportName="raw-websocket",t._resetRetry(),Object(s.d)(t._config.user)||n.log("user expected to be string"),Object(s.d)(t._config.info)||n.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),t._latencyStart=new Date,t.addMessage(e).then(function(e){t._connectResponse(e)},function(t){})},this._transport.onerror=function(e){t._debug("transport level error",e)},this._transport.onclose=function(e){t._transportClosed=!0;var n="connection closed",s=!0;if(e&&"reason"in e&&e.reason)try{var i=JSON.parse(e.reason);t._debug("reason is an advice object",i),n=i.reason,s=i.reconnect}catch(i){n=e.reason,t._debug("reason is a plain string",n),s="disconnect"!==n}if(t._config.onTransportClose&&t._config.onTransportClose({event:e,reason:n,reconnect:s}),t._disconnect(n,s),!0===t._reconnect){t._reconnecting=!0;var r=t._getRetryInterval();t._debug("reconnect after "+r+" milliseconds"),setTimeout(function(){!0===t._reconnect&&t.connect()},r)}},this._transport.onmessage=function(e){var n=e.data.split("\n");for(var s in n)if(n.hasOwnProperty(s)&&n[s]){var i=JSON.parse(n[s]);t._debug("Received",i),t._receive(i)}t._restartPing()}},n.jsonpCallbacks={},n.nextJSONPCallbackID=1,n}(i.a)}.call(e,n(3))},function(t,e,n){"use strict";function s(t){return void 0===t}function i(t){return"function"==typeof t}function r(t){return"string"==typeof t}function o(t){for(var e=[],n=1;n{ + method: 'ping' + }, false); + } + + public startBatching(): void { + // start collecting messages without sending them to Centrifuge until flush + // method called + this._isBatching = true; + } + + public stopBatching(flush?: boolean): void { + // stop collecting messages + flush = flush || false; + this._isBatching = false; + if (flush === true) { + this.flush(); + } + } + + public flush(): void { + // send batched messages to Centrifuge + const messages = this._messages.slice(0); + this._messages = []; + this._send(messages); + } + + public startAuthBatching(): void { + // start collecting private channels to create bulk authentication + // request to authEndpoint when stopAuthBatching will be called + this._isAuthBatching = true; + } + + public stopAuthBatching(): void { + let i: string; + let channel: string; + + // create request to authEndpoint with collected private channels + // to ask if this client can subscribe on each channel + this._isAuthBatching = false; + const authChannels = this._authChannels; + this._authChannels = {}; + let channels = []; + + for (channel in authChannels) { + if (authChannels.hasOwnProperty(channel)) { + if (!this._getSub(channel)) { + continue; + } + channels.push(channel); + } + } + + if (channels.length === 0) { + return; + } + + const cb = (error: boolean, data: any) => { + if (error === true) { + this._debug('authorization request failed'); + for (i in channels) { + if (channels.hasOwnProperty(i)) { + channel = channels[i]; + this._subscribeError({ + error: 'authorization request failed', + advice: 'fix', + body: { + channel, + } + }); + } + } + return; + } + + // try to send all subscriptions in one request. + let batch = false; + if (!this._isBatching) { + this.startBatching(); + batch = true; + } + + for (i in channels) { + if (channels.hasOwnProperty(i)) { + channel = channels[i]; + const channelResponse = data[channel]; + if (!channelResponse) { + // subscription:error + this._subscribeError({ + error: 'channel not found in authorization response', + advice: 'fix', + body: { + channel, + } + }); + continue; + } + if (!channelResponse.status || channelResponse.status === 200) { + const msg = { + method: 'subscribe', + params: { + channel: channel, + client: this.getClientId(), + info: channelResponse.info, + sign: channelResponse.sign + } + }; + if (this._recover(channel) === true) { + msg.params.recover = true; + msg.params.last = this._getLastID(channel); + } + this.addMessage(msg).then((response: ICentrifugeSubscribeResponse) => { + this._subscribeResponse(response); + }, (error: ICentrifugeError) => { + }); + } else { + this._subscribeError({ + error: channelResponse.status, + body: { + channel: channel + } + }); + } + } + } + + if (batch) { + this.stopBatching(true); + } + + }; + + const data = { + client: this.getClientId(), + channels, + }; + + if (isFunction(this._config.onPrivateChannelAuth)) { + this._config.onPrivateChannelAuth({ + data, + }, cb); + } else { + const transport = this._config.authTransport.toLowerCase(); + if (transport === 'ajax') { + this._ajax(this._config.authEndpoint, this._config.authParams, this._config.authHeaders, data, cb); + } else if (transport === 'jsonp') { + this._jsonp(this._config.authEndpoint, this._config.authParams, this._config.authHeaders, data, cb); + } else { + throw 'Unknown private channel auth transport ' + transport; + } + } + } + + public subscribe(channel: string, events?: any): Subscription { + if (!isString(channel)) { + 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'; + } + + const currentSub = this._getSub(channel); + + if (currentSub !== null) { + currentSub.setEvents(events); + if (currentSub.isUnsubscribed()) { + currentSub.subscribe(); + } + return currentSub; + } else { + const sub = new Subscription(this, channel, events); + this._subs[channel] = sub; + sub.subscribe(); + return sub; + } + } + + public subscribeSub(sub: Subscription): void { + const 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(); + + // 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.subscribeSub(sub); + this.stopAuthBatching(); + } + } else { + const msg = { + method: 'subscribe', + params: { + channel, + } + }; + if (this._recover(channel) === true) { + msg.params.recover = true; + msg.params.last = this._getLastID(channel); + } + this.addMessage(msg).then((response: ICentrifugeSubscribeResponse) => { + this._subscribeResponse(response); + }, (error: ICentrifugeError) => { + this._subscribeError(error); + }); + } + } + + public unsubscribeSub(sub: Subscription): void { + if (this.isConnected()) { + // No need to unsubscribe in disconnected state - i.e. client already unsubscribed. + this.addMessage({ + method: 'unsubscribe', + params: { + channel: sub.channel + } + }).then((response: ICentrifugeUnsubscribeResponse) => { + this._unsubscribeResponse(response); + }, (error: ICentrifugeError) => { + }); + } + } + + public getClientId(): string { + return this._clientID; + } + + public registerCall(uid: string, callback?: Function, errback?: Function) { + this._callbacks[uid] = { + callback: callback, + errback: errback + }; + setTimeout(() => { + delete this._callbacks[uid]; + if (isFunction(errback)) { + errback(Centrifuge.createErrorObject('timeout', 'retry')); + } + }, this._config.timeout); + } + + public addMessage(message: ICentrifugeMessage, registerCall?: boolean): Promise { + return new Promise((resolve: Function, reject: Function) => { + const uid = this._getNextMessageId() + ''; + message.uid = uid; + if (this._isBatching === true) { + this._messages.push(message); + } else { + this._send([message]); + } + if (registerCall !== false) { + this.registerCall(uid, resolve, reject); + } + }); + } + + public static createErrorObject(error: string, advice?: string): ICentrifugeError { + const result: ICentrifugeError = { + error, + }; + if (advice) { + result.advice = advice; + } + return result; + } + + public static log(...args: any[]): void { + log('info', args); + } + + private _debug(...args: any[]): void { + if (this._config.debug === true) { + log('debug', args); + } + } + + private _jsonp(url: string, params: any, headers: any, data: any, callback: Function) { + if (Object.keys(headers).length > 0) { + Centrifuge.log('Only AJAX request allows to send custom headers, it is not possible with JSONP.'); + } + this._debug('sending JSONP request to', url); + + const callbackName = 'centrifuge_jsonp_' + Centrifuge.nextJSONPCallbackID.toString(); + Centrifuge.nextJSONPCallbackID++; + + const script = document.createElement('script'); + + const timeoutTrigger = setTimeout(() => { + Centrifuge.jsonpCallbacks[callbackName] = () => { + }; + callback(true, 'timeout'); + }, 3000); + + Centrifuge.jsonpCallbacks[callbackName] = (callbackData: any) => { + clearTimeout(timeoutTrigger); + callback(false, callbackData); + delete Centrifuge.jsonpCallbacks[callbackName]; + }; + + const callback_name = 'Centrifuge._jsonpCallbacks[\'' + callbackName + '\']'; + script.src = this._config.authEndpoint + + '?callback=' + encodeURIComponent(callback_name) + + '&data=' + encodeURIComponent(JSON.stringify(data)) + + '&' + objectToQuery(params); + + const head = document.getElementsByTagName('head')[0] || document.documentElement; + head.insertBefore(script, head.firstChild); + } + + private _ajax(url: string, params: any, headers: any, data: any, callback: Function) { + this._debug('sending AJAX request to', url); + + const xhr = (XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')); + + let query = objectToQuery(params); + if (query.length > 0) { + query = '?' + query; + } + + xhr.open('POST', url + query, true); + if ('withCredentials' in xhr) { + xhr.withCredentials = true; + } + + // add request headers + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('Content-Type', 'application/json'); + for (let headerName in headers) { + if (headers.hasOwnProperty(headerName)) { + xhr.setRequestHeader(headerName, headers[headerName]); + } + } + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let data, + parsed = false; + try { + data = JSON.parse(xhr.responseText); + parsed = true; + } catch (e) { + callback(true, 'JSON returned was invalid, yet status code was 200. Data was: ' + xhr.responseText); + } + if (parsed) { // prevents double execution. + callback(false, data); + } + } else { + Centrifuge.log('Could not get auth info from application', xhr.status); + callback(true, xhr.status); + } + } + }; + + setTimeout(() => { + xhr.send(JSON.stringify(data)); + }, 20); + return xhr; + } + + private _configure(config: ICentrifugeConfig): void { + this._debug('Configuring centrifuge object with', config); + config = Object.assign({ + retry: 1000, + maxRetry: 20000, + timeout: 5000, + info: '', + resubscribe: true, + ping: true, + pingInterval: 30000, + pongWaitTimeout: 5000, + debug: false, + insecure: false, + privateChannelPrefix: '$', + refreshEndpoint: '/centrifuge/refresh/', + refreshHeaders: {}, + refreshParams: {}, + refreshData: {}, + refreshTransport: 'ajax', + refreshAttempts: 0, + refreshInterval: 3000, + authEndpoint: '/centrifuge/auth/', + authHeaders: {}, + authParams: {}, + authTransport: 'ajax', + }, config); + + if (!config.url) { + throw 'Missing required configuration parameter \'url\' specifying server URL'; + } + config.url = stripSlash(config.url); + + if (endsWith(config.url, 'connection/websocket')) { + this._debug('client will connect to raw Websocket endpoint'); + } else { + if (endsWith(config.url, 'connection')) { + this._debug('client will connect to SockJS endpoint'); + } else { + this._debug('client will detect connection endpoint itself'); + } + if (config.sockJS !== null) { + this._debug('SockJS explicitly provided in options'); + } else { + if (isUndefined(global['SockJS'])) { + throw 'Include SockJS client library before Centrifuge javascript client library or provide SockJS object in options or use raw Websocket connection endpoint'; + } else { + this._debug('Use globally defined SockJS'); + config.sockJS = global['SockJS']; + } + } + } + + if (!config.user) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'user\' specifying user\'s unique ID in your application'; + } else { + this._debug('user not found but this is OK for insecure mode - anonymous access will be used'); + } + } else { + if (!isString(config.user)) { + Centrifuge.log('Configuration parameter \'user\' expected to be string'); + } + } + + if (!config.timestamp) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'timestamp\''; + } else { + this._debug('Configuration parameter \'v\' not found but this is OK for insecure mode'); + } + } else { + if (!isString(config.timestamp)) { + Centrifuge.log('Configuration parameter \'timestamp\' expected to be string'); + } + } + + if (!config.token) { + if (!config.insecure) { + throw 'Missing required configuration parameter \'token\' specifying the sign of authorization request'; + } else { + this._debug('Configuration parameter \'token\' not found but this is OK for insecure mode'); + } + } else { + if (!isString(config.token)) { + Centrifuge.log('Configuration parameter \'token\' expected to be string'); + } + } + + if (config.info && !isString(config.info)) { + Centrifuge.log('Configuration parameter \'info\' expected to be string'); + } + + this._config = config; + } + + private _getSockjsEndpoint(): string { + let url = this._config.url; + url = url + .replace('ws://', 'http://') + .replace('wss://', 'https://'); + url = stripSlash(url); + if (!endsWith(this._config.url, 'connection')) { + url = url + '/connection'; + } + return url; + } + + private _getWebsocketEndpoint(): string { + let url = this._config.url; + url = url + .replace('http://', 'ws://') + .replace('https://', 'wss://'); + url = stripSlash(url); + if (!endsWith(this._config.url, 'connection/websocket')) { + url = url + '/connection/websocket'; + } + return url; + } + + private _recover(channel: string): boolean { + return channel in this._lastMessageID; + } + + private _getLastID(channel: string): string { + const lastUID = this._lastMessageID[channel]; + if (lastUID) { + this._debug('Last uid found and sent for channel', channel); + return lastUID; + } else { + this._debug('No last uid found for channel', channel); + return ''; + } + } + + private _getSub(channel: string): Subscription { + return this._subs[channel] || null; + } + + private _clearConnectedState(reconnect: boolean): void { + this._clientID = null; + + // fire errbacks of registered calls. + for (let uid in this._callbacks) { + if (this._callbacks.hasOwnProperty(uid)) { + const callbacks = this._callbacks[uid]; + const errback = callbacks.errback; + if (!errback) { + continue; + } + errback(Centrifuge.createErrorObject('disconnected', 'retry')); + } + } + this._callbacks = {}; + + // fire unsubscribe events + for (let channel in this._subs) { + if (this._subs.hasOwnProperty(channel)) { + const sub = this._getSub(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 = {}; + } + } + + private _setStatus(newStatus: string): void { + if (this._status !== newStatus) { + this._debug('Status', this._status, '->', newStatus); + this._status = newStatus; + } + } + + private _disconnect(reason: string, shouldReconnect?: boolean): void { + if (this.isDisconnected()) { + return; + } + this._debug('disconnected:', reason, shouldReconnect); + + const reconnect = shouldReconnect || false; + if (reconnect === false) { + this._reconnect = false; + } + + this._clearConnectedState(reconnect); + + if (!this.isDisconnected()) { + this._setStatus('disconnected'); + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (this._reconnecting === false) { + this.trigger('disconnect', [{ + reason, + reconnect, + }]); + } + } + + if (!this._transportClosed) { + this._transport.close(); + } + } + + private _send(messages: ICentrifugeMessage[]): void { + if (!messages.length) { + return; + } + this._debug('Send', messages); + let encodedMessages = []; + for (const i in messages) { + encodedMessages.push(JSON.stringify(messages[i])) + } + this._transport.send(encodedMessages.join("\n")); + } + + private _getNextMessageId(): number { + return ++this._messageId; + } + + private _stopPing(): void { + if (this._pongTimeout !== null) { + clearTimeout(this._pongTimeout); + } + if (this._pingInterval !== null) { + clearInterval(this._pingInterval); + } + } + + private _startPing(): void { + if (this._config.ping !== true || this._config.pingInterval <= 0 || !this.isConnected()) { + return; + } + this._pingInterval = setInterval(() => { + if (!this.isConnected()) { + this._stopPing(); + return; + } + this.ping(); + this._pongTimeout = setTimeout(function () { + this._disconnect('no ping', true); + }, this._config.pongWaitTimeout); + }, this._config.pingInterval); + } + + private _restartPing(): void { + this._stopPing(); + this._startPing(); + } + + private _resetRetry(): void { + this._debug('reset retries count to 0'); + this._retries = 0; + } + + private _getRetryInterval(): number { + this._retries += 1; + const jitter = 0.5 * Math.random(); + let interval = this._config.retry * Math.pow(2, this._retries + 1); + if (interval > this._config.maxRetry) { + interval = this._config.maxRetry; + } + return Math.floor((1 - jitter) * interval); + } + + private _refreshFailed(): void { + this._numRefreshFailed = 0; + if (!this.isDisconnected()) { + this._disconnect('refresh failed'); + } + if (this._config.refreshFailed) { + this._config.refreshFailed(); + } + } + + private _refresh(): void { + // ask web app for connection parameters - user ID, + // timestamp, info and token + this._debug('refresh credentials'); + + if (this._config.refreshAttempts === 0) { + this._debug('refresh attempts set to 0, do not send refresh request at all'); + this._refreshFailed(); + return; + } + + if (this._refreshTimeout !== null) { + clearTimeout(this._refreshTimeout); + } + + const cb = (error: boolean, data: ICentrifugeCredentials) => { + if (error === true) { + // We don't perform any connection status related actions here as we are + // relying on Centrifugo that must close connection eventually. + this._debug('error getting connection credentials from refresh endpoint', data); + this._numRefreshFailed++; + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (this._config.refreshAttempts !== null && this._numRefreshFailed >= this._config.refreshAttempts) { + this._refreshFailed(); + return; + } + this._refreshTimeout = setTimeout(() => { + this._refresh(); + }, this._config.refreshInterval + Math.round(Math.random() * 1000)); + return; + } + this._numRefreshFailed = 0; + this._config.user = data.user; + this._config.timestamp = data.timestamp; + this._config.token = data.token; + if ('info' in data) { + this._config.info = data.info; + } else { + data.info = ''; + } + if (this.isDisconnected()) { + this._debug('credentials refreshed, connect from scratch'); + this.connect(); + } else { + this._debug('send refreshed credentials'); + this.addMessage({ + method: 'refresh', + params: data, + }).then((response: ICentrifugeRefreshResponse) => { + this._refreshResponse(response); + }, (error: ICentrifugeError) => { + }); + } + }; + + if (this._config.onRefresh !== null) { + this._config.onRefresh({}, cb); + } else { + const transport = this._config.refreshTransport.toLowerCase(); + if (transport === 'ajax') { + this._ajax(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, this._config.refreshData, cb); + } else if (transport === 'jsonp') { + this._jsonp(this._config.refreshEndpoint, this._config.refreshParams, this._config.refreshHeaders, this._config.refreshData, cb); + } else { + throw 'Unknown refresh transport ' + transport; + } + } + } + + private _connectResponse(response: ICentrifugeConnectResponse): void { + if (this.isConnected()) { + return; + } + if (this._latencyStart !== null) { + this._latency = (new Date()).getTime() - this._latencyStart.getTime(); + this._latencyStart = null; + } + if (response.expires) { + if (response.expired) { + this._reconnecting = true; + this._disconnect('expired', true); + this._refresh(); + return; + } + } + this._clientID = response.client; + this._setStatus('connected'); + + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + + if (response.expires) { + this._refreshTimeout = setTimeout(() => { + this._refresh(); + }, response.ttl * 1000); + } + + if (this._config.resubscribe) { + this.startBatching(); + this.startAuthBatching(); + for (let channel in this._subs) { + if (this._subs.hasOwnProperty(channel)) { + const sub = this._getSub(channel); + if (sub.shouldResubscribe()) { + this.subscribeSub(sub); + } + } + } + this.stopAuthBatching(); + this.stopBatching(true); + } + + this._restartPing(); + this.trigger('connect', [{ + client: response.client, + transport: this._transportName, + latency: this._latency + }]); + } + + private _subscribeResponse(response: ICentrifugeSubscribeResponse): void { + const channel = response.channel; + const sub = this._getSub(channel); + if (!sub || !sub.isSubscribing()) { + return; + } + let messages = response.messages; + if (messages && messages.length > 0) { + // handle missed messages + messages = messages.reverse(); + for (let i in messages) { + if (messages.hasOwnProperty(i)) { + this._messageResponse({ + body: messages[i] + }); + } + } + } else { + if ('last' in response) { + // no missed messages found so set last message id from body. + this._lastMessageID[channel] = response.last; + } + } + let recovered = false; + if ('recovered' in response) { + recovered = response.recovered; + } + sub.setSubscribeSuccess(recovered); + } + + private _subscribeError(error: ICentrifugeError): void { + const channel = error.channel; + const sub = this._getSub(channel); + if (!sub || !sub.isSubscribing()) { + return; + } + this.trigger('error', [{ + error, + }]); + sub.setSubscribeError(error); + } + + private _unsubscribeResponse(response: ICentrifugeUnsubscribeResponse): void { + const sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.setUnsubscribed(); + } + + private _joinResponse(response: ICentrifugeJoinResponse): void { + const sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.trigger('join', [response]); + } + + private _leaveResponse(response: ICentrifugeLeaveResponse): void { + const sub = this._getSub(response.channel); + if (!sub) { + return; + } + sub.trigger('leave', [response]); + } + + private _refreshResponse(response: ICentrifugeRefreshResponse): void { + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + if (response.expires) { + if (response.expired) { + this._refreshTimeout = setTimeout(() => { + this._refresh(); + }, this._config.refreshInterval + Math.round(Math.random() * 1000)); + return; + } + this._clientID = response.client; + this._refreshTimeout = setTimeout(() => { + this._refresh(); + }, response.ttl * 1000); + } + } + + private _messageResponse(message: ICentrifugeMessage): void { + const body = message.body; + const channel = body.channel; + + // keep last uid received from channel. + this._lastMessageID[channel] = body.uid; + + const sub = this._getSub(channel); + if (!sub) { + return; + } + sub.trigger('message', [body]); + } + + private _handleResponse(message: ICentrifugeMessage): void { + const uid = message.uid; + if (!(uid in this._callbacks)) { + return; + } + const callbacks = this._callbacks[uid]; + delete this._callbacks[uid]; + if (!errorExists(message)) { + const callback = callbacks.callback; + if (!callback) { + return; + } + callback(message.body); + } else { + const errback = callbacks.errback; + if (!errback) { + return; + } + errback(Centrifuge.createErrorObject(message.error, message.advice)); + this.trigger('error', [{ + message, + }]); + } + } + + private _dispatchMessage(message: any): void { + if (message === undefined || message === null) { + this._debug('dispatch: got undefined or null message'); + return; + } + switch (message.method) { + case 'join': + this._joinResponse(message); + break; + case 'leave': + this._leaveResponse(message); + break; + case 'message': + this._messageResponse(message); + break; + default: + this._handleResponse(message); + } + } + + private _receive(data: any): void { + if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) { + // array of responses received + for (let i in data) { + if (data.hasOwnProperty(i)) { + this._dispatchMessage(data[i]); + } + } + } else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) { + // one response received + this._dispatchMessage(data); + } + } + + private _setTransport(): void { + if (isFunction(this._config.sockJS)) { + const sockjsOptions: ISockJSOptions = { + transports: this._config.transports || [ + 'websocket', + 'xdr-streaming', + 'xhr-streaming', + 'eventsource', + 'iframe-eventsource', + 'iframe-htmlfile', + 'xdr-polling', + 'xhr-polling', + 'iframe-xhr-polling', + 'jsonp-polling' + ] + }; + if (this._config.server) { + sockjsOptions.server = this._config.server; + } + this._transport = new this._config.sockJS(this._getSockjsEndpoint(), null, sockjsOptions); + this._isSockJS = true; + } else { + if (!isFunction(WebSocket)) { + throw 'No Websocket support and no SockJS configured, can not connect'; + } + this._transport = new WebSocket(this._getWebsocketEndpoint()); + } + + this._transport.onopen = () => { + this._transportClosed = false; + this._reconnecting = false; + + if (this._isSockJS) { + this._transportName = this._transport.transport; + this._transport.onheartbeat = function () { + this._restartPing(); + }; + } else { + this._transportName = 'raw-websocket'; + } + + this._resetRetry(); + + if (!isString(this._config.user)) { + Centrifuge.log('user expected to be string'); + } + if (!isString(this._config.info)) { + Centrifuge.log('info expected to be string'); + } + + const msg: ICentrifugeConnectMessage = { + method: 'connect', + params: { + user: this._config.user, + info: this._config.info, + } + }; + + if (!this._config.insecure) { + // in insecure client mode we don't need timestamp and token. + msg.params.timestamp = this._config.timestamp; + msg.params.token = this._config.token; + } + this._latencyStart = new Date(); + this.addMessage(msg).then((response: ICentrifugeConnectResponse) => { + this._connectResponse(response); + }, (error: ICentrifugeError) => { + }); + }; + + this._transport.onerror = (error: any) => { + this._debug('transport level error', error); + }; + + this._transport.onclose = (event: any) => { + this._transportClosed = true; + let reason = 'connection closed'; + let reconnect = true; + if (event && 'reason' in event && event.reason) { + try { + const advice = JSON.parse(event.reason); + this._debug('reason is an advice object', advice); + reason = advice.reason; + reconnect = advice.reconnect; + } catch (e) { + reason = event.reason; + this._debug('reason is a plain string', reason); + reconnect = reason !== 'disconnect'; + } + } + + // onTransportClose callback should be executed every time transport was closed. + // This can be helpful to catch failed connection events (because our disconnect + // event only called once and every future attempts to connect do not fire disconnect + // event again). + if (this._config.onTransportClose) { + this._config.onTransportClose({ + event, + reason, + reconnect, + }); + } + + this._disconnect(reason, reconnect); + + if (this._reconnect === true) { + this._reconnecting = true; + const interval = this._getRetryInterval(); + this._debug('reconnect after ' + interval + ' milliseconds'); + setTimeout(() => { + if (this._reconnect === true) { + this.connect(); + } + }, interval); + } + }; + + this._transport.onmessage = (event: any) => { + const replies = event.data.split("\n"); + for (const i in replies) { + if (replies.hasOwnProperty(i) && replies[i]) { + const data = JSON.parse(replies[i]); + this._debug('Received', data); + this._receive(data); + } + } + this._restartPing(); + }; + } + +} diff --git a/src/Functions.ts b/src/Functions.ts new file mode 100644 index 0000000..2409cfa --- /dev/null +++ b/src/Functions.ts @@ -0,0 +1,49 @@ +export function isUndefined(value: any): boolean { + return typeof value === 'undefined'; +} + +export function isFunction(value: any): boolean { + return typeof value === 'function'; +} + +export function isString(value: any): boolean { + return typeof value === 'string'; +} + +export function log(level: string, ...args: any[]) { + if (console) { + if (args.length === 1) { + args = args[0]; + } + const logger = console[level]; + if (isFunction(logger)) { + logger.apply(logger, args); + } + } +} + +export function stripSlash(value: string): string { + return value.replace(/\/$/, ''); +} + +export function startsWith(value: string, prefix: string): boolean { + return value.lastIndexOf(prefix, 0) === 0; +} + +export function endsWith(value: string, suffix: string): boolean { + return value.indexOf(suffix, value.length - suffix.length) !== -1; +} + +export function errorExists(data: any): boolean { + return 'error' in data && data.error !== null && data.error !== ''; +} + +export function objectToQuery(object: any): string { + let p = []; + for (let i in object) { + if (object.hasOwnProperty(i)) { + p.push(encodeURIComponent(i) + (object[i] ? '=' + encodeURIComponent(object[i]) : '')); + } + } + return p.join('&'); +} diff --git a/src/Observable.ts b/src/Observable.ts new file mode 100644 index 0000000..1d84321 --- /dev/null +++ b/src/Observable.ts @@ -0,0 +1,53 @@ +export class Observable { + + private __callbacks = {}; + + public on(events: string, fn: Function): this { + events.replace(/[^\s]+/g, (name: string): string => { + (this.__callbacks[name] = this.__callbacks[name] || []).push(fn); + return ''; + }); + return this; + } + + public one(name: string, fn: Function): this { + fn['one'] = true; + return this.on(name, fn); + } + + public off(events: string, fn?: Function): this { + if (events === '*') { + this.__callbacks = {}; + } else if (fn) { + let arr = this.__callbacks[events]; + for (let i = 0, cb; (cb = arr && arr[i]); ++i) { + if (cb === fn) { + arr.splice(i, 1); + } + } + } else { + events.replace(/[^\s]+/g, (name: string): string => { + this.__callbacks[name] = []; + return ''; + }); + } + return this; + } + + public trigger(name: string, args: any[]): this { + let fns = this.__callbacks[name] || []; + for (let i = 0, fn; (fn = fns[i]); ++i) { + if (!fn['busy']) { + fn['busy'] = true; + fn.apply(this, args); + if (fn['one']) { + fns.splice(i, 1); + i--; + } + fn['busy'] = false; + } + } + return this; + } + +} diff --git a/src/Subscription.ts b/src/Subscription.ts new file mode 100644 index 0000000..908f6b9 --- /dev/null +++ b/src/Subscription.ts @@ -0,0 +1,223 @@ +import { + isFunction, +} from './Functions'; +import {Observable} from './Observable'; +import {Centrifuge} from './Centrifuge'; +import { + ICentrifugeMessage, + ICentrifugeError, + ISubscriptionMessage, + ISubscriptionError, + ISubscriptionSuccess, +} from './interfaces'; + +export class Subscription extends Observable { + + static STATE_NEW: number = 0; + static STATE_SUBSCRIBING: number = 1; + static STATE_SUCCESS: number = 2; + static STATE_ERROR: number = 3; + static STATE_UNSUBSCRIBED: number = 4; + + public channel: string = null; + + private _status: number = Subscription.STATE_NEW; + private _error: ISubscriptionError = null; + private _centrifuge: Centrifuge = null; + private _isResubscribe: boolean = false; + private _recovered: boolean = false; + private _ready: boolean = false; + private _promise: Promise = null; + private _noResubscribe: boolean = false; + private _resolve: Function; + private _reject: Function; + + constructor(centrifuge: Centrifuge, channel: string, events: any) { + super(); + this._centrifuge = centrifuge; + this.channel = channel; + this.setEvents(events); + this._initializePromise(); + } + + public setEvents(events: any): void { + if (!events) { + return; + } + if (isFunction(events)) { + this.on('message', events); + } else if (Object.prototype.toString.call(events) === Object.prototype.toString.call({})) { + const knownEvents = ['message', 'join', 'leave', 'unsubscribe', 'subscribe', 'error']; + for (let i = 0, l = knownEvents.length; i < l; i++) { + const ev = knownEvents[i]; + if (ev in events) { + this.on(ev, events[ev]); + } + } + } + } + + public setNew(): void { + this._status = Subscription.STATE_NEW; + } + + public setSubscribing(): void { + if (this._ready === true) { + // new promise for this subscription + this._initializePromise(); + this._isResubscribe = true; + } + this._status = Subscription.STATE_SUBSCRIBING; + } + + public setUnsubscribed(noResubscribe?: boolean): void { + if (this._status === Subscription.STATE_UNSUBSCRIBED) { + return; + } + this._status = Subscription.STATE_UNSUBSCRIBED; + if (noResubscribe === true) { + this._noResubscribe = true; + } + this.triggerUnsubscribe(); + } + + public setSubscribeSuccess(recovered: boolean): void { + if (this._status === Subscription.STATE_SUCCESS) { + return; + } + this._recovered = recovered; + this._status = Subscription.STATE_SUCCESS; + const successContext = this._getSubscribeSuccess(); + this.trigger('subscribe', [successContext]); + this._resolve(successContext); + } + + public setSubscribeError(err: any): void { + if (this._status === Subscription.STATE_ERROR) { + return; + } + this._status = Subscription.STATE_ERROR; + this._error = err; + const errContext = this._getSubscribeError(); + this.trigger('error', [errContext]); + this._reject(errContext); + } + + public triggerUnsubscribe(): void { + this.trigger('unsubscribe', [{ + channel: this.channel + }]); + } + + public isUnsubscribed(): boolean { + return this._status === Subscription.STATE_UNSUBSCRIBED; + } + + public isSuccess(): boolean { + return this._status === Subscription.STATE_SUCCESS; + } + + public isSubscribing(): boolean { + return this._status === Subscription.STATE_SUBSCRIBING; + } + + public shouldResubscribe(): boolean { + return !this._noResubscribe; + } + + public ready(callback: Function, errback: Function) { + if (this._ready) { + if (this.isSuccess()) { + callback(this._getSubscribeSuccess()); + } else { + errback(this._getSubscribeError()); + } + } + } + + public subscribe(): this { + if (this._status === Subscription.STATE_SUCCESS) { + return; + } + this._centrifuge.subscribeSub(this); + return this; + } + + public unsubscribe(): void { + this.setUnsubscribed(true); + this._centrifuge.unsubscribeSub(this); + } + + public publish(data: any): Promise { + return this._request('publish', data); + } + + public presence(): Promise { + return this._request('presence'); + } + + public history(): Promise { + return this._request('history'); + } + + private _initializePromise(): void { + this._ready = false; + this._promise = new Promise((resolve: Function, reject: Function) => { + this._resolve = (value: any) => { + this._ready = true; + resolve(value); + }; + this._reject = (err: any) => { + this._ready = true; + reject(err); + }; + }); + } + + private _getSubscribeSuccess(): ISubscriptionSuccess { + return { + channel: this.channel, + isResubscribe: this._isResubscribe, + recovered: this._recovered + }; + } + + private _getSubscribeError(): ISubscriptionError { + const subscribeError = this._error; + subscribeError.channel = this.channel; + subscribeError.isResubscribe = this._isResubscribe; + return subscribeError; + } + + private _request(method: string, data?: any): Promise { + return new Promise((resolve: Function, reject: Function) => { + if (this.isUnsubscribed()) { + reject(Centrifuge.createErrorObject('subscription unsubscribed', 'fix')); + return; + } + this._promise.then(() => { + if (!this._centrifuge.isConnected()) { + reject(Centrifuge.createErrorObject('disconnected', 'retry')); + return; + } + const params = { + channel: this.channel, + }; + if (data) { + params['data'] = data; + } + this._centrifuge.addMessage({ + method, + params, + }).then((response: ICentrifugeMessage) => { + resolve(response); + }, (error: ICentrifugeError) => { + reject(error); + }); + }, (err: any) => { + reject(err); + }); + }); + } + +} diff --git a/src/interfaces/ICentrifugeConfig.ts b/src/interfaces/ICentrifugeConfig.ts new file mode 100644 index 0000000..d5f8055 --- /dev/null +++ b/src/interfaces/ICentrifugeConfig.ts @@ -0,0 +1,35 @@ +export interface ICentrifugeConfig { + sockJS?: any; + server?: string; + transports?: string[]; + retry?: number; + maxRetry?: number; + timeout?: number; + info?: string; + resubscribe?: boolean; + ping?: boolean; + pingInterval?: number; + pongWaitTimeout?: number; + debug?: boolean; + insecure?: boolean; + privateChannelPrefix?: string; + onTransportClose?: Function; + onRefresh?: Function; + refreshEndpoint?: string; + refreshHeaders?: Object; + refreshParams?: Object; + refreshData?: Object; + refreshTransport?: string; + refreshAttempts?: number; + refreshInterval?: number; + refreshFailed?: Function; + onPrivateChannelAuth?: Function; + authEndpoint?: string; + authHeaders?: Object; + authParams?: Object; + authTransport?: string; + user?: string; + timestamp?: string; + token?: string; + url?: string; +} diff --git a/src/interfaces/ICentrifugeConnectMessage.ts b/src/interfaces/ICentrifugeConnectMessage.ts new file mode 100644 index 0000000..fcb38be --- /dev/null +++ b/src/interfaces/ICentrifugeConnectMessage.ts @@ -0,0 +1,7 @@ +import {ICentrifugeCredentials} from './ICentrifugeCredentials'; + +export interface ICentrifugeConnectMessage { + method?: string; + uid?: string; + params?: ICentrifugeCredentials; +} diff --git a/src/interfaces/ICentrifugeConnectResponse.ts b/src/interfaces/ICentrifugeConnectResponse.ts new file mode 100644 index 0000000..35ce206 --- /dev/null +++ b/src/interfaces/ICentrifugeConnectResponse.ts @@ -0,0 +1,6 @@ +export interface ICentrifugeConnectResponse { + expires: boolean; + expired: boolean; + client: string; + ttl: number; +} diff --git a/src/interfaces/ICentrifugeCredentials.ts b/src/interfaces/ICentrifugeCredentials.ts new file mode 100644 index 0000000..aa27956 --- /dev/null +++ b/src/interfaces/ICentrifugeCredentials.ts @@ -0,0 +1,6 @@ +export interface ICentrifugeCredentials { + user?: string; + timestamp?: string; + token?: string; + info?: string; +} diff --git a/src/interfaces/ICentrifugeError.ts b/src/interfaces/ICentrifugeError.ts new file mode 100644 index 0000000..6960a34 --- /dev/null +++ b/src/interfaces/ICentrifugeError.ts @@ -0,0 +1,7 @@ +export interface ICentrifugeError { + error: string; + advice?: string; + channel?: string; + isResubscribe?: boolean; + body?: any; +} diff --git a/src/interfaces/ICentrifugeJoinResponse.ts b/src/interfaces/ICentrifugeJoinResponse.ts new file mode 100644 index 0000000..eecc94b --- /dev/null +++ b/src/interfaces/ICentrifugeJoinResponse.ts @@ -0,0 +1,3 @@ +export interface ICentrifugeJoinResponse { + channel: string; +} diff --git a/src/interfaces/ICentrifugeLeaveResponse.ts b/src/interfaces/ICentrifugeLeaveResponse.ts new file mode 100644 index 0000000..4bf9950 --- /dev/null +++ b/src/interfaces/ICentrifugeLeaveResponse.ts @@ -0,0 +1,4 @@ +import {ICentrifugeJoinResponse} from './ICentrifugeJoinResponse'; + +export interface ICentrifugeLeaveResponse extends ICentrifugeJoinResponse { +} diff --git a/src/interfaces/ICentrifugeMessage.ts b/src/interfaces/ICentrifugeMessage.ts new file mode 100644 index 0000000..29295dc --- /dev/null +++ b/src/interfaces/ICentrifugeMessage.ts @@ -0,0 +1,9 @@ +export interface ICentrifugeMessage { + uid?: string; + body?: { + channel: string; + uid: string; + }; + error?: string; + advice?: string; +} diff --git a/src/interfaces/ICentrifugePingMessage.ts b/src/interfaces/ICentrifugePingMessage.ts new file mode 100644 index 0000000..a1c2653 --- /dev/null +++ b/src/interfaces/ICentrifugePingMessage.ts @@ -0,0 +1,4 @@ +export interface ICentrifugePingMessage { + method?: string; + uid?: string; +} diff --git a/src/interfaces/ICentrifugeRefreshMessage.ts b/src/interfaces/ICentrifugeRefreshMessage.ts new file mode 100644 index 0000000..3933759 --- /dev/null +++ b/src/interfaces/ICentrifugeRefreshMessage.ts @@ -0,0 +1,4 @@ +import {ICentrifugeConnectMessage} from './ICentrifugeConnectMessage'; + +export interface ICentrifugeRefreshMessage extends ICentrifugeConnectMessage { +} diff --git a/src/interfaces/ICentrifugeRefreshResponse.ts b/src/interfaces/ICentrifugeRefreshResponse.ts new file mode 100644 index 0000000..15f7552 --- /dev/null +++ b/src/interfaces/ICentrifugeRefreshResponse.ts @@ -0,0 +1,4 @@ +import {ICentrifugeConnectResponse} from './ICentrifugeConnectResponse'; + +export interface ICentrifugeRefreshResponse extends ICentrifugeConnectResponse { +} diff --git a/src/interfaces/ICentrifugeSubscribeMessage.ts b/src/interfaces/ICentrifugeSubscribeMessage.ts new file mode 100644 index 0000000..c8d10cd --- /dev/null +++ b/src/interfaces/ICentrifugeSubscribeMessage.ts @@ -0,0 +1,12 @@ +export interface ICentrifugeSubscribeMessage { + method?: string; + uid?: string; + params?: { + channel: string; + client?: string; + info?: string; + sign?: string; + recover?: boolean; + last?: string; + }; +} diff --git a/src/interfaces/ICentrifugeSubscribeResponse.ts b/src/interfaces/ICentrifugeSubscribeResponse.ts new file mode 100644 index 0000000..9494d96 --- /dev/null +++ b/src/interfaces/ICentrifugeSubscribeResponse.ts @@ -0,0 +1,7 @@ +export interface ICentrifugeSubscribeResponse { + channel: string; + last: string; + messages: any; + recovered: boolean; + status: boolean; +} diff --git a/src/interfaces/ICentrifugeUnsubscribeMessage.ts b/src/interfaces/ICentrifugeUnsubscribeMessage.ts new file mode 100644 index 0000000..2fe83cc --- /dev/null +++ b/src/interfaces/ICentrifugeUnsubscribeMessage.ts @@ -0,0 +1,7 @@ +export interface ICentrifugeUnsubscribeMessage { + method: string; + uid?: string; + params?: { + channel: string; + }; +} diff --git a/src/interfaces/ICentrifugeUnsubscribeResponse.ts b/src/interfaces/ICentrifugeUnsubscribeResponse.ts new file mode 100644 index 0000000..9e1f483 --- /dev/null +++ b/src/interfaces/ICentrifugeUnsubscribeResponse.ts @@ -0,0 +1,4 @@ +import {ICentrifugeJoinResponse} from './ICentrifugeJoinResponse'; + +export interface ICentrifugeUnsubscribeResponse extends ICentrifugeJoinResponse { +} diff --git a/src/interfaces/ISockJSOptions.ts b/src/interfaces/ISockJSOptions.ts new file mode 100644 index 0000000..dd40606 --- /dev/null +++ b/src/interfaces/ISockJSOptions.ts @@ -0,0 +1,4 @@ +export interface ISockJSOptions { + server?: string; + transports?: string[]; +} diff --git a/src/interfaces/ISubscriptionError.ts b/src/interfaces/ISubscriptionError.ts new file mode 100644 index 0000000..8687d3c --- /dev/null +++ b/src/interfaces/ISubscriptionError.ts @@ -0,0 +1,6 @@ +export interface ISubscriptionError { + error: string; + advice?: string; + channel?: string; + isResubscribe?: boolean; +} diff --git a/src/interfaces/ISubscriptionMessage.ts b/src/interfaces/ISubscriptionMessage.ts new file mode 100644 index 0000000..f9d96cc --- /dev/null +++ b/src/interfaces/ISubscriptionMessage.ts @@ -0,0 +1,8 @@ +export interface ISubscriptionMessage { + method?: string; + uid?: string; + params?: { + channel: string; + data?: any; + }; +} diff --git a/src/interfaces/ISubscriptionSuccess.ts b/src/interfaces/ISubscriptionSuccess.ts new file mode 100644 index 0000000..3836e95 --- /dev/null +++ b/src/interfaces/ISubscriptionSuccess.ts @@ -0,0 +1,5 @@ +export interface ISubscriptionSuccess { + channel?: string; + isResubscribe?: boolean; + recovered?: boolean; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..d6fd3b7 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,19 @@ +export * from './ISockJSOptions'; +export * from './ICentrifugeConfig'; +export * from './ICentrifugeError'; +export * from './ICentrifugeCredentials'; +export * from './ICentrifugeMessage'; +export * from './ICentrifugeConnectMessage'; +export * from './ICentrifugePingMessage'; +export * from './ICentrifugeSubscribeMessage'; +export * from './ICentrifugeUnsubscribeMessage'; +export * from './ICentrifugeRefreshMessage'; +export * from './ICentrifugeConnectResponse'; +export * from './ICentrifugeRefreshResponse'; +export * from './ICentrifugeSubscribeResponse'; +export * from './ICentrifugeUnsubscribeResponse'; +export * from './ICentrifugeJoinResponse'; +export * from './ICentrifugeLeaveResponse'; +export * from './ISubscriptionError'; +export * from './ISubscriptionSuccess'; +export * from './ISubscriptionMessage'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0809b56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es5", + "module": "es6", + "moduleResolution": "node", + "sourceMap": false, + "declaration": false, + "removeComments": true, + "lib": [ + "es6", + "dom" + ] + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..d707c48 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,52 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: { + 'centrifuge': './src/Centrifuge.ts', + 'centrifuge.min': './src/Centrifuge.ts' + }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + use: 'ts-loader' + } + ] + }, + resolve: { + extensions: ['.ts', '.tsx'] + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + libraryTarget: 'umd' + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + include: /\.min\.js$/, + minimize: true, + beautify: false, + output: { + comments: false + }, + mangle: { + screw_ie8: true + }, + compress: { + screw_ie8: true, + warnings: false, + conditionals: true, + unused: true, + comparisons: true, + sequences: true, + dead_code: true, + evaluate: true, + if_return: true, + join_vars: true, + negate_iife: false + } + }) + ] +};