ai generated solutions to our ai generated problems

This commit is contained in:
Heidi
2026-06-20 00:20:48 +01:00
parent e7a172f52f
commit 7f1e6d0bef
5 changed files with 139 additions and 34 deletions
+120 -20
View File
@@ -54,6 +54,7 @@ const PUBLIC_DATA_CACHE_DIR = path.resolve(
const PUBLIC_DATA_CACHE_FRESH_MS = Number(process.env.PUBLIC_DATA_CACHE_FRESH_MS || 5 * 60 * 1000)
const PUBLIC_DATA_CACHE_STALE_MS = Number(process.env.PUBLIC_DATA_CACHE_STALE_MS || 24 * 60 * 60 * 1000)
const PUBLIC_DATA_PREWARM_INTERVAL_MS = Number(process.env.PUBLIC_DATA_PREWARM_INTERVAL_MS || PUBLIC_DATA_CACHE_FRESH_MS)
const PUBLIC_DATA_COLD_TIMEOUT_MS = Number(process.env.PUBLIC_DATA_COLD_TIMEOUT_MS || 8000)
const PUBLIC_DATA_CACHE_MAX_AGE_SECONDS = Math.max(0, Math.floor(PUBLIC_DATA_CACHE_FRESH_MS / 1000))
const PUBLIC_DATA_STALE_REVALIDATE_SECONDS = Math.max(0, Math.floor(PUBLIC_DATA_CACHE_STALE_MS / 1000))
const API_RATE_LIMIT_WINDOW_MS = Number(process.env.API_RATE_LIMIT_WINDOW_MS || 60000)
@@ -220,6 +221,14 @@ function publicDataKey(value) {
return encodeURIComponent(String(value || '').trim()).replace(/%/g, '~')
}
function publicDataValue(value) {
try {
return decodeURIComponent(String(value || '').replace(/~/g, '%'))
} catch {
return ''
}
}
function publicDataCachePathForUrl(requestUrl) {
const pathname = requestUrl.pathname
const params = requestUrl.searchParams
@@ -269,6 +278,45 @@ function publicDataCachePathForUrl(requestUrl) {
return null
}
function publicDataApiPathForRelative(relativePath) {
if (relativePath === 'leaderboard-teams.json') return '/api/tss/leaderboard/teams?limit=100'
if (relativePath === 'leaderboard-players.json') return '/api/tss/leaderboard/players?limit=100'
if (relativePath === 'home-teams.json') return '/api/tss/leaderboard/teams?limit=4'
if (relativePath === 'recent-games.json') return '/api/tss/games/recent?limit=50'
let match = relativePath.match(/^teams\/(.+)\.games\.json$/)
if (match) {
const team = publicDataValue(match[1])
return team ? `/api/tss/teams/${encodeURIComponent(team)}/games` : ''
}
match = relativePath.match(/^teams\/(.+)\.json$/)
if (match) {
const team = publicDataValue(match[1])
return team ? `/api/tss/teams/${encodeURIComponent(team)}` : ''
}
match = relativePath.match(/^players\/(.+)\.json$/)
if (match) {
const uid = publicDataValue(match[1])
return /^[0-9]{1,32}$/.test(uid) ? `/api/tss/player/${encodeURIComponent(uid)}` : ''
}
match = relativePath.match(/^games\/(.+)\.logs\.json$/)
if (match) {
const gameId = publicDataValue(match[1])
return /^[A-Za-z0-9_-]{1,96}$/.test(gameId) ? `/api/tss/games/${encodeURIComponent(gameId)}/logs` : ''
}
match = relativePath.match(/^games\/(.+)\.json$/)
if (match) {
const gameId = publicDataValue(match[1])
return /^[A-Za-z0-9_-]{1,96}$/.test(gameId) ? `/api/tss/games/${encodeURIComponent(gameId)}` : ''
}
return ''
}
function safePublicDataPath(requestPath) {
let decoded = '/'
try {
@@ -286,6 +334,14 @@ function safePublicDataPath(requestPath) {
return filePath
}
function publicDataRelativePath(requestPath) {
try {
return decodeURIComponent(requestPath).replace(/^\/data\/?/, '')
} catch {
return ''
}
}
function sendPublicDataFile(req, res, filePath, status = 200, extraHeaders = {}) {
fs.readFile(filePath, (error, data) => {
if (error) {
@@ -301,6 +357,40 @@ function sendPublicDataFile(req, res, filePath, status = 200, extraHeaders = {})
})
}
async function servePublicData(req, res, pathname) {
const filePath = safePublicDataPath(pathname)
const relativePath = publicDataRelativePath(pathname)
if (!filePath || !relativePath) {
sendJson(res, 400, { error: 'Bad request' })
return
}
const current = cachedPublicData(filePath)
if (current?.fresh) {
sendPublicDataFile(req, res, filePath, 200, { 'x-tssbot-cache': 'data-hit' })
return
}
const apiPath = publicDataApiPathForRelative(relativePath)
if (!apiPath) {
sendJson(res, 404, { error: 'Snapshot not found' })
return
}
if (current) {
refreshPublicData(filePath, new URL(apiPath, API_UPSTREAM))
sendPublicDataFile(req, res, filePath, 200, { 'x-tssbot-cache': 'data-stale' })
return
}
try {
await fillPublicData(filePath, new URL(apiPath, API_UPSTREAM), PUBLIC_DATA_COLD_TIMEOUT_MS)
sendPublicDataFile(req, res, filePath, 200, { 'x-tssbot-cache': 'data-filled' })
} catch (error) {
sendJson(res, 504, { error: 'Snapshot unavailable', detail: error.message })
}
}
function cachedPublicData(filePath) {
if (!filePath) return null
@@ -322,16 +412,24 @@ function writePublicDataFile(filePath, body) {
fs.renameSync(tmpPath, filePath)
}
function refreshPublicData(filePath, target) {
if (!filePath || publicDataRefreshes.has(filePath)) return
async function fillPublicData(filePath, target, timeoutMs = SERVER_REQUEST_TIMEOUT_MS) {
if (!filePath) return false
if (publicDataRefreshes.has(filePath)) return false
publicDataRefreshes.add(filePath)
fetchUpstreamJsonBuffer(target)
.then((body) => writePublicDataFile(filePath, body))
.catch((error) => {
console.warn(`public data refresh failed for ${target.pathname}: ${error.message}`)
})
.finally(() => publicDataRefreshes.delete(filePath))
try {
const body = await fetchUpstreamJsonBuffer(target, timeoutMs)
writePublicDataFile(filePath, body)
return true
} finally {
publicDataRefreshes.delete(filePath)
}
}
function refreshPublicData(filePath, target) {
fillPublicData(filePath, target).catch((error) => {
console.warn(`public data refresh failed for ${target.pathname}: ${error.message}`)
})
}
function publicDataPrewarmTargets() {
@@ -351,21 +449,26 @@ function publicDataPrewarmTargets() {
}
function prewarmPublicDataCache() {
const jobs = []
for (const { filePath, target } of publicDataPrewarmTargets()) {
const current = cachedPublicData(filePath)
if (current?.fresh) continue
refreshPublicData(filePath, target)
jobs.push(fillPublicData(filePath, target, PUBLIC_DATA_COLD_TIMEOUT_MS))
}
return Promise.allSettled(jobs)
}
function startPublicDataPrewarmer() {
fs.mkdirSync(PUBLIC_DATA_CACHE_DIR, { recursive: true })
prewarmPublicDataCache()
publicDataPrewarmTimer = setInterval(prewarmPublicDataCache, PUBLIC_DATA_PREWARM_INTERVAL_MS)
publicDataPrewarmTimer = setInterval(() => {
prewarmPublicDataCache().catch((error) => {
console.warn(`public data prewarm failed: ${error.message}`)
})
}, PUBLIC_DATA_PREWARM_INTERVAL_MS)
publicDataPrewarmTimer.unref?.()
}
function fetchUpstreamJsonBuffer(target) {
function fetchUpstreamJsonBuffer(target, timeoutMs = SERVER_REQUEST_TIMEOUT_MS) {
return new Promise((resolve, reject) => {
const client = target.protocol === 'https:' ? https : http
const chunks = []
@@ -401,7 +504,7 @@ function fetchUpstreamJsonBuffer(target) {
},
)
proxy.setTimeout(SERVER_REQUEST_TIMEOUT_MS, () => proxy.destroy(new Error('Upstream request timed out')))
proxy.setTimeout(timeoutMs, () => proxy.destroy(new Error(`Upstream request timed out after ${timeoutMs}ms`)))
proxy.on('error', reject)
proxy.end()
})
@@ -2687,12 +2790,7 @@ const server = http.createServer((req, res) => {
}
if (pathname.startsWith('/data/')) {
const filePath = safePublicDataPath(pathname)
if (!filePath) {
sendJson(res, 400, { error: 'Bad request' })
return
}
sendPublicDataFile(req, res, filePath)
servePublicData(req, res, pathname)
return
}
}
@@ -2713,7 +2811,7 @@ const server = http.createServer((req, res) => {
server.requestTimeout = SERVER_REQUEST_TIMEOUT_MS
server.headersTimeout = SERVER_HEADERS_TIMEOUT_MS
server.listen(PORT, '0.0.0.0', () => {
server.listen(PORT, '0.0.0.0', async () => {
console.log(`tssbot-web serving http://localhost:${PORT}`)
console.log(`proxying API requests to ${API_UPSTREAM}`)
if (RUN_BACKGROUND_JOBS) {
@@ -2729,6 +2827,8 @@ server.listen(PORT, '0.0.0.0', () => {
}
if (RUN_BACKGROUND_JOBS) {
startUptimeSampler()
fs.mkdirSync(PUBLIC_DATA_CACHE_DIR, { recursive: true })
await prewarmPublicDataCache()
startPublicDataPrewarmer()
}
process.send?.('ready')