File size: 2,188 Bytes
00a912e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { nanoid } from 'nanoid'
import { config } from '../config.js'

interface RejoinTicket {
  token: string
  roomId: string
  userId: string
  expiresAt: number
}

const tickets = new Map<string, RejoinTicket>()
const roomUserIndex = new Map<string, Set<string>>()

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<string>()
  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)
  }
}