function conflict(message: string): Error & { statusCode: number } { return Object.assign(new Error(message), { statusCode: 409 }) } /** Handler lifetime, not socket lifetime: disconnect does not finish an active write. */ export class DatabaseRequestGate { private paused = false private active = new Map void) | undefined>() private idle: (() => void) | undefined async request(run: () => Promise, cancelStream?: () => void): Promise { if (this.paused) throw conflict("Выполняется операция с базой приложения; повторите запрос позже") const lease = Symbol() this.active.set(lease, cancelStream) try { return await run() } finally { this.active.delete(lease) if (!this.active.size) this.idle?.() } } async exclusive(run: () => Promise, timeoutMs = 30_000): Promise { if (this.paused) throw conflict("Операция с базой приложения уже выполняется") this.paused = true let timer: ReturnType | undefined try { for (const cancel of this.active.values()) cancel?.() if (this.active.size) await new Promise((resolve, reject) => { this.idle = resolve timer = setTimeout(() => reject(conflict("Не удалось дождаться завершения API-запросов; операция с базой отменена")), timeoutMs) }) return await run() } finally { if (timer) clearTimeout(timer) this.idle = undefined this.paused = false } } }