強化房間清理與比賽中分頁限制
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user