import { nanoid } from 'nanoid' import { config } from '../config.js' interface RejoinTicket { token: string roomId: string userId: string expiresAt: number } const tickets = new Map() const roomUserIndex = new Map>() function roomUserKey(roomId: string, userId: string): string { return `${roomId}:${userId}` } function pruneExpired(): void { const now = Date.now() for (const [token, ticket] of tickets) { if (ticket.expiresAt <= now) { tickets.delete(token) const key = roomUserKey(ticket.roomId, ticket.userId) const set = roomUserIndex.get(key) if (set) { set.delete(token) if (set.size === 0) roomUserIndex.delete(key) } } } } export function issueRejoinTicket(roomId: string, userId: string): { token: string; expiresAt: number } { pruneExpired() const token = nanoid(36) const expiresAt = Date.now() + config.rejoin.ttlMs const ticket: RejoinTicket = { token, roomId, userId, expiresAt } tickets.set(token, ticket) const key = roomUserKey(roomId, userId) const set = roomUserIndex.get(key) ?? new Set() set.add(token) roomUserIndex.set(key, set) return { token, expiresAt } } export function consumeRejoinTicket(token: string, roomId: string, userId: string): boolean { pruneExpired() const ticket = tickets.get(token) if (!ticket) return false if (ticket.roomId !== roomId || ticket.userId !== userId) return false tickets.delete(token) const key = roomUserKey(roomId, userId) const set = roomUserIndex.get(key) if (set) { set.delete(token) if (set.size === 0) roomUserIndex.delete(key) } return true } export function revokeRejoinTickets(roomId: string, userId: string): void { const key = roomUserKey(roomId, userId) const set = roomUserIndex.get(key) if (!set) return for (const token of set) tickets.delete(token) roomUserIndex.delete(key) } export function cleanupRoomRejoinTickets(roomId: string): void { for (const [key, set] of roomUserIndex) { if (!key.startsWith(`${roomId}:`)) continue for (const token of set) tickets.delete(token) roomUserIndex.delete(key) } }