diff --git a/assets/js/app.js b/assets/js/app.js
index c58425b..c291dc6 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -1,4 +1,5 @@
import { ServerRegistry } from './servers'
+import { SocketManager } from './socket'
import { SortController } from './sort'
import { GraphDisplayManager } from './graph'
import { MojangUpdater } from './mojang'
@@ -13,6 +14,7 @@ export class App {
this.tooltip = new Tooltip()
this.caption = new Caption()
this.serverRegistry = new ServerRegistry(this)
+ this.socketManager = new SocketManager(this)
this.sortController = new SortController(this)
this.graphDisplayManager = new GraphDisplayManager(this)
this.mojangUpdater = new MojangUpdater()
@@ -22,6 +24,11 @@ export class App {
this._taskIds = []
}
+ // Called once the DOM is ready and the app can begin setup
+ init () {
+ this.socketManager.createWebSocket()
+ }
+
setPageReady (isReady) {
document.getElementById('push').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
this.serverRegistry.reset()
+ this.socketManager.reset()
this.sortController.reset()
this.graphDisplayManager.reset()
this.mojangUpdater.reset()
@@ -81,9 +89,6 @@ export class App {
document.getElementById('stat_totalPlayers').innerText = 0
document.getElementById('stat_networks').innerText = 0
- // Modify page state to display loading overlay
- this.caption.set('Lost connection!')
-
this.setPageReady(false)
}
diff --git a/assets/js/main.js b/assets/js/main.js
index 2b3b36d..c0ee1b5 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,123 +1,9 @@
import { App } from './app'
-import io from 'socket.io-client'
-
const app = new App()
-document.addEventListener('DOMContentLoaded', function () {
- const socket = io.connect({
- reconnect: true,
- reconnectDelay: 1000,
- reconnectionAttempts: 10
- })
-
- // The backend will automatically push data once connected
- socket.on('connect', function () {
- app.caption.set('Loading...')
- })
-
- socket.on('disconnect', function () {
- app.handleDisconnect()
-
- // Reset modified DOM structures
- document.getElementById('big-graph-mobile-load-request').style.display = 'none'
- })
-
- socket.on('historyGraph', function (data) {
- // 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(data)
-
- // 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 += '
' +
- '' +
- ' ' + serverName +
- '
'
-
- // Occasionally break table rows using a magic number
- if (++lastRowCounter % 6 === 0) {
- controlsHTML += '
'
- }
- })
-
- // Apply generated HTML and show controls
- document.getElementById('big-graph-checkboxes').innerHTML = '
' +
- controlsHTML +
- '
'
-
- document.getElementById('big-graph-controls').style.display = 'block'
-
- // Bind click event for updating graph data
- app.graphDisplayManager.initEventListeners()
- })
-
- socket.on('add', function (data) {
- data.forEach(app.addServer)
- })
-
- socket.on('update', function (data) {
- // 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(data.serverId)
-
- if (serverRegistration) {
- serverRegistration.updateServerStatus(data, false, app.publicConfig.minecraftVersions)
- }
-
- // Use update payloads to conditionally append data to graph
- // Skip any incoming updates if the graph is disabled
- if (data.updateHistoryGraph && app.graphDisplayManager.isVisible) {
- // Update may not be successful, safely append 0 points
- const playerCount = data.result ? data.result.players.online : 0
-
- app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, playerCount)
-
- // Only redraw the graph if not mutating hidden data
- if (serverRegistration.isVisible) {
- app.graphDisplayManager.requestRedraw()
- }
- }
- })
-
- socket.on('updateMojangServices', function (data) {
- app.mojangUpdater.updateStatus(data)
- })
-
- socket.on('setPublicConfig', function (data) {
- app.setPublicConfig(data)
-
- // 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 (data.isGraphVisible) {
- if (app.graphDisplayManager.isVisible) {
- socket.emit('requestHistoryGraph')
- } else {
- document.getElementById('big-graph-mobile-load-request').style.display = 'block'
- }
- }
- })
-
- // Fired once the backend has sent all requested data
- socket.on('syncComplete', function () {
- app.handleSyncComplete()
- })
+document.addEventListener('DOMContentLoaded', () => {
+ app.init()
window.addEventListener('resize', function () {
app.percentageBar.redraw()
@@ -128,7 +14,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('big-graph-mobile-load-request-button').addEventListener('click', function () {
// Send a graph data request to the backend
- socket.emit('requestHistoryGraph')
+ app.socketManager.sendHistoryGraphRequest()
// Hide the activation link to avoid multiple requests
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
diff --git a/assets/js/socket.js b/assets/js/socket.js
new file mode 100644
index 0000000..fd0d6de
--- /dev/null
+++ b/assets/js/socket.js
@@ -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 += '
' +
+ '' +
+ ' ' + serverName +
+ '
'
+
+ // Occasionally break table rows using a magic number
+ if (++lastRowCounter % 6 === 0) {
+ controlsHTML += '
'
+ }
+ })
+
+ // Apply generated HTML and show controls
+ document.getElementById('big-graph-checkboxes').innerHTML = '