import { hash, verify } from '@node-rs/argon2' /** * argon2 держит ~19 МиБ на вызов и занимает поток libuv; без ограничителя * шквал логинов раздувает RSS. Семафор ограничивает число одновременных хешей. */ let limit = 2 let active = 0 const waiters: Array<() => void> = [] export function setPasswordConcurrency(n: number): void { limit = Math.max(1, Math.floor(n)) } export function passwordHashStats(): { active: number; waiting: number } { return { active, waiting: waiters.length } } async function withSlot(fn: () => Promise): Promise { if (active >= limit) { await new Promise((resolve) => waiters.push(resolve)) } else { active += 1 } try { return await fn() } finally { const next = waiters.shift() if (next) next() // слот передаётся ожидающему без изменения active else active -= 1 } } export function hashPassword(password: string): Promise { return withSlot(() => hash(password)) } export function verifyPassword( passwordHash: string, password: string, ): Promise { return withSlot(() => verify(passwordHash, password)) } let dummyHash: Promise | null = null /** * Для неизвестного/отключённого пользователя выполняем настоящую проверку * по фиктивному хешу: время ответа не выдаёт существование учётки. */ export async function verifyDummyPassword(password: string): Promise { dummyHash ??= hash('dummy-password-for-timing') try { await withSlot(async () => verify(await dummyHash!, password)) } catch { /* результат не важен */ } }