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

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

@@ -1,197 +1 @@
[
{
"createdAt": "2026-04-19T04:50:38.216Z",
"groupId": 1,
"hostToken": "mo5afjewicxz50b9",
"leftTeamName": "柏威 / 玟瑄",
"matchupLabel": "柏威 / 玟瑄 vs 小念 / 建喵",
"pointLog": [
{
"round": 0,
"starter": 3,
"winCount": 0,
"winner": 1
},
{
"round": 1,
"starter": 3,
"winCount": 0,
"winner": 0
},
{
"round": 2,
"starter": 1,
"winCount": 1,
"winner": 0
},
{
"round": 3,
"starter": 1,
"winCount": 0,
"winner": 1
},
{
"round": 4,
"starter": 2,
"winCount": 1,
"winner": 1
}
],
"rightTeamName": "小念 / 建喵",
"roomId": "341793",
"scoreState": {
"scoreLeft": 2,
"scoreRight": 3,
"gamesLeft": 0,
"gamesRight": 0,
"currentGame": 1,
"targetScore": 21,
"serving": "right",
"leftRightCourtPlayer": "playerB",
"rightRightCourtPlayer": "playerA"
},
"status": "finished",
"targetDate": "2026-04-13",
"updatedAt": "2026-04-19T04:50:56.351Z",
"winnerTeamName": "小念 / 建喵"
},
{
"createdAt": "2026-04-19T04:51:17.794Z",
"groupId": 2,
"hostToken": "mo5agdyag7fyqyxv",
"leftTeamName": "景涵 / 小念",
"matchupLabel": "景涵 / 小念 vs 柏威 / 玟瑄",
"pointLog": [
{
"round": 0,
"starter": 0,
"winCount": 0,
"winner": 0
},
{
"round": 1,
"starter": 0,
"winCount": 0,
"winner": 1
}
],
"rightTeamName": "柏威 / 玟瑄",
"roomId": "174740",
"scoreState": {
"scoreLeft": 1,
"scoreRight": 1,
"gamesLeft": 0,
"gamesRight": 0,
"currentGame": 1,
"targetScore": 21,
"serving": "right",
"leftRightCourtPlayer": "playerB",
"rightRightCourtPlayer": "playerA"
},
"status": "finished",
"targetDate": "2026-04-13",
"updatedAt": "2026-04-19T04:51:25.160Z",
"winnerTeamName": "景涵 / 小念"
},
{
"createdAt": "2026-04-19T04:51:25.190Z",
"groupId": 2,
"hostToken": "mo5agjnqeabkfpr2",
"leftTeamName": "景涵 / 小念",
"matchupLabel": "景涵 / 小念 vs 柏威 / 玟瑄",
"pointLog": [
{
"round": 0,
"starter": 0,
"winCount": 0,
"winner": 0
},
{
"round": 1,
"starter": 0,
"winCount": 0,
"winner": 1
},
{
"round": 2,
"starter": 2,
"winCount": 1,
"winner": 1
}
],
"rightTeamName": "柏威 / 玟瑄",
"roomId": "239300",
"scoreState": {
"scoreLeft": 1,
"scoreRight": 2,
"gamesLeft": 0,
"gamesRight": 0,
"currentGame": 1,
"targetScore": 21,
"serving": "right",
"leftRightCourtPlayer": "playerB",
"rightRightCourtPlayer": "playerB"
},
"status": "finished",
"targetDate": "2026-04-13",
"updatedAt": "2026-04-19T04:52:26.087Z",
"winnerTeamName": "柏威 / 玟瑄"
},
{
"createdAt": "2026-04-19T04:58:15.291Z",
"groupId": 1,
"hostToken": "mo5apc3foksw0enn",
"leftTeamName": "景涵 / RuRu",
"matchupLabel": "景涵 / RuRu vs 小念 / 柏威",
"pointLog": [
{
"round": 0,
"starter": 0,
"winCount": 0,
"winner": 0
}
],
"rightTeamName": "小念 / 柏威",
"roomId": "432277",
"scoreState": {
"scoreLeft": 1,
"scoreRight": 0,
"gamesLeft": 0,
"gamesRight": 0,
"currentGame": 1,
"targetScore": 21,
"serving": "left",
"leftRightCourtPlayer": "playerB",
"rightRightCourtPlayer": "playerA"
},
"status": "finished",
"targetDate": "2026-04-13",
"updatedAt": "2026-04-19T04:58:25.870Z",
"winnerTeamName": "景涵 / RuRu"
},
{
"createdAt": "2026-04-19T05:01:10.705Z",
"groupId": 1,
"hostToken": "mo5at3g18sybc1fw",
"leftTeamName": "柏威 / RuRu",
"matchupLabel": "柏威 / RuRu vs 建喵 / 小念",
"pointLog": [],
"rightTeamName": "建喵 / 小念",
"roomId": "498013",
"scoreState": {
"scoreLeft": 0,
"scoreRight": 0,
"gamesLeft": 0,
"gamesRight": 0,
"currentGame": 1,
"targetScore": 21,
"serving": null,
"leftRightCourtPlayer": "playerA",
"rightRightCourtPlayer": "playerA"
},
"status": "live",
"targetDate": "2026-04-13",
"updatedAt": "2026-04-19T05:01:10.731Z",
"winnerTeamName": null
}
]
[]

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')