feat: add leaderboard cache, performance benchmarks, and verify SSE cleanup

- Add 30s TTL leaderboard cache with invalidation on fight completion
- Add scoreRound performance benchmark: 1000 rounds in <100ms
- Add checkAnswer performance benchmark: 1000 checks in <50ms
- Add adversarial regex backtracking test for checkAnswer
- Verify SSE cleanup: connections, IP counters, spectator counts, event listeners
  all properly decremented in finally block on disconnect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 07:50:46 +00:00
co-authored by Claude Opus 4.6
parent 7d9b8b1dbf
commit 4074ef94eb
4 changed files with 104 additions and 30 deletions
+30
View File
@@ -145,3 +145,33 @@ describe('checkAnswer', () => {
expect(checkAnswer('\t\n \r', ['Paris'])).toBe(0)
})
})
describe('checkAnswer performance', () => {
it('completes 1000 checks in under 50ms (<0.05ms each)', () => {
const answers = ['Paris', 'London', 'Tokyo']
const start = performance.now()
for (let i = 0; i < 1000; i++) {
checkAnswer('I think the answer is probably Paris', answers)
}
const elapsed = performance.now() - start
expect(elapsed).toBeLessThan(50)
})
it('no regex backtracking on adversarial input', () => {
// ReDoS-style strings that could cause catastrophic backtracking
const adversarial = [
'a'.repeat(10000),
'a'.repeat(5000) + '!' + 'a'.repeat(5000),
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab',
'(((((((((((((((((((((((((((((((',
'x'.repeat(2000) + 'y'.repeat(2000),
]
const start = performance.now()
for (const input of adversarial) {
checkAnswer(input, ['correct answer', '42', 'true'])
}
const elapsed = performance.now() - start
// Must complete in <100ms total for all adversarial inputs
expect(elapsed).toBeLessThan(100)
})
})
+4
View File
@@ -17,6 +17,7 @@ import { publishFightResult } from './nostr-publish.js'
import { getCurrentSeason } from './seasons.js'
import { onFightFinished as onTournamentFightFinished } from './tournaments.js'
import { trackFightCompleted, trackBotActive, trackMetric } from './analytics.js'
import { invalidateLeaderboardCache } from '../routes/bots.js'
const webhookResponseSchema = z.object({
answer: z.string().nullable().optional(),
@@ -539,6 +540,9 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
trackMetric(`challenge_${ct}`)
}
// Invalidate leaderboard cache after Elo/stats update
invalidateLeaderboardCache()
// Advance tournament bracket if this was a tournament match
try { onTournamentFightFinished(fightId, winnerId ?? null) } catch { /* not a tournament fight */ }
+20
View File
@@ -326,3 +326,23 @@ describe('calculateTier', () => {
expect(calculateTier(1900, 39)).toBe(5) // below Legend wins
})
})
describe('scoreRound performance', () => {
it('completes 1000 rounds in under 100ms (<0.1ms each)', () => {
const challenge = makeChallenge()
const botA = { id: 'a1', name: 'AlphaBot' }
const botB = { id: 'b1', name: 'BetaBot' }
const start = performance.now()
for (let i = 0; i < 1000; i++) {
scoreRound(
challenge, botA, botB,
makeResponse('4', 200 + i),
makeResponse('banana', 500 + i),
null, i % 6, 0,
)
}
const elapsed = performance.now() - start
expect(elapsed).toBeLessThan(100) // <0.1ms per call
})
})