import assert from "node:assert/strict" import { test } from "node:test" import Fastify from "fastify" import { DatabaseRequestGate } from "./database-request-gate.js" import { installDatabaseRequestGate, registerDatabaseStreamAbort } from "../plugins/database-request-gate.js" function deferred() { let resolve!: () => void const promise = new Promise(r => { resolve = r }) return { promise, resolve } } test("exclusive operation waits for handler settlement and blocks new requests", async () => { const gate = new DatabaseRequestGate(), pending = deferred(), entered = deferred() const request = gate.request(async () => { entered.resolve(); await pending.promise }) await entered.promise let commands = 0 const operation = gate.exclusive(async () => { commands++ }) await assert.rejects(gate.request(async () => {}), /повторите запрос позже/) await assert.rejects(gate.exclusive(async () => {}), /уже выполняется/) assert.equal(commands, 0) pending.resolve(); await request; await operation assert.equal(commands, 1) await gate.request(async () => {}) }) test("drain timeout cancels command without declaring an active handler finished", async () => { const gate = new DatabaseRequestGate(), pending = deferred() const request = gate.request(() => pending.promise) let commands = 0 await assert.rejects(gate.exclusive(async () => { commands++ }, 10), /операция с базой отменена/) assert.equal(commands, 0) const retry = gate.exclusive(async () => { commands++ }) await Promise.resolve(); assert.equal(commands, 0) pending.resolve(); await request; await retry assert.equal(commands, 1) }) test("stream cancellation waits for its asynchronous cleanup", async () => { const gate = new DatabaseRequestGate(), aborted = deferred(), cleanup = deferred() const stream = gate.request(async () => { await aborted.promise; await cleanup.promise }, aborted.resolve) let commands = 0 const operation = gate.exclusive(async () => { commands++ }) await aborted.promise; assert.equal(commands, 0) cleanup.resolve(); await stream; await operation; assert.equal(commands, 1) }) test("Fastify gate covers GET, keeps health available and drains late stream registration", async () => { const app = Fastify(); installDatabaseRequestGate(app) const entered = deferred(), read = deferred(), cleanup = deferred(), command = deferred(), leave = deferred() app.get('/health', async () => ({ ok: true })) await app.register(async child => { child.get('/traffic/flow/live', async request => { entered.resolve(); await read.promise const aborted = deferred(); registerDatabaseStreamAbort(request, aborted.resolve) await aborted.promise; await cleanup.promise return { closed: true } }) child.get('/servers', async () => ({ servers: [] })) child.post('/system/database/restore', async () => { command.resolve(); await leave.promise; return { ok: true } }) }, { prefix: '/api' }) try { const stream = app.inject('/api/traffic/flow/live'); await entered.promise const operation = app.inject({ method: 'POST', url: '/api/system/database/restore' }) // inject schedules asynchronously: wait for gate to reject a subsequent GET. await new Promise(r => setTimeout(r, 10)) assert.equal((await app.inject('/api/servers')).statusCode, 409) assert.equal((await app.inject('/health')).statusCode, 200) read.resolve(); cleanup.resolve(); await stream; await command.promise assert.equal((await app.inject({ method: 'POST', url: '/api/system/database/restore' })).statusCode, 409) leave.resolve(); assert.equal((await operation).statusCode, 200) assert.equal((await app.inject('/api/servers')).statusCode, 200) } finally { read.resolve(); cleanup.resolve(); leave.resolve(); await app.close() } })