diff --git a/assets/js/app.js b/assets/js/app.js index 4a1de42..c58425b 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -99,7 +99,7 @@ export class App { // error = defined with "Waiting" description // info = safely defined with configured data const latestPing = pings[pings.length - 1] - const serverRegistration = this.serverRegistry.createServerRegistration(latestPing.info.name) + const serverRegistration = this.serverRegistry.createServerRegistration(latestPing.serverId) serverRegistration.initServerStatus(latestPing) diff --git a/assets/js/graph.js b/assets/js/graph.js index 6d4a2b3..c9e7f1c 100644 --- a/assets/js/graph.js +++ b/assets/js/graph.js @@ -148,11 +148,7 @@ export class GraphDisplayManager { this.loadLocalStorage() } - // Remap the incoming data from being string (serverName) keyed into serverId keys - for (const serverName of Object.keys(graphData)) { - const serverRegistration = this._app.serverRegistry.getServerRegistration(serverName) - this._graphData[serverRegistration.serverId] = graphData[serverName] - } + this._graphData = graphData // Explicitly define a height so flot.js can rescale the Y axis document.getElementById('big-graph').style.height = '400px' diff --git a/assets/js/main.js b/assets/js/main.js index d34f4cc..2b3b36d 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -34,19 +34,22 @@ document.addEventListener('DOMContentLoaded', function () { let lastRowCounter = 0 let controlsHTML = '' - Object.keys(data).sort().forEach(function (serverName) { - const serverRegistration = app.serverRegistry.getServerRegistration(serverName) + app.serverRegistry.getServerRegistrations() + .map(serverRegistration => serverRegistration.data.name) + .sort() + .forEach(serverName => { + const serverRegistration = app.serverRegistry.getServerRegistration(serverName) - controlsHTML += '' + - '' + - ' ' + serverName + - '' + controlsHTML += '' + + '' + + ' ' + serverName + + '' - // Occasionally break table rows using a magic number - if (++lastRowCounter % 6 === 0) { - controlsHTML += '' - } - }) + // 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 = '' + @@ -59,25 +62,6 @@ document.addEventListener('DOMContentLoaded', function () { app.graphDisplayManager.initEventListeners() }) - socket.on('updateHistoryGraph', function (data) { - // Skip any incoming updates if the graph is disabled - // The backend shouldn't send these anyways - if (!app.graphDisplayManager.isVisible) { - return - } - - const serverRegistration = app.serverRegistry.getServerRegistration(data.name) - - if (serverRegistration) { - app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, data.playerCount) - - // Only redraw the graph if not mutating hidden data - if (serverRegistration.isVisible) { - app.graphDisplayManager.requestRedraw() - } - } - }) - socket.on('add', function (data) { data.forEach(app.addServer) }) @@ -86,11 +70,25 @@ document.addEventListener('DOMContentLoaded', function () { // 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.info.name) + 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) { @@ -121,26 +119,6 @@ document.addEventListener('DOMContentLoaded', function () { app.handleSyncComplete() }) - socket.on('updatePeak', function (data) { - const serverRegistration = app.serverRegistry.getServerRegistration(data.name) - - if (serverRegistration) { - serverRegistration.updateServerPeak(data.timestamp, data.playerCount) - } - }) - - socket.on('peaks', function (data) { - Object.keys(data).forEach(function (serverName) { - const serverRegistration = app.serverRegistry.getServerRegistration(serverName) - - if (serverRegistration) { - const graphPeak = data[serverName] - - serverRegistration.updateServerPeak(graphPeak.timestamp, graphPeak.playerCount) - } - }) - }) - window.addEventListener('resize', function () { app.percentageBar.redraw() diff --git a/assets/js/servers.js b/assets/js/servers.js index 6b37212..8c530e0 100644 --- a/assets/js/servers.js +++ b/assets/js/servers.js @@ -1,4 +1,4 @@ -import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions, isArrayEqual, isObjectEqual } from './util' +import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions } from './util' import MISSING_FAVICON from '../images/missing_favicon.svg' @@ -48,8 +48,7 @@ export class ServerRegistry { } } - createServerRegistration (serverName) { - const serverId = this._serverIdsByName[serverName] + createServerRegistration (serverId) { const serverData = this._serverDataById[serverId] const serverRegistration = new ServerRegistration(this._app, serverId, serverData) this._registeredServers[serverId] = serverRegistration @@ -65,23 +64,6 @@ export class ServerRegistry { } } - getServerRankBy (serverRegistration, x, sort) { - const records = Object.values(this._registeredServers) - .map(x) - .filter(val => val !== undefined) - - // Invalidate any results that do not account for all serverRegistrations - if (records.length === this._registeredServers.length) { - records.sort(sort) - - // Pull matching data from target serverRegistration - // Assume indexOf cannot be -1 or val undefined since they have been pre-tested in the map call above - const val = x(serverRegistration) - const indexOf = records.indexOf(val) - return indexOf + 1 - } - } - getServerRegistrations = () => Object.values(this._registeredServers) reset () { @@ -102,7 +84,6 @@ export class ServerRegistration { isFavorite = false rankIndex lastRecordData - lastVersions = [] lastPeakData constructor (app, serverId, data) { @@ -139,7 +120,7 @@ export class ServerRegistration { if (pushToGraph) { // Only update graph for successful pings // This intentionally pauses the server graph when pings begin to fail - this._graphData.push([payload.info.timestamp, this.playerCount]) + this._graphData.push([payload.timestamp, this.playerCount]) // Trim graphData to within the max length by shifting out the leading elements if (this._graphData.length > SERVER_GRAPH_DATA_MAX_LENGTH) { @@ -174,7 +155,7 @@ export class ServerRegistration { document.getElementById('ranking_' + this.serverId).innerText = '#' + (rankIndex + 1) } - updateServerPeak (time, playerCount) { + updateServerPeak (data) { const peakLabelElement = document.getElementById('peak_' + this.serverId) // Always set label once any peak data has been received @@ -182,13 +163,10 @@ export class ServerRegistration { const peakValueElement = document.getElementById('peak-value_' + this.serverId) - peakValueElement.innerText = formatNumber(playerCount) - peakLabelElement.title = 'At ' + formatTimestamp(time) + peakValueElement.innerText = formatNumber(data.playerCount) + peakLabelElement.title = 'At ' + formatTimestamp(data.timestamp) - this.lastPeakData = { - timestamp: time, - playerCount: playerCount - } + this.lastPeakData = data } updateServerStatus (ping, isInitialUpdate, minecraftVersions) { @@ -196,21 +174,14 @@ export class ServerRegistration { // Otherwise the ping value is pushed into the graphData when already present this.handlePing(ping, !isInitialUpdate) - // Compare against a cached value to avoid empty updates - // Allow undefined ping.versions inside the if statement for text reset handling - if (ping.versions && !isArrayEqual(ping.versions, this.lastVersions)) { - this.lastVersions = ping.versions - + if (ping.versions) { const versionsElement = document.getElementById('version_' + this.serverId) versionsElement.style.display = 'block' versionsElement.innerText = formatMinecraftVersions(ping.versions, minecraftVersions[this.data.type]) || '' } - // Compare against a cached value to avoid empty updates - if (ping.recordData !== undefined && !isObjectEqual(ping.recordData, this.lastRecordData, ['playerCount', 'timestamp'])) { - this.lastRecordData = ping.recordData - + if (ping.recordData) { // Always set label once any record data has been received const recordLabelElement = document.getElementById('record_' + this.serverId) @@ -227,6 +198,12 @@ export class ServerRegistration { } else { recordValueElement.innerText = formatNumber(recordData.playerCount) } + + this.lastRecordData = recordData + } + + if (ping.graphPeakData) { + this.updateServerPeak(ping.graphPeakData) } const playerCountLabelElement = document.getElementById('player-count_' + this.serverId) diff --git a/assets/js/util.js b/assets/js/util.js index e0f7932..6e55e42 100644 --- a/assets/js/util.js +++ b/assets/js/util.js @@ -135,20 +135,6 @@ export function isArrayEqual (a, b) { return true } -export function isObjectEqual (a, b, props) { - if (typeof a === 'undefined' || typeof a !== typeof b) { - return false - } - for (let i = 0; i < props.length; i++) { - const prop = props[i] - - if (typeof a[prop] === 'undefined' || typeof a[prop] !== typeof b[prop] || a[prop] !== b[prop]) { - return false - } - } - return true -} - // From http://detectmobilebrowsers.com/ export function isMobileBrowser () { var check = false; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 347473b..377b4d8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,8 @@ +**5.2.0** *(Apr 29 2020)* +- Updated protocol to use serverIds instead of string names. This will reduce wasted bandwidth when pushing updates. +- Removed "updatePeak", "peaks" and "updateHistoryGraph" socket events. Their behavior has been optimized and merged into "update". +- Removed various legacy code. + **5.1.2** *(Apr 22 2020)* - Fixes the historical graph overflowing the maximum graphDuration value. diff --git a/lib/app.js b/lib/app.js index e5e3d9e..b62f44a 100644 --- a/lib/app.js +++ b/lib/app.js @@ -39,24 +39,11 @@ class App { client.on('requestHistoryGraph', () => { // Send historical graphData built from all serverRegistrations const graphData = {} - const graphPeaks = {} this.serverRegistrations.forEach((serverRegistration) => { - graphData[serverRegistration.data.name] = serverRegistration.graphData - - // Send current peak, if any - const graphPeak = serverRegistration.getGraphPeak() - if (graphPeak) { - graphPeaks[serverRegistration.data.name] = graphPeak - } + graphData[serverRegistration.serverId] = serverRegistration.graphData }) - // Send current peaks, if any - // Emit peaks first since graphData may take a while to receive - if (Object.keys(graphPeaks).length > 0) { - client.emit('peaks', graphPeaks) - } - client.emit('historyGraph', graphData) }) } diff --git a/lib/ping.js b/lib/ping.js index 6128a0c..ac6a67c 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -107,10 +107,10 @@ class PingController { handlePing (serverRegistration, resp, err, version) { const timestamp = new Date().getTime() - this._app.server.broadcast('update', serverRegistration.getUpdate(timestamp, resp, err, version)) - serverRegistration.addPing(timestamp, resp) + let updateHistoryGraph = false + if (config.logToDatabase) { const playerCount = resp ? resp.players.online : 0 @@ -118,25 +118,14 @@ class PingController { this._app.database.insertPing(serverRegistration.data.ip, timestamp, playerCount) if (serverRegistration.addGraphPoint(resp !== undefined, playerCount, timestamp)) { - this._app.server.broadcast('updateHistoryGraph', { - name: serverRegistration.data.name, - playerCount: playerCount, - timestamp: timestamp - }) - } - - // Update calculated graph peak regardless if the graph is being updated - // This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes - if (serverRegistration.findNewGraphPeak()) { - const graphPeak = serverRegistration.getGraphPeak() - - this._app.server.broadcast('updatePeak', { - name: serverRegistration.data.name, - playerCount: graphPeak.playerCount, - timestamp: graphPeak.timestamp - }) + updateHistoryGraph = true } } + + // Generate a combined update payload + // This includes any modified fields and flags used by the frontend + // This will not be cached and can contain live metadata + this._app.server.broadcast('update', serverRegistration.getUpdate(timestamp, resp, err, version, updateHistoryGraph)) } } diff --git a/lib/servers.js b/lib/servers.js index 919be31..7668866 100644 --- a/lib/servers.js +++ b/lib/servers.js @@ -2,22 +2,22 @@ const config = require('../config') const minecraftVersions = require('../minecraft_versions') class ServerRegistration { + serverId lastFavicon versions = [] recordData graphData = [] - constructor (data) { + constructor (serverId, data) { + this.serverId = serverId this.data = data this._pingHistory = [] } - getUpdate (timestamp, resp, err, version) { + getUpdate (timestamp, resp, err, version, updateHistoryGraph) { const update = { - info: { - name: this.data.name, - timestamp: timestamp - } + serverId: this.serverId, + timestamp: timestamp } if (resp) { @@ -48,6 +48,20 @@ class ServerRegistration { update.result = { players: resp.players } + + if (config.logToDatabase) { + // Update calculated graph peak regardless if the graph is being updated + // This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes + if (this.findNewGraphPeak()) { + update.graphPeakData = this.getGraphPeak() + } + + // Handled inside logToDatabase to validate logic from #getUpdate call + // Only append when true since an undefined value == false + if (updateHistoryGraph) { + update.updateHistoryGraph = true + } + } } else if (err) { // Append a filtered copy of err // This ensures any unintended data is not leaked @@ -94,15 +108,21 @@ class ServerRegistration { const lastPing = this._pingHistory[this._pingHistory.length - 1] const payload = { - info: { - name: this.data.name - }, + serverId: this.serverId, timestamp: lastPing.timestamp, versions: this.versions, recordData: this.recordData, favicon: this.lastFavicon } + // Only append graphPeakData if defined + // The value is lazy computed and conditional that config->logToDatabase == true + const graphPeakData = this.getGraphPeak() + + if (graphPeakData) { + payload.graphPeakData = graphPeakData + } + // Conditionally append to avoid defining fields with undefined values if (lastPing.result) { payload.result = lastPing.result @@ -118,15 +138,14 @@ class ServerRegistration { } return [{ + serverId: this.serverId, + timestamp: new Date().getTime(), error: { message: 'Waiting...', placeholder: true }, - timestamp: new Date().getTime(), - info: { - name: this.data.name - }, - recordData: this.recordData + recordData: this.recordData, + graphPeakData: this.getGraphPeak() }] } diff --git a/main.js b/main.js index 32c53d1..44d05d4 100644 --- a/main.js +++ b/main.js @@ -8,7 +8,7 @@ const servers = require('./servers') const app = new App() -servers.forEach(server => { +servers.forEach((server, serverId) => { // Assign a generated color for each servers.json entry if not manually defined // These will be passed to the frontend for use in rendering if (!server.color) { @@ -22,7 +22,7 @@ servers.forEach(server => { } // Init a ServerRegistration instance of each entry in servers.json - app.serverRegistrations.push(new ServerRegistration(server)) + app.serverRegistrations.push(new ServerRegistration(serverId, server)) }) if (!config.logToDatabase) { diff --git a/package-lock.json b/package-lock.json index 08f317a..22a0c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "minetrack", - "version": "5.1.2", + "version": "5.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index df644e5..5633989 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minetrack", - "version": "5.1.2", + "version": "5.2.0", "description": "A Minecraft server tracker that lets you focus on the basics.", "main": "main.js", "dependencies": {