Minetrack/assets/js/socket.js

206 lines
6.8 KiB
JavaScript
Raw Permalink Normal View History

2020-05-05 22:17:12 +00:00
export class SocketManager {
2023-12-30 23:03:54 +00:00
constructor(app) {
this._app = app;
this._hasRequestedHistoryGraph = false;
this._reconnectDelayBase = 0;
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
reset() {
this._hasRequestedHistoryGraph = false;
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
createWebSocket() {
let webSocketProtocol = "ws:";
if (location.protocol === "https:") {
webSocketProtocol = "wss:";
}
2023-12-30 23:03:54 +00:00
this._webSocket = new WebSocket(`${webSocketProtocol}//${location.host}`);
2020-05-05 22:17:12 +00:00
// The backend will automatically push data once connected
this._webSocket.onopen = () => {
2023-12-30 23:03:54 +00:00
this._app.caption.set("Loading...");
2020-05-05 22:17:12 +00:00
// Reset reconnection scheduling since the WebSocket has been established
2023-12-30 23:03:54 +00:00
this._reconnectDelayBase = 0;
};
2020-05-05 22:17:12 +00:00
this._webSocket.onclose = (event) => {
2023-12-30 23:03:54 +00:00
this._app.handleDisconnect();
2020-05-05 22:17:12 +00:00
// 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) {
2023-12-30 23:03:54 +00:00
this._app.caption.set("Lost connection!");
2020-05-05 22:17:12 +00:00
} else {
2023-12-30 23:03:54 +00:00
this._app.caption.set("Disconnected due to error.");
2020-05-05 22:17:12 +00:00
}
// Schedule socket reconnection attempt
2023-12-30 23:03:54 +00:00
this.scheduleReconnect();
};
2020-05-05 22:17:12 +00:00
this._webSocket.onmessage = (message) => {
2023-12-30 23:03:54 +00:00
const payload = JSON.parse(message.data);
2020-05-05 22:17:12 +00:00
switch (payload.message) {
2023-12-30 23:03:54 +00:00
case "init":
this._app.setPublicConfig(payload.config);
2020-05-05 22:17:12 +00:00
// Display the main page component
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
2023-12-30 23:03:54 +00:00
this._app.setPageReady(true);
2020-05-05 22:17:12 +00:00
// 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) {
2023-12-30 23:03:54 +00:00
this.sendHistoryGraphRequest();
2020-05-05 22:17:12 +00:00
}
payload.servers.forEach((serverPayload, serverId) => {
2023-12-30 23:03:54 +00:00
this._app.addServer(
serverId,
serverPayload,
payload.timestampPoints
);
});
2020-05-05 22:17:12 +00:00
// Init payload contains all data needed to render the page
// Alert the app it is ready
2023-12-30 23:03:54 +00:00
this._app.handleSyncComplete();
2020-05-05 22:17:12 +00:00
2023-12-30 23:03:54 +00:00
break;
2020-05-05 22:17:12 +00:00
2023-12-30 23:03:54 +00:00
case "updateServers": {
for (
let serverId = 0;
serverId < payload.updates.length;
serverId++
) {
// 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
2023-12-30 23:03:54 +00:00
const serverRegistration =
this._app.serverRegistry.getServerRegistration(serverId);
const serverUpdate = payload.updates[serverId];
if (serverRegistration) {
2023-12-30 23:03:54 +00:00
serverRegistration.handlePing(serverUpdate, payload.timestamp);
serverRegistration.updateServerStatus(
serverUpdate,
this._app.publicConfig.minecraftVersions
);
}
}
2020-05-05 22:17:12 +00:00
// Bulk add playerCounts into graph during #updateHistoryGraph
if (payload.updateHistoryGraph) {
2023-12-30 23:03:54 +00:00
this._app.graphDisplayManager.addGraphPoint(
payload.timestamp,
Object.values(payload.updates).map((update) => update.playerCount)
);
2020-05-05 22:17:12 +00:00
// Run redraw tasks after handling bulk updates
2023-12-30 23:03:54 +00:00
this._app.graphDisplayManager.redraw();
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
this._app.percentageBar.redraw();
this._app.updateGlobalStats();
2023-12-30 23:03:54 +00:00
break;
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
case "historyGraph": {
this._app.graphDisplayManager.buildPlotInstance(
payload.timestamps,
payload.graphData
);
2020-05-05 22:17:12 +00:00
// Build checkbox elements for graph controls
2023-12-30 23:03:54 +00:00
let lastRowCounter = 0;
let controlsHTML = "";
2020-05-05 22:17:12 +00:00
2023-12-30 23:03:54 +00:00
this._app.serverRegistry
.getServerRegistrations()
.map((serverRegistration) => serverRegistration.data.name)
2020-05-05 22:17:12 +00:00
.sort()
2023-12-30 23:03:54 +00:00
.forEach((serverName) => {
const serverRegistration =
this._app.serverRegistry.getServerRegistration(serverName);
2020-05-05 22:17:12 +00:00
controlsHTML += `<td><label>
2023-12-30 23:03:54 +00:00
<input type="checkbox" class="graph-control" minetrack-server-id="${
serverRegistration.serverId
}" ${serverRegistration.isVisible ? "checked" : ""}>
${serverName}
2023-12-30 23:03:54 +00:00
</label></td>`;
2020-05-05 22:17:12 +00:00
// Occasionally break table rows using a magic number
if (++lastRowCounter % 6 === 0) {
2023-12-30 23:03:54 +00:00
controlsHTML += "</tr><tr>";
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
});
2020-05-05 22:17:12 +00:00
// Apply generated HTML and show controls
2023-12-30 23:03:54 +00:00
document.getElementById(
"big-graph-checkboxes"
).innerHTML = `<table><tr>${controlsHTML}</tr></table>`;
document.getElementById("big-graph-controls").style.display = "block";
2020-05-05 22:17:12 +00:00
// Bind click event for updating graph data
2023-12-30 23:03:54 +00:00
this._app.graphDisplayManager.initEventListeners();
break;
2020-05-05 22:17:12 +00:00
}
}
2023-12-30 23:03:54 +00:00
};
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
scheduleReconnect() {
2020-05-05 22:17:12 +00:00
// Release any active WebSocket references
2023-12-30 23:03:54 +00:00
this._webSocket = undefined;
2020-05-05 22:17:12 +00:00
2023-12-30 23:03:54 +00:00
this._reconnectDelayBase++;
2020-05-05 22:17:12 +00:00
// Exponential backoff for reconnection attempts
// Clamp ceiling value to 30 seconds
2023-12-30 23:03:54 +00:00
this._reconnectDelaySeconds = Math.min(
this._reconnectDelayBase * this._reconnectDelayBase,
30
);
2020-05-05 22:17:12 +00:00
const reconnectInterval = setInterval(() => {
2023-12-30 23:03:54 +00:00
this._reconnectDelaySeconds--;
2020-05-05 22:17:12 +00:00
if (this._reconnectDelaySeconds === 0) {
// Explicitly clear interval, this avoids race conditions
// #clearInterval first to avoid potential errors causing pre-mature returns
2023-12-30 23:03:54 +00:00
clearInterval(reconnectInterval);
2020-05-05 22:17:12 +00:00
// Update displayed text
2023-12-30 23:03:54 +00:00
this._app.caption.set("Reconnecting...");
2020-05-05 22:17:12 +00:00
// Attempt reconnection
// Only attempt when reconnectDelaySeconds === 0 and not <= 0, otherwise multiple attempts may be started
2023-12-30 23:03:54 +00:00
this.createWebSocket();
2020-05-05 22:17:12 +00:00
} else if (this._reconnectDelaySeconds > 0) {
// Update displayed text
2023-12-30 23:03:54 +00:00
this._app.caption.set(
`Reconnecting in ${this._reconnectDelaySeconds}s...`
);
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
}, 1000);
2020-05-05 22:17:12 +00:00
}
2023-12-30 23:03:54 +00:00
sendHistoryGraphRequest() {
2020-05-05 22:17:12 +00:00
if (!this._hasRequestedHistoryGraph) {
2023-12-30 23:03:54 +00:00
this._hasRequestedHistoryGraph = true;
2020-05-05 22:17:12 +00:00
// 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
2023-12-30 23:03:54 +00:00
this._webSocket.send("requestHistoryGraph");
2020-05-05 22:17:12 +00:00
}
}
}