import { randomUUID } from 'node:crypto'; import { Pool } from 'pg'; import { pino } from 'pino'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { PostgresSessionRegistry } from './session-registry.js'; import { collabTestDatabaseUrlOrUndefined } from './testing/test-db.js'; const url = collabTestDatabaseUrlOrUndefined; const logger = pino({ enabled: false }); /** Poll `predicate` until it is true or the timeout elapses. */ async function waitFor(predicate: () => Promise, timeoutMs = 2000): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await predicate()) return; await new Promise((resolve) => setTimeout(resolve, 20)); } throw new Error('timed out waiting for condition'); } describe.skipIf(!url)('PostgresSessionRegistry (DB-backed, issue #40)', () => { let pool: Pool; const created: string[] = []; beforeAll(() => { pool = new Pool({ connectionString: url }); }); afterAll(async () => { if (created.length > 0) { await pool.query('DELETE FROM collab_open_sessions WHERE page_id = ANY($1::text[])', [ created, ]); } await pool.end(); }); async function heartbeatOf(pageId: string): Promise { const res = await pool.query<{ heartbeat_at: Date }>( 'SELECT heartbeat_at FROM collab_open_sessions WHERE page_id = $1', [pageId], ); return res.rows[0]?.heartbeat_at ?? null; } it('records an open session and clears it on close', async () => { const pageId = randomUUID(); created.push(pageId); const registry = new PostgresSessionRegistry({ pool, logger }); registry.markOpen(pageId); await waitFor(async () => (await heartbeatOf(pageId)) !== null); expect(await heartbeatOf(pageId)).toBeInstanceOf(Date); registry.markClosed(pageId); await waitFor(async () => (await heartbeatOf(pageId)) === null); expect(await heartbeatOf(pageId)).toBeNull(); }); it('refreshes the heartbeat of open documents on its interval', async () => { const pageId = randomUUID(); created.push(pageId); const registry = new PostgresSessionRegistry({ pool, logger, heartbeatMs: 40 }); registry.markOpen(pageId); await waitFor(async () => (await heartbeatOf(pageId)) !== null); const first = await heartbeatOf(pageId); registry.start(() => [pageId]); try { await waitFor(async () => { const current = await heartbeatOf(pageId); return current !== null && first !== null && current.getTime() > first.getTime(); }); } finally { registry.stop(); } const refreshed = await heartbeatOf(pageId); expect(refreshed!.getTime()).toBeGreaterThan(first!.getTime()); }); });