Implement reconnect mechanism for websockets
therungg opened this issue · 2 comments
therungg commented
Right now, the websockets have no reconnect mechanism. AWS' websockets hang up (always!!) after 10 minutes on no messages, and after 2 hours regardless.
This means the live page stops working (timers just keep running and the page needs to be refreshed) after 2 hours.
Implement a reconnect when the server hangs up.
zoglo commented
When I was building the live comparison for waifus tournament (after Livesplit changes with custom splits I did abandon it), I wrote a heartbeat for testing purposes.
Basically listening to the websocket connection and it's events.
Was native JS tho so might look into it.
zoglo commented
Example ES6 plugin below, I didn't finish coding it but the logic is in _initHandlers
import {extend} from "../core/options"
import options from "../data/routes.json"
/*import { Sortable, Swap } from 'sortablejs/modular/sortable.core.esm'
Sortable.mount(new Swap())*/
export class theRunDashboard
{
constructor(options)
{
this.options = extend(true,
{
selector: '#runners',
runners: [],
start: '#startws',
close: '#closews',
socket: {
keepalive: 9,
createTimeout: 500,
},
console: {
start: ['color: GreenYellow'],
error: ['color: Red'],
update: ['color: Aqua'],
info: ['color: DarkGray'],
end: ['color: Magenta']
}
}, options ||{})
this.selector = document.querySelector(this.options.selector)
this.start = document.querySelector(this.options.start)
this.close = document.querySelector(this.options.close)
this.runners = this.options.runners
if (
!this.selector ||
!this.start ||
!this.close ||
!this.runners.length
)
return
this.contents = []
this.sockets = []
this.timeouts = []
this.timers = []
this.abort = false
this._init()
}
_init()
{
this._bindButtons()
}
_bindButtons()
{
this.start.addEventListener('click', () => {
this._initPlayers()
})
this.close.addEventListener('click', () => {
this._clearSockets()
})
}
_initPlayers()
{
this.runners.forEach((runner, index, collection) => {
setTimeout(() => {
this._createWebSocket(runner, index)
this._createRunner(runner, index)
}, index * this.options.socket.createTimeout);
})
}
_setPlayers()
{
this.runners.forEach((runner, index, collection) => {
this._createRunner(runner, index)
})
}
_createRunner(runner, index)
{
let template = document.createElement('template')
template.innerHTML = `<div class="item" data-item-index="${index}">
<div class="inside">
<span class="name">${runner}</span>
<span class="timer" data-timer="${runner}"></span>
<span class="split" data-split="${runner}"></span>
</div>
</div>`
this.contents[index] = this.selector.appendChild(template.content.firstChild)
}
_createWebSocket(runner, index)
{
let socket = new WebSocket(options.websocket.route + runner);
this.sockets[index] = socket
this._initHandlers(socket, runner, index)
}
_initHandlers(socket, runner, index)
{
socket.onclose = (evt) => {
if (this.abort)
return
console.log('%cClose event: socket for %s closed: %s', this.options.console.error.join(';'), runner, evt.reason);
this._resetSocket(index)
this._createWebSocket(runner, index)
}
socket.onerror = (evt) => {
if (this.abort)
return
console.log('%cError event: socket for %s closed: %s', this.options.console.error.join(';'), runner, evt.reason);
this._resetSocket(index)
this._createWebSocket(runner, index)
}
socket.onopen = (evt) => {
console.log('%cListening to: %s', this.options.console.start.join(';'), runner);
this._heartBeat(socket, runner, index);
}
socket.onmessage = (evt) => {
this._analyzeData(evt, runner, index)
}
}
_setCurrentTime(index, ctime)
{
if (this.timers[index])
clearInterval(this.timers[index])
const timer = this.contents[index].querySelector('.timer')
// ToDo: create timer that updates
setInterval(this._setCurrentTimeInMs(timer, ctime), 1000);
}
_setCurrentTimeInMs(timer, cms)
{
const ms = Math.floor(cms % 60).toString().padStart(2,'0')
const s = Math.floor((cms / 1000) % 60).toString().padStart(2,'0')
const m = Math.floor((cms / 1000 / 60) % 60).toString().padStart(2,'0')
const h = Math.floor((cms / 1000 / 60 / 60) % 24).toString().padStart(2,'0')
timer.innerHTML = `${h}:${m}:${s}:${ms}`;
}
_convertDate(time)
{
const date = new Date(time);
const h = date.getHours();
const m = "0" + date.getMinutes();
const s = "0" + date.getSeconds();
const ms = "0" + date.getMilliseconds();
return h.pad(2) + ':' + m.substring(-2).padStart(2) + ':' + s.substring(-2).padStart(2) + ':' + ms.substring(-2).padStart(2);
}
_analyzeData(evt, runner, index)
{
const data = JSON.parse(evt.data)
if (data.message)
return
const date = (new Date()).toUTCString()
const run = data.run
this._setCurrentTime(index, run.currentTime)
console.group('%s: %s', runner, date);
console.log('%cUpdate for: %s', this.options.console.update.join(';'), runner);
console.log('User: %s', data.user)
console.log('CurrentSplit: %s', run.currentSplitIndex)
console.log('CurrentTime: %s', run.currentTime)
console.log('CurrentPrediction: %s', run.currentPrediction)
console.log('Delta: %s', run.delta)
console.log('PB: %s', run.pb)
console.log('Run Time: %s', run.gameData.totalRunTime)
console.log(data);
console.groupEnd()
//localStorage.setItem(key, 'Value');
}
_heartBeat(socket, runner, index)
{
this.timeouts[index] = setInterval(() => {
//console.log('keepalive for: %s', runner)
socket.send('ping')
}, 60000 * this.options.socket.keepalive);
}
_resetSocket(index)
{
clearInterval(this.timeouts[index])
delete this.sockets[index]
delete this.timeouts[index]
this.contents[index].remove()
delete this.contents[index]
}
_clearSockets()
{
this.abort = true
console.log('%cStopped all sockets', this.options.console.end.join(';'))
this.sockets.forEach((socket, index) => {
socket.close()
this._resetSocket(index)
})
this.sockets = []
this.timeouts = []
this.contents = []
}
}