From 33e01a4a8faffba96928fa668d67fd05ab3bbfa6 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 22 Jul 2026 17:35:23 +0300 Subject: [PATCH] Fixed reconnect --- index.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- public/app.js | 2 ++ public/index.html | 2 +- public/styles.css | 7 ++++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 66a4c91..a02578a 100644 --- a/index.js +++ b/index.js @@ -7,11 +7,17 @@ const { EventEmitter } = require('events'); const SteamUser = require('steam-user'); const protobuf = require('protobufjs'); +const APP_VERSION = require('./package.json').version; + const DOTA_APPID = 570; const DEFAULT_PORT = 3000; const DEFAULT_REFRESH_MS = 3 * 60 * 1000; const READY_TIMEOUT_MS = 60 * 1000; const LOBBY_TIMEOUT_MS = 30 * 1000; +const WATCHDOG_INTERVAL_MS = 30 * 1000; +const GC_RELAUNCH_AFTER_MS = 2 * 60 * 1000; +const GC_RELOG_AFTER_MS = 10 * 60 * 1000; +const STEAM_STUCK_AFTER_MS = 10 * 60 * 1000; const AUTH_FILE = process.env.STEAM_AUTH_FILE || path.join(__dirname, '.steam-auth.json'); const CREDENTIALS_FILE = process.env.STEAM_CREDENTIALS_FILE || path.join(path.dirname(AUTH_FILE), '.steam-credentials.json'); const ADMIN_PASSWORD = process.env.AUTH_ADMIN_PASSWORD || process.env.ADMIN_PASSWORD || ''; @@ -263,6 +269,11 @@ class DotaLobbyClient extends EventEmitter { this.pendingLobbyRequest = null; this.steamGuardPrompt = null; this.steamGuardCallback = null; + this.gcDownSince = null; + this.steamDownSince = null; + this.lastGcRelaunchAt = 0; + this.watchdogTimer = setInterval(() => this.watchdog(), WATCHDOG_INTERVAL_MS); + this.watchdogTimer.unref(); this.createSteamUser(); } @@ -482,6 +493,72 @@ class DotaLobbyClient extends EventEmitter { this.emit('change'); } + // Periodic self-healing: the GC can silently drop our session (e.g. GC + // maintenance restarts) while hello messages keep going into the void. + // Escalate: relaunch Dota after GC_RELAUNCH_AFTER_MS, then tear down and + // rebuild the whole Steam connection after GC_RELOG_AFTER_MS. Also rebuilds + // the connection if Steam itself stays offline longer than STEAM_STUCK_AFTER_MS. + watchdog() { + const now = Date.now(); + + if (this.steamGuardCallback) { + // Waiting on user input; nothing to heal automatically. + this.gcDownSince = null; + this.steamDownSince = null; + return; + } + + if (!this.loggedOn) { + this.gcDownSince = null; + if (!this.steamDownSince) { + this.steamDownSince = now; + return; + } + if (now - this.steamDownSince >= STEAM_STUCK_AFTER_MS) { + this.steamDownSince = now; + console.warn('watchdog: Steam connection stuck offline, rebuilding session...'); + this.hardReset(); + } + return; + } + + this.steamDownSince = null; + + if (this.gcReady) { + this.gcDownSince = null; + return; + } + + if (!this.gcDownSince) { + this.gcDownSince = now; + return; + } + + if (now - this.gcDownSince >= GC_RELOG_AFTER_MS) { + this.gcDownSince = now; + console.warn('watchdog: GC unavailable despite relaunches, rebuilding Steam session...'); + this.hardReset(); + return; + } + + if (now - this.gcDownSince >= GC_RELAUNCH_AFTER_MS && now - this.lastGcRelaunchAt >= GC_RELAUNCH_AFTER_MS) { + this.lastGcRelaunchAt = now; + console.warn('watchdog: GC not ready, relaunching Dota 2...'); + this.clearHelloTimer(); + this.launchDota(true); + } + } + + hardReset() { + this.gcDownSince = null; + this.resetSteamUser(); + try { + this.start(); + } catch (err) { + console.error(`watchdog: could not restart Steam connection: ${err.message}`); + } + } + scheduleReconnect() { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { @@ -569,6 +646,10 @@ class DotaLobbyClient extends EventEmitter { this.readyWaiters.delete(waiter); this.gcReady = false; this.setGcState('timeout'); + if (this.loggedOn) { + this.clearHelloTimer(); + this.launchDota(true); + } reject(new Error('Dota 2 game coordinator did not become ready in time.')); }, timeoutMs), }; @@ -741,6 +822,7 @@ let activeRefresh = null; function publicState() { return { ...state, + version: APP_VERSION, connection: dota.status(), refreshIntervalMs, regions: REGIONS, diff --git a/package.json b/package.json index e0d446a..1b4dd8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "doka-lobby", - "version": "1.0.0", + "version": "1.1.0", "private": true, "description": "List joinable Dota 2 custom game lobbies, filtered by game name / map", "main": "index.js", diff --git a/public/app.js b/public/app.js index 647fec0..fb799be 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,6 @@ const els = { statusBadge: document.querySelector('#statusBadge'), + appVersion: document.querySelector('#appVersion'), serverFilter: document.querySelector('#serverFilter'), visibleCount: document.querySelector('#visibleCount'), totalCount: document.querySelector('#totalCount'), @@ -213,6 +214,7 @@ function render() { const [label, klass] = statusInfo(data); setBadge(label, klass); + els.appVersion.textContent = data?.version ? `v${data.version}` : ''; els.visibleCount.textContent = String(filtered.length); els.totalCount.textContent = String(data?.totalLobbies || lobbies.length || 0); els.lastUpdated.textContent = formatDate(data?.lastUpdatedAt); diff --git a/public/index.html b/public/index.html index ff06cc6..e7d2726 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,7 @@
-

Dota 2 lobbies

+

Dota 2 lobbies

starting GC: Auto diff --git a/public/styles.css b/public/styles.css index 0f964fe..e1c4b7b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -55,6 +55,13 @@ select { letter-spacing: 0; } +.title .version { + font-size: 12px; + font-weight: 400; + color: var(--muted); + vertical-align: middle; +} + .subline { display: flex; flex-wrap: wrap;