強化房間清理與比賽中分頁限制

This commit is contained in:
2026-04-19 18:05:33 +08:00
parent 2d1ad0600e
commit edab74f125
9 changed files with 285 additions and 233 deletions

View File

@@ -11,6 +11,7 @@ const matchTableName = process.env.DB_TABLE ?? 'badminton'
const historyTableName = process.env.DB_HISTORY_TABLE ?? 'history'
const appVersion = process.env.APP_VERSION ?? `${Date.now()}`
const appStartedAt = new Date().toISOString()
const LIVE_ROOM_STALE_MS = 30_000
const currentFilePath = fileURLToPath(import.meta.url)
const currentDir = path.dirname(currentFilePath)
@@ -77,6 +78,17 @@ app.get('/api/rooms', (_request, response) => {
})
})
app.post('/api/rooms/reconcile', (_request, response) => {
const removedRoomIds = pruneStaleRooms()
response.json({
ok: true,
data: {
removedRoomIds,
},
})
})
app.get('/api/rooms/stream', (request, response) => {
setupSse(response)
roomListClients.add(response)
@@ -106,6 +118,7 @@ app.post('/api/rooms', (request, response) => {
clients: new Set(),
createdAt: now,
hostToken,
hostSeenAt: now,
roomId,
status: 'live',
updatedAt: now,
@@ -171,6 +184,35 @@ app.post('/api/rooms/:roomId/release', (request, response) => {
})
})
app.post('/api/rooms/:roomId/heartbeat', (request, response) => {
const room = rooms.get(request.params.roomId)
if (!room) {
response.status(404).json({
ok: false,
message: '找不到這個房間。',
})
return
}
const { hostToken } = request.body ?? {}
if (typeof hostToken !== 'string' || hostToken !== room.hostToken) {
response.status(403).json({
ok: false,
message: '沒有權限更新房間心跳。',
})
return
}
room.hostSeenAt = new Date().toISOString()
persistRooms()
response.json({
ok: true,
})
})
app.get('/api/rooms/:roomId', (request, response) => {
const room = rooms.get(request.params.roomId)
@@ -520,6 +562,8 @@ function loadPersistedRooms() {
nextRooms.set(savedRoom.roomId, {
...savedRoom,
hostSeenAt:
typeof savedRoom.hostSeenAt === 'string' ? savedRoom.hostSeenAt : savedRoom.updatedAt,
clients: new Set(),
})
})
@@ -540,6 +584,7 @@ function persistRooms() {
createdAt: room.createdAt,
groupId: room.groupId,
hostToken: room.hostToken,
hostSeenAt: room.hostSeenAt,
leftTeamName: room.leftTeamName,
matchupLabel: room.matchupLabel,
pointLog: room.pointLog,
@@ -618,6 +663,39 @@ function serializeRoom(room) {
}
}
function pruneStaleRooms() {
const now = Date.now()
const removedRoomIds = []
rooms.forEach((room, roomId) => {
if (room.status !== 'live') {
return
}
const hostSeenAtTime = Date.parse(room.hostSeenAt ?? '')
if (!Number.isFinite(hostSeenAtTime) || now - hostSeenAtTime > LIVE_ROOM_STALE_MS) {
room.clients.forEach((client) => {
sendSse(client, 'room-closed', {
roomId: room.roomId,
status: 'stale',
})
client.end()
})
rooms.delete(roomId)
removedRoomIds.push(roomId)
}
})
if (removedRoomIds.length > 0) {
persistRooms()
broadcastRoomList()
}
return removedRoomIds
}
function getLiveRoomSummaries() {
return Array.from(rooms.values())
.filter((room) => room.status === 'live')