import {Polymer, html} from '@polymer/polymer/polymer-legacy'; import '@polymer/iron-ajax/iron-ajax.js'; const elemTemplate = html` <iron-ajax id="ajaxConnect" url="" handle-as="json" method="post" content-type="application/json" loading="{{loadingConnect}}" last-response="{{connectLastResponse}}" on-response="_handleConnect" on-error="_handleConnectError" debounce-duration="100"></iron-ajax> <iron-ajax id="ajaxDisconnect" url="" handle-as="json" method="post" content-type="application/json" loading="{{loadingDisconnect}}" last-response="{{_disconnectLastResponse}}" on-response="_handleDisconnect" debounce-duration="100"></iron-ajax> <iron-ajax id="ajaxSubscribe" url="" handle-as="json" method="post" content-type="application/json" loading="{{loadingSubscribe}}" last-response="{{subscribeLastResponse}}" on-response="_handleSubscribe" debounce-duration="100"></iron-ajax> <iron-ajax id="ajaxUnsubscribe" url="" handle-as="json" method="post" content-type="application/json" loading="{{loadingUnsubscribe}}" last-response="{{unsubscribeLastResponse}}" on-response="_handleUnsubscribe" debounce-duration="100"></iron-ajax> <iron-ajax id="ajaxMessage" url="" handle-as="json" method="post" content-type="application/json" loading="{{loadingMessage}}" last-response="{{messageLastResponse}}" on-response="_handleMessage" on-error="_handleMessageError" debounce-duration="100"></iron-ajax> <iron-ajax id="ajaxListen" url="" handle-as="text" loading="{{loadingListen}}" last-response="{{listenLastResponse}}" on-request="_handleListenOpen" on-error="_handleListenError" on-response="_handleListenMessageEvent" debounce-duration="100"></iron-ajax> ` Polymer({ is: 'channelstream-connection', _template: elemTemplate, /** * Fired when `channels` array changes. * * @event channelstream-channels-changed */ /** * Fired when `connect()` method succeeds. * * @event channelstream-connected */ /** * Fired when `connect` fails. * * @event channelstream-connect-error */ /** * Fired when `disconnect()` succeeds. * * @event channelstream-disconnected */ /** * Fired when `message()` succeeds. * * @event channelstream-message-sent */ /** * Fired when `message()` fails. * * @event channelstream-message-error */ /** * Fired when `subscribe()` succeeds. * * @event channelstream-subscribed */ /** * Fired when `subscribe()` fails. * * @event channelstream-subscribe-error */ /** * Fired when `unsubscribe()` succeeds. * * @event channelstream-unsubscribed */ /** * Fired when `unsubscribe()` fails. * * @event channelstream-unsubscribe-error */ /** * Fired when listening connection receives a message. * * @event channelstream-listen-message */ /** * Fired when listening connection is opened. * * @event channelstream-listen-opened */ /** * Fired when listening connection is closed. * * @event channelstream-listen-closed */ /** * Fired when listening connection suffers an error. * * @event channelstream-listen-error */ properties: { isReady: Boolean, /** List of channels user should be subscribed to. */ channels: { type: Array, value: function () { return [] }, notify: true }, /** Username of connecting user. */ username: { type: String, value: 'Anonymous', reflectToAttribute: true }, /** Connection identifier. */ connectionId: { type: String, reflectToAttribute: true }, /** Websocket instance. */ websocket: { type: Object, value: null }, /** Websocket connection url. */ websocketUrl: { type: String, value: '' }, /** URL used in `connect()`. */ connectUrl: { type: String, value: '' }, /** URL used in `disconnect()`. */ disconnectUrl: { type: String, value: '' }, /** URL used in `subscribe()`. */ subscribeUrl: { type: String, value: '' }, /** URL used in `unsubscribe()`. */ unsubscribeUrl: { type: String, value: '' }, /** URL used in `message()`. */ messageUrl: { type: String, value: '' }, /** Long-polling connection url. */ longPollUrl: { type: String, value: '' }, /** Long-polling connection url. */ shouldReconnect: { type: Boolean, value: true }, /** Should send heartbeats. */ heartbeats: { type: Boolean, value: true }, /** How much should every retry interval increase (in milliseconds) */ increaseBounceIv: { type: Number, value: 2000 }, _currentBounceIv: { type: Number, reflectToAttribute: true, value: 0 }, /** Should use websockets or long-polling by default */ useWebsocket: { type: Boolean, reflectToAttribute: true, value: true }, connected: { type: Boolean, reflectToAttribute: true, value: false } }, observers: [ '_handleChannelsChange(channels.splices)' ], listeners: { 'channelstream-connected': 'startListening', 'channelstream-connect-error': 'retryConnection', }, /** * Mutators hold functions that you can set locally to change the data * that the client is sending to all endpoints * you can call it like `elem.mutators('connect', yourFunc())` * mutators will be executed in order they were pushed onto arrays * */ mutators: { connect: function () { return [] }(), message: function () { return [] }(), subscribe: function () { return [] }(), unsubscribe: function () { return [] }(), disconnect: function () { return [] }() }, ready: function () { this.isReady = true; }, /** * Connects user and fetches connection id from the server. * */ connect: function () { var request = this.$['ajaxConnect']; request.url = this.connectUrl; request.body = { username: this.username, channels: this.channels }; for (var i = 0; i < this.mutators.connect.length; i++) { this.mutators.connect[i](request); } request.generateRequest() }, /** * Overwrite with custom function that will */ addMutator: function (type, func) { this.mutators[type].push(func); }, /** * Subscribes user to channels. * */ subscribe: function (channels) { var request = this.$['ajaxSubscribe']; request.url = this.subscribeUrl; request.body = { channels: channels, conn_id: this.connectionId }; for (var i = 0; i < this.mutators.subscribe.length; i++) { this.mutators.subscribe[i](request); } if (request.body.channels.length) { request.generateRequest(); } }, /** * Unsubscribes user from channels. * */ unsubscribe: function (unsubscribe) { var request = this.$['ajaxUnsubscribe']; request.url = this.unsubscribeUrl; request.body = { channels: unsubscribe, conn_id: this.connectionId }; for (var i = 0; i < this.mutators.unsubscribe.length; i++) { this.mutators.unsubscribe[i](request); } request.generateRequest() }, /** * calculates list of channels we should add user to based on difference * between channels property and passed channel list */ calculateSubscribe: function (channels) { var currentlySubscribed = this.channels; var toSubscribe = []; for (var i = 0; i < channels.length; i++) { if (currentlySubscribed.indexOf(channels[i]) === -1) { toSubscribe.push(channels[i]); } } return toSubscribe }, /** * calculates list of channels we should remove user from based difference * between channels property and passed channel list */ calculateUnsubscribe: function (channels) { var currentlySubscribed = this.channels; var toUnsubscribe = []; for (var i = 0; i < channels.length; i++) { if (currentlySubscribed.indexOf(channels[i]) !== -1) { toUnsubscribe.push(channels[i]); } } return toUnsubscribe }, /** * Marks the connection as expired. * */ disconnect: function () { var request = this.$['ajaxDisconnect']; request.url = this.disconnectUrl; request.params = { conn_id: this.connectionId }; for (var i = 0; i < this.mutators.disconnect.length; i++) { this.mutators.disconnect[i](request); } // mark connection as expired request.generateRequest(); // disconnect existing connection this.closeConnection(); }, /** * Sends a message to the server. * */ message: function (message) { var request = this.$['ajaxMessage']; request.url = this.messageUrl; request.body = message; for (var i = 0; i < this.mutators.message.length; i++) { this.mutators.message[i](request) } request.generateRequest(); }, /** * Opens "long lived" (websocket/longpoll) connection to the channelstream server. * */ startListening: function (event) { this.fire('start-listening', {}); if (this.useWebsocket) { this.useWebsocket = window.WebSocket ? true : false; } if (this.useWebsocket) { this.openWebsocket(); } else { this.openLongPoll(); } }, /** * Opens websocket connection. * */ openWebsocket: function () { var url = this.websocketUrl + '?conn_id=' + this.connectionId; this.websocket = new WebSocket(url); this.websocket.onopen = this._handleListenOpen.bind(this); this.websocket.onclose = this._handleListenCloseEvent.bind(this); this.websocket.onerror = this._handleListenErrorEvent.bind(this); this.websocket.onmessage = this._handleListenMessageEvent.bind(this); }, /** * Opens long-poll connection. * */ openLongPoll: function () { var request = this.$['ajaxListen']; request.url = this.longPollUrl + '?conn_id=' + this.connectionId; request.generateRequest() }, /** * Retries `connect()` call while incrementing interval between tries up to 1 minute. * */ retryConnection: function () { if (!this.shouldReconnect) { return; } if (this._currentBounceIv < 60000) { this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv; } else { this._currentBounceIv = 60000; } setTimeout(this.connect.bind(this), this._currentBounceIv); }, /** * Closes listening connection. * */ closeConnection: function () { var request = this.$['ajaxListen']; if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.onclose = null; this.websocket.onerror = null; this.websocket.close(); } if (request.loading) { request.lastRequest.abort(); } this.connected = false; }, _handleChannelsChange: function (event) { // do not fire the event if set() didn't mutate anything // is this a reliable way to do it? if (!this.isReady || event === undefined) { return } this.fire('channelstream-channels-changed', event) }, _handleListenOpen: function (event) { this.connected = true; this.fire('channelstream-listen-opened', event); this.createHeartBeats(); }, createHeartBeats: function () { if (typeof self._heartbeat === 'undefined' && this.websocket !== null && this.heartbeats) { self._heartbeat = setInterval(this._sendHeartBeat.bind(this), 10000); } }, _sendHeartBeat: function () { if (this.websocket.readyState === WebSocket.OPEN && this.heartbeats) { this.websocket.send(JSON.stringify({type: 'heartbeat'})); } }, _handleListenError: function (event) { this.connected = false; this.retryConnection(); }, _handleConnectError: function (event) { this.connected = false; this.fire('channelstream-connect-error', event.detail); }, _handleListenMessageEvent: function (event) { var data = null; // comes from iron-ajax if (event.detail) { data = JSON.parse(event.detail.response) // comes from websocket setTimeout(this.openLongPoll.bind(this), 0); } else { data = JSON.parse(event.data) } this.fire('channelstream-listen-message', data); }, _handleListenCloseEvent: function (event) { this.connected = false; this.fire('channelstream-listen-closed', event.detail); this.retryConnection(); }, _handleListenErrorEvent: function (event) { this.connected = false; this.fire('channelstream-listen-error', {}) }, _handleConnect: function (event) { this.currentBounceIv = 0; this.connectionId = event.detail.response.conn_id; this.fire('channelstream-connected', event.detail.response); }, _handleDisconnect: function (event) { this.connected = false; this.fire('channelstream-disconnected', {}); }, _handleMessage: function (event) { this.fire('channelstream-message-sent', event.detail.response); }, _handleMessageError: function (event) { this.fire('channelstream-message-error', event.detail); }, _handleSubscribe: function (event) { this.fire('channelstream-subscribed', event.detail.response); }, _handleSubscribeError: function (event) { this.fire('channelstream-subscribe-error', event.detail); }, _handleUnsubscribe: function (event) { this.fire('channelstream-unsubscribed', event.detail.response); }, _handleUnsubscribeError: function (event) { this.fire('channelstream-unsubscribe-error', event.detail); } });