add WebSocket reconnection logic
This commit is contained in:
parent
ca9e127e3e
commit
438a72724b
@ -1,4 +1,5 @@
|
|||||||
import { ServerRegistry } from './servers'
|
import { ServerRegistry } from './servers'
|
||||||
|
import { SocketManager } from './socket'
|
||||||
import { SortController } from './sort'
|
import { SortController } from './sort'
|
||||||
import { GraphDisplayManager } from './graph'
|
import { GraphDisplayManager } from './graph'
|
||||||
import { MojangUpdater } from './mojang'
|
import { MojangUpdater } from './mojang'
|
||||||
@ -13,6 +14,7 @@ export class App {
|
|||||||
this.tooltip = new Tooltip()
|
this.tooltip = new Tooltip()
|
||||||
this.caption = new Caption()
|
this.caption = new Caption()
|
||||||
this.serverRegistry = new ServerRegistry(this)
|
this.serverRegistry = new ServerRegistry(this)
|
||||||
|
this.socketManager = new SocketManager(this)
|
||||||
this.sortController = new SortController(this)
|
this.sortController = new SortController(this)
|
||||||
this.graphDisplayManager = new GraphDisplayManager(this)
|
this.graphDisplayManager = new GraphDisplayManager(this)
|
||||||
this.mojangUpdater = new MojangUpdater()
|
this.mojangUpdater = new MojangUpdater()
|
||||||
@ -22,6 +24,11 @@ export class App {
|
|||||||
this._taskIds = []
|
this._taskIds = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called once the DOM is ready and the app can begin setup
|
||||||
|
init () {
|
||||||
|
this.socketManager.createWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
setPageReady (isReady) {
|
setPageReady (isReady) {
|
||||||
document.getElementById('push').style.display = isReady ? 'block' : 'none'
|
document.getElementById('push').style.display = isReady ? 'block' : 'none'
|
||||||
document.getElementById('footer').style.display = isReady ? 'block' : 'none'
|
document.getElementById('footer').style.display = isReady ? 'block' : 'none'
|
||||||
@ -60,6 +67,7 @@ export class App {
|
|||||||
|
|
||||||
// Reset individual tracker elements to flush any held data
|
// Reset individual tracker elements to flush any held data
|
||||||
this.serverRegistry.reset()
|
this.serverRegistry.reset()
|
||||||
|
this.socketManager.reset()
|
||||||
this.sortController.reset()
|
this.sortController.reset()
|
||||||
this.graphDisplayManager.reset()
|
this.graphDisplayManager.reset()
|
||||||
this.mojangUpdater.reset()
|
this.mojangUpdater.reset()
|
||||||
|
@ -2,141 +2,8 @@ import { App } from './app'
|
|||||||
|
|
||||||
const app = new App()
|
const app = new App()
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const webSocket = new WebSocket('ws://' + location.host)
|
app.init()
|
||||||
|
|
||||||
// The backend will automatically push data once connected
|
|
||||||
webSocket.onopen = () => {
|
|
||||||
app.caption.set('Loading...')
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.onclose = (event) => {
|
|
||||||
app.handleDisconnect()
|
|
||||||
|
|
||||||
// Modify page state to display loading overlay
|
|
||||||
// Code 1006 denotes "Abnormal closure", most likely from the server or client losing connection
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
|
||||||
// Treat other codes as active errors (besides connectivity errors) when displaying the message
|
|
||||||
if (event.code === 1006) {
|
|
||||||
app.caption.set('Lost connection!')
|
|
||||||
} else {
|
|
||||||
app.caption.set('Disconnected due to error.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset modified DOM structures
|
|
||||||
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
|
||||||
|
|
||||||
// TODO: Reconnect behavior
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.onmessage = (message) => {
|
|
||||||
const payload = JSON.parse(message.data)
|
|
||||||
|
|
||||||
switch (payload.message) {
|
|
||||||
case 'init':
|
|
||||||
app.setPublicConfig(payload.config)
|
|
||||||
|
|
||||||
// Display the main page component
|
|
||||||
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
|
|
||||||
// Otherwise flot.js will cause visual alignment bugs
|
|
||||||
app.setPageReady(true)
|
|
||||||
|
|
||||||
// Allow the graphDisplayManager to control whether or not the historical graph is loaded
|
|
||||||
// Defer to isGraphVisible from the publicConfig to understand if the frontend will ever receive a graph payload
|
|
||||||
if (app.publicConfig.isGraphVisible) {
|
|
||||||
if (app.graphDisplayManager.isVisible) {
|
|
||||||
// Send request as a plain text string to avoid the server needing to parse JSON
|
|
||||||
// This is mostly to simplify the backend server's need for error handling
|
|
||||||
webSocket.send('requestHistoryGraph')
|
|
||||||
} else {
|
|
||||||
document.getElementById('big-graph-mobile-load-request').style.display = 'block'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.servers.forEach(app.addServer)
|
|
||||||
|
|
||||||
if (payload.mojangServices) {
|
|
||||||
app.mojangUpdater.updateStatus(payload.mojangServices)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init payload contains all data needed to render the page
|
|
||||||
// Alert the app it is ready
|
|
||||||
app.handleSyncComplete()
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'updateServer': {
|
|
||||||
// The backend may send "update" events prior to receiving all "add" events
|
|
||||||
// A server has only been added once it's ServerRegistration is defined
|
|
||||||
// Checking undefined protects from this race condition
|
|
||||||
const serverRegistration = app.serverRegistry.getServerRegistration(payload.serverId)
|
|
||||||
|
|
||||||
if (serverRegistration) {
|
|
||||||
serverRegistration.updateServerStatus(payload, false, app.publicConfig.minecraftVersions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use update payloads to conditionally append data to graph
|
|
||||||
// Skip any incoming updates if the graph is disabled
|
|
||||||
if (payload.updateHistoryGraph && app.graphDisplayManager.isVisible) {
|
|
||||||
// Update may not be successful, safely append 0 points
|
|
||||||
const playerCount = payload.result ? payload.result.players.online : 0
|
|
||||||
|
|
||||||
app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, payload.timestamp, playerCount)
|
|
||||||
|
|
||||||
// Only redraw the graph if not mutating hidden data
|
|
||||||
if (serverRegistration.isVisible) {
|
|
||||||
app.graphDisplayManager.requestRedraw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'updateMojangServices': {
|
|
||||||
app.mojangUpdater.updateStatus(payload)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'historyGraph': {
|
|
||||||
// Consider the graph visible since a payload has been received
|
|
||||||
// This is used for the manual graph load request behavior
|
|
||||||
app.graphDisplayManager.isVisible = true
|
|
||||||
|
|
||||||
app.graphDisplayManager.buildPlotInstance(payload.graphData)
|
|
||||||
|
|
||||||
// Build checkbox elements for graph controls
|
|
||||||
let lastRowCounter = 0
|
|
||||||
let controlsHTML = ''
|
|
||||||
|
|
||||||
app.serverRegistry.getServerRegistrations()
|
|
||||||
.map(serverRegistration => serverRegistration.data.name)
|
|
||||||
.sort()
|
|
||||||
.forEach(serverName => {
|
|
||||||
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
|
|
||||||
|
|
||||||
controlsHTML += '<td>' +
|
|
||||||
'<input type="checkbox" class="graph-control" minetrack-server-id="' + serverRegistration.serverId + '" ' + (serverRegistration.isVisible ? 'checked' : '') + '>' +
|
|
||||||
' ' + serverName +
|
|
||||||
'</input></td>'
|
|
||||||
|
|
||||||
// Occasionally break table rows using a magic number
|
|
||||||
if (++lastRowCounter % 6 === 0) {
|
|
||||||
controlsHTML += '</tr><tr>'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Apply generated HTML and show controls
|
|
||||||
document.getElementById('big-graph-checkboxes').innerHTML = '<table><tr>' +
|
|
||||||
controlsHTML +
|
|
||||||
'</tr></table>'
|
|
||||||
|
|
||||||
document.getElementById('big-graph-controls').style.display = 'block'
|
|
||||||
|
|
||||||
// Bind click event for updating graph data
|
|
||||||
app.graphDisplayManager.initEventListeners()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
app.percentageBar.redraw()
|
app.percentageBar.redraw()
|
||||||
@ -147,9 +14,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
document.getElementById('big-graph-mobile-load-request-button').addEventListener('click', function () {
|
document.getElementById('big-graph-mobile-load-request-button').addEventListener('click', function () {
|
||||||
// Send a graph data request to the backend
|
// Send a graph data request to the backend
|
||||||
// Send request as a plain text string to avoid the server needing to parse JSON
|
app.socketManager.sendHistoryGraphRequest()
|
||||||
// This is mostly to simplify the backend server's need for error handling
|
|
||||||
webSocket.send('requestHistoryGraph')
|
|
||||||
|
|
||||||
// Hide the activation link to avoid multiple requests
|
// Hide the activation link to avoid multiple requests
|
||||||
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
||||||
|
191
assets/js/socket.js
Normal file
191
assets/js/socket.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
export class SocketManager {
|
||||||
|
constructor (app) {
|
||||||
|
this._app = app
|
||||||
|
this._hasRequestedHistoryGraph = false
|
||||||
|
this._reconnectDelayBase = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
reset () {
|
||||||
|
this._hasRequestedHistoryGraph = false
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebSocket () {
|
||||||
|
this._webSocket = new WebSocket('ws://' + location.host)
|
||||||
|
|
||||||
|
// The backend will automatically push data once connected
|
||||||
|
this._webSocket.onopen = () => {
|
||||||
|
this._app.caption.set('Loading...')
|
||||||
|
|
||||||
|
// Reset reconnection scheduling since the WebSocket has been established
|
||||||
|
this._reconnectDelayBase = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this._webSocket.onclose = (event) => {
|
||||||
|
this._app.handleDisconnect()
|
||||||
|
|
||||||
|
// Modify page state to display loading overlay
|
||||||
|
// Code 1006 denotes "Abnormal closure", most likely from the server or client losing connection
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
||||||
|
// Treat other codes as active errors (besides connectivity errors) when displaying the message
|
||||||
|
if (event.code === 1006) {
|
||||||
|
this._app.caption.set('Lost connection!')
|
||||||
|
} else {
|
||||||
|
this._app.caption.set('Disconnected due to error.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset modified DOM structures
|
||||||
|
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
||||||
|
|
||||||
|
// Schedule socket reconnection attempt
|
||||||
|
this.scheduleReconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._webSocket.onmessage = (message) => {
|
||||||
|
const payload = JSON.parse(message.data)
|
||||||
|
|
||||||
|
switch (payload.message) {
|
||||||
|
case 'init':
|
||||||
|
this._app.setPublicConfig(payload.config)
|
||||||
|
|
||||||
|
// Display the main page component
|
||||||
|
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
|
||||||
|
// Otherwise flot.js will cause visual alignment bugs
|
||||||
|
this._app.setPageReady(true)
|
||||||
|
|
||||||
|
// Allow the graphDisplayManager to control whether or not the historical graph is loaded
|
||||||
|
// Defer to isGraphVisible from the publicConfig to understand if the frontend will ever receive a graph payload
|
||||||
|
if (this._app.publicConfig.isGraphVisible) {
|
||||||
|
if (this._app.graphDisplayManager.isVisible) {
|
||||||
|
this.sendHistoryGraphRequest()
|
||||||
|
} else {
|
||||||
|
document.getElementById('big-graph-mobile-load-request').style.display = 'block'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.servers.forEach(this._app.addServer)
|
||||||
|
|
||||||
|
if (payload.mojangServices) {
|
||||||
|
this._app.mojangUpdater.updateStatus(payload.mojangServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init payload contains all data needed to render the page
|
||||||
|
// Alert the app it is ready
|
||||||
|
this._app.handleSyncComplete()
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'updateServer': {
|
||||||
|
// The backend may send "update" events prior to receiving all "add" events
|
||||||
|
// A server has only been added once it's ServerRegistration is defined
|
||||||
|
// Checking undefined protects from this race condition
|
||||||
|
const serverRegistration = this._app.serverRegistry.getServerRegistration(payload.serverId)
|
||||||
|
|
||||||
|
if (serverRegistration) {
|
||||||
|
serverRegistration.updateServerStatus(payload, false, this._app.publicConfig.minecraftVersions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use update payloads to conditionally append data to graph
|
||||||
|
// Skip any incoming updates if the graph is disabled
|
||||||
|
if (payload.updateHistoryGraph && this._app.graphDisplayManager.isVisible) {
|
||||||
|
// Update may not be successful, safely append 0 points
|
||||||
|
const playerCount = payload.result ? payload.result.players.online : 0
|
||||||
|
|
||||||
|
this._app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, payload.timestamp, playerCount)
|
||||||
|
|
||||||
|
// Only redraw the graph if not mutating hidden data
|
||||||
|
if (serverRegistration.isVisible) {
|
||||||
|
this._app.graphDisplayManager.requestRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'updateMojangServices': {
|
||||||
|
this._app.mojangUpdater.updateStatus(payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'historyGraph': {
|
||||||
|
// Consider the graph visible since a payload has been received
|
||||||
|
// This is used for the manual graph load request behavior
|
||||||
|
this._app.graphDisplayManager.isVisible = true
|
||||||
|
|
||||||
|
this._app.graphDisplayManager.buildPlotInstance(payload.graphData)
|
||||||
|
|
||||||
|
// Build checkbox elements for graph controls
|
||||||
|
let lastRowCounter = 0
|
||||||
|
let controlsHTML = ''
|
||||||
|
|
||||||
|
this._app.serverRegistry.getServerRegistrations()
|
||||||
|
.map(serverRegistration => serverRegistration.data.name)
|
||||||
|
.sort()
|
||||||
|
.forEach(serverName => {
|
||||||
|
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverName)
|
||||||
|
|
||||||
|
controlsHTML += '<td>' +
|
||||||
|
'<input type="checkbox" class="graph-control" minetrack-server-id="' + serverRegistration.serverId + '" ' + (serverRegistration.isVisible ? 'checked' : '') + '>' +
|
||||||
|
' ' + serverName +
|
||||||
|
'</input></td>'
|
||||||
|
|
||||||
|
// Occasionally break table rows using a magic number
|
||||||
|
if (++lastRowCounter % 6 === 0) {
|
||||||
|
controlsHTML += '</tr><tr>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply generated HTML and show controls
|
||||||
|
document.getElementById('big-graph-checkboxes').innerHTML = '<table><tr>' +
|
||||||
|
controlsHTML +
|
||||||
|
'</tr></table>'
|
||||||
|
|
||||||
|
document.getElementById('big-graph-controls').style.display = 'block'
|
||||||
|
|
||||||
|
// Bind click event for updating graph data
|
||||||
|
this._app.graphDisplayManager.initEventListeners()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleReconnect () {
|
||||||
|
// Release any active WebSocket references
|
||||||
|
this._webSocket = undefined
|
||||||
|
|
||||||
|
this._reconnectDelayBase++
|
||||||
|
|
||||||
|
// Exponential backoff for reconnection attempts
|
||||||
|
// Clamp ceiling value to 30 seconds
|
||||||
|
this._reconnectDelaySeconds = Math.min((this._reconnectDelayBase * this._reconnectDelayBase), 30)
|
||||||
|
|
||||||
|
const reconnectInterval = setInterval(() => {
|
||||||
|
this._reconnectDelaySeconds--
|
||||||
|
|
||||||
|
if (this._reconnectDelaySeconds === 0) {
|
||||||
|
// Explicitly clear interval, this avoids race conditions
|
||||||
|
// #clearInterval first to avoid potential errors causing pre-mature returns
|
||||||
|
clearInterval(reconnectInterval)
|
||||||
|
|
||||||
|
// Update displayed text
|
||||||
|
this._app.caption.set('Reconnecting...')
|
||||||
|
|
||||||
|
// Attempt reconnection
|
||||||
|
// Only attempt when reconnectDelaySeconds === 0 and not <= 0, otherwise multiple attempts may be started
|
||||||
|
this.createWebSocket()
|
||||||
|
} else if (this._reconnectDelaySeconds > 0) {
|
||||||
|
// Update displayed text
|
||||||
|
this._app.caption.set('Reconnecting in ' + this._reconnectDelaySeconds + 's...')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendHistoryGraphRequest () {
|
||||||
|
if (!this._hasRequestedHistoryGraph) {
|
||||||
|
this._hasRequestedHistoryGraph = true
|
||||||
|
|
||||||
|
// Send request as a plain text string to avoid the server needing to parse JSON
|
||||||
|
// This is mostly to simplify the backend server's need for error handling
|
||||||
|
this._webSocket.send('requestHistoryGraph')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user