File size: 1,584 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 | import { RateLimiterMemory } from 'rate-limiter-flexible'
import { EVENTS, ERROR_CODE } from '@music-together/shared'
import type { TypedSocket } from './types.js'
const RATE_LIMIT_POINTS = 10
const RATE_LIMIT_DURATION_S = 5
/**
* Per-socket rate limiter for critical socket events.
* Shared across all controllers to prevent event spam.
*
* Separate limiter from chat (which has its own) — this covers
* VOTE_START, QUEUE_ADD, PLAYER_PLAY, PLAYER_SEEK, etc.
*/
const socketEventLimiter = new RateLimiterMemory({
points: RATE_LIMIT_POINTS,
duration: RATE_LIMIT_DURATION_S,
})
/**
* Consume a rate limit point for the given socket's user.
* Keyed by userId (not socket.id) to prevent bypass via multiple connections.
* Returns true if allowed, false if rate-limited (error already emitted).
*/
export async function checkSocketRateLimit(socket: TypedSocket): Promise<boolean> {
const key = socket.data.identityUserId ?? socket.id
try {
await socketEventLimiter.consume(key)
return true
} catch {
socket.emit(EVENTS.ROOM_ERROR, {
code: ERROR_CODE.RATE_LIMITED,
message: '操作过于频繁,请稍后再试',
})
return false
}
}
/**
* Clean up rate limiter entries for a disconnected socket.
* Call this in the disconnect handler to prevent memory growth.
*/
export function cleanupSocketRateLimit(socket: TypedSocket): void {
const key = socket.data.identityUserId ?? socket.id
socketEventLimiter.delete(key).catch(() => {
// Ignore — key may not exist if the socket never triggered a rate-limited event
})
}
|