|
|
<link rel="import" href="../../../../../../bower_components/polymer/polymer.html">
|
|
|
<link rel="import" href="../../../../../../bower_components/iron-ajax/iron-ajax.html">
|
|
|
|
|
|
<!--
|
|
|
|
|
|
`<channelstream-connection>` allows you to connect and interact with channelstream server
|
|
|
abstracting websocket/long-polling connections from you.
|
|
|
|
|
|
In typical use, just slap some `<channelstream-connection>` at the top of your body:
|
|
|
|
|
|
<body>
|
|
|
<channelstream-connection
|
|
|
username="{{user.username}}"
|
|
|
connect-url="http://127.0.0.1:8000/demo/connect"
|
|
|
disconnect-url="http://127.0.0.1:8000/disconnect"
|
|
|
subscribe-url="http://127.0.0.1:8000/demo/subscribe"
|
|
|
message-url="http://127.0.0.1:8000/demo/message"
|
|
|
long-poll-url="http://127.0.0.1:8000/listen"
|
|
|
websocket-url="http://127.0.0.1:8000/ws"
|
|
|
channels-url='["channel1", "channel2"]' />
|
|
|
|
|
|
Then you can do `channelstreamElem.connect()` to kick off your connection.
|
|
|
This element also handles automatic reconnections.
|
|
|
|
|
|
## Default handlers
|
|
|
|
|
|
By default element has a listener attached that will fire `startListening()` handler on `channelstream-connected` event.
|
|
|
|
|
|
By default element has a listener attached that will fire `retryConnection()` handler on `channelstream-connect-error` event,
|
|
|
this handler will forever try to re-establish connection to the server incrementing intervals between retries up to 1 minute.
|
|
|
|
|
|
-->
|
|
|
<dom-module id="channelstream-connection">
|
|
|
<template>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</template>
|
|
|
<script>
|
|
|
Polymer({
|
|
|
is: 'channelstream-connection',
|
|
|
|
|
|
/**
|
|
|
* 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);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
</dom-module>
|
|
|
|