channelstream-connection.js
502 lines
| 13.3 KiB
| application/javascript
|
JavascriptLexer
/ rhodecode / public / js / src / components / channelstream-connection / channelstream-connection.js
r787 | 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); | ||||
} | ||||
}); | ||||