UNPKG

jii

Version:

Jii - Full-Stack JavaScript Framework

420 lines (354 loc) 10.9 kB
/** * @author Vladimir Kozhin <affka@affka.ru> * @license MIT */ 'use strict'; const Jii = require('../../BaseJii'); const String = require('../../helpers/String'); const TransportInterface = require('./transport/TransportInterface'); const RequestEvent = require('./RequestEvent'); const MessageEvent = require('./MessageEvent'); const ChannelEvent = require('../ChannelEvent'); const _indexOf = require('lodash/indexOf'); const _each = require('lodash/each'); const Component = require('../../base/Component'); const AutoReconnect = require('./plugin/AutoReconnect'); /** * Read-only from api stationUid * @type {null} */ var stationUid = null; class Client extends Component { preInit() { this._subscribes = []; /** * @type {object} */ this._requestsInProcess = {}; /** * @type {boolean} */ this._forceClosed = false; /** * @type {boolean} */ this._isOpened = false; /** * Url to comet server * @type {string} */ this._serverUrl = ''; /** * @type {boolean} */ this.autoSubscribeOnReconnect = true; /** * @type {boolean} */ this.autoOpen = true; /** * Max comet workers number. Used for auto generate different server urls (balancer). */ this.workersCount = null; this.plugins = { /** * @type {AutoReconnect} */ autoReconnect: { className: AutoReconnect } }; /** * @type {TransportInterface} */ this.transport = null; super.preInit(...arguments); } init() { stationUid = String.generateUid(); // Init transport this.transport = Jii.createObject(this.transport); this.transport.on(TransportInterface.EVENT_OPEN, this._onOpen.bind(this)); this.transport.on(TransportInterface.EVENT_CLOSE, this._onClose.bind(this)); this.transport.on(TransportInterface.EVENT_MESSAGE, this._onMessage.bind(this)); // Init plugins _each(this.plugins, (config, name) => { config.comet = this; this.plugins[name] = Jii.createObject(config); }); // Auto open if (this.autoOpen) { this.open(); } } /** * Set url to comet server * Detect server url by pattern, if set. Used for balancer server by clients random(). * @param {string} value */ setServerUrl(value) { // Normalize if (value.indexOf('//') === 0) { var sslSuffix = location.protocol === 'https' ? 's' : ''; value = 'http' + sslSuffix + ':' + value; } // Balancer if (value.indexOf('{workerIndex}') !== -1) { var min = 0; var max = Math.max(this.workersCount || 0, 1) - 1; var workerIndex = min + Math.floor(Math.random() * (max - min + 1)); value = value.replace('{workerIndex}', String(workerIndex)); } // Switch server URL protocol to HTTP instead of HTTPS if browser is IE9 or lesser var isIE = window.navigator && (/MSIE/.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)); if (isIE && window.document && window.document.all && !window.atob) { var isSsl = /^(http|ws)s/.test(value); if (isSsl === location.protocol === 'https') { value = value.replace(/^(http|ws)s/, '$1'); } } this._serverUrl = value; } /** * Return comet server url * @returns {string} */ getServerUrl() { return this._serverUrl; } /** * Return station UID - unique id of current javascript environment (browser tab) * @returns {null} */ getStationUid() { return stationUid; } /** * Return true, if connection is opened * @returns {boolean} */ isOpened() { return this._isOpened; } /** * Return true, if connection closed by client (manually) * @returns {boolean} */ isForceClosed() { return this._forceClosed; } /** * Open connection */ open() { this._forceClosed = false; if (!this._isOpened) { this.transport.open(this._serverUrl); } } /** * Close connection */ close() { this._forceClosed = true; if (this._isOpened) { this.transport.close(); } } /** * * @param {string} name * @param {function} handler * @param {*} [data] * @param {boolean} [isAppend] */ on(name, handler, data, isAppend) { // Subscribe on hub channels if (name.indexOf(Client.EVENT_CHANNEL_NAME) === 0) { this.subscribe(name.substr(Client.EVENT_CHANNEL_NAME.length)); } super.on(...arguments); } /** * @param {string} name * @param {function} [handler] * @return boolean */ off(name, handler) { super.off(...arguments); // Unsubscribe on hub channels if (name.indexOf(Client.EVENT_CHANNEL_NAME) === 0) { this.unsubscribe(name.substr(Client.EVENT_CHANNEL_NAME.length)); } } /** * @param {string} channel */ subscribe(channel) { if (_indexOf(this._subscribes, channel) === -1) { this._sendInternal('subscribe ' + channel); this._subscribes.push(channel); } } /** * @param {string} channel */ unsubscribe(channel) { var index = _indexOf(this._subscribes, channel); if (index !== -1) { this._sendInternal('unsubscribe ' + channel); this._subscribes.splice(index, 1); } } /** * * @param {string} name * @returns {boolean} */ hasChannelHandlers(name) { return this.hasEventHandlers(Client.EVENT_CHANNEL_NAME + name); } /** * * @param {string} channel * @param {object} data */ send(channel, data) { if (typeof data !== 'string') { data = JSON.stringify(data); } this._sendInternal('channel ' + channel + ' ' + data); } /** * * @param {string} route * @param {object} [data] */ request(route, data) { data = data || {}; data.requestUid = String.generateUid(); // Trigger event for append data var event = new RequestEvent({ route: route, params: data }); this.trigger(Client.EVENT_BEFORE_REQUEST, event); data = event.params; // Generate promise for wait response var promise = new Promise(resolve => { this._requestsInProcess[data.requestUid] = { route: route, resolve: resolve }; }); // Send request this._sendInternal('action ' + route + ' ' + JSON.stringify(data)); return promise; } /** * * @param {string} message * @private */ _sendInternal(message) { // Trigger event before send message var event = new MessageEvent({ message: message }); this.trigger(Client.EVENT_BEFORE_SEND, event); message = event.message; if (this._isOpened) { this.transport.send(message); } } _onOpen(event) { if (!this._isOpened) { this._isOpened = true; if (this.autoSubscribeOnReconnect) { var channels = this._subscribes; this._subscribes = []; _each(channels, this.subscribe.bind(this)); } this.trigger(Client.EVENT_OPEN, event); } } _onClose(event) { if (this._isOpened) { this._isOpened = false; this.trigger(Client.EVENT_CLOSE, event); } } _onMessage(event) { if (event.message.indexOf('action ') === 0) { var response = JSON.parse(event.message.substr(7)); if (response.requestUid && this._requestsInProcess[response.requestUid]) { this._requestsInProcess[response.requestUid].resolve(response); // Trigger request event this.trigger(Client.EVENT_REQUEST, new RequestEvent({ route: this._requestsInProcess[response.requestUid].route, params: response })); delete this._requestsInProcess[response.requestUid]; } } if (event.message.indexOf('channel ') === 0) { var message = event.message.substr(8); var i = message.indexOf(' '); var messageString = message.substr(i + 1); var params = messageString.match(/^[\{\[]/) ? JSON.parse(messageString) : null; var channelEvent = new ChannelEvent({ channel: message.substr(0, i), params: params, message: !params ? messageString : null }); // Trigger channel and channel:* events this.trigger(Client.EVENT_CHANNEL_NAME + channelEvent.channel, channelEvent); this.trigger(Client.EVENT_CHANNEL, channelEvent); } // Trigger message event this.trigger(Client.EVENT_MESSAGE, new MessageEvent({ message: event.message })); } } /** * @event Client#request * @property {RequestEvent} event */ Client.EVENT_REQUEST = 'request'; /** * @event Client#beforeRequest * @property {RequestEvent} event */ Client.EVENT_BEFORE_REQUEST = 'beforeRequest'; /** * @event Client#message * @property {MessageEvent} event */ Client.EVENT_MESSAGE = 'message'; /** * @event Client#channel: * @property {ChannelEvent} event */ Client.EVENT_CHANNEL_NAME = 'channel:'; /** * @event Client#channel * @property {ChannelEvent} event */ Client.EVENT_CHANNEL = 'channel'; /** * @event Client#beforeSend * @property {MessageEvent} event */ Client.EVENT_BEFORE_SEND = 'beforeSend'; /** * @event Client#close * @property {Event} event */ Client.EVENT_CLOSE = 'close'; /** * @event Client#open * @property {Event} event */ Client.EVENT_OPEN = 'open'; module.exports = Client;