Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import { Client, Pool } from "pg";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { PostgresRealtimeFanout, type RealtimeListenerClient } from "./realtime.js";
|
|
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
const describePostgres = process.env.VERIFY_DATABASE && databaseUrl ? describe : describe.skip;
|
|
|
|
describePostgres("PostgresRealtimeFanout (PostgreSQL contract)", () => {
|
|
it("reconnects a real listener and signals durable catch-up for a missed notification", async () => {
|
|
const pool = new Pool({ connectionString: databaseUrl! });
|
|
const table = `realtime_contract_events_${process.pid}_${Date.now()}`;
|
|
const listenerClients: Client[] = [];
|
|
const signals: string[] = [];
|
|
const seen: string[] = [];
|
|
let cursor = 0;
|
|
let drainError: unknown;
|
|
let draining = Promise.resolve();
|
|
|
|
await pool.query(`CREATE TABLE ${table} (id bigserial PRIMARY KEY, body text NOT NULL)`);
|
|
const realtime = new PostgresRealtimeFanout({
|
|
connectionString: databaseUrl!,
|
|
publisher: {
|
|
query: async (sql, values) => pool.query(sql, values),
|
|
},
|
|
clientFactory: () => {
|
|
const client = new Client({ connectionString: databaseUrl! });
|
|
listenerClients.push(client);
|
|
return client as RealtimeListenerClient;
|
|
},
|
|
reconnectBaseMs: 100,
|
|
reconnectMaxMs: 100,
|
|
random: () => 0,
|
|
});
|
|
|
|
try {
|
|
const unsubscribe = await realtime.subscribe("thread:contract", (signal) => {
|
|
signals.push(signal);
|
|
draining = draining
|
|
.then(async () => {
|
|
const result = await pool.query<{ id: string; body: string }>(
|
|
`SELECT id, body FROM ${table} WHERE id > $1 ORDER BY id`,
|
|
[cursor],
|
|
);
|
|
for (const row of result.rows) {
|
|
cursor = Number(row.id);
|
|
seen.push(row.body);
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
drainError = error;
|
|
});
|
|
});
|
|
|
|
await vi.waitFor(() => expect(signals).toEqual([""]), { timeout: 5_000 });
|
|
const pidResult = await listenerClients[0]!.query<{ pid: number }>(
|
|
"SELECT pg_backend_pid() AS pid",
|
|
);
|
|
const listenerPid = pidResult.rows[0]!.pid;
|
|
|
|
await pool.query(`INSERT INTO ${table} (body) VALUES ($1)`, ["notified-event"]);
|
|
await realtime.publish("thread:contract", "durable-event-available");
|
|
await vi.waitFor(() => expect(seen).toEqual(["notified-event"]), { timeout: 5_000 });
|
|
|
|
await pool.query("SELECT pg_terminate_backend($1)", [listenerPid]);
|
|
await pool.query(`INSERT INTO ${table} (body) VALUES ($1)`, ["missed-while-disconnected"]);
|
|
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(listenerClients.length).toBeGreaterThanOrEqual(2);
|
|
expect(signals.filter((signal) => signal === "")).toHaveLength(2);
|
|
expect(seen).toEqual(["notified-event", "missed-while-disconnected"]);
|
|
},
|
|
{ timeout: 10_000, interval: 25 },
|
|
);
|
|
await draining;
|
|
expect(drainError).toBeUndefined();
|
|
expect(signals).toContain("durable-event-available");
|
|
await unsubscribe();
|
|
} finally {
|
|
await realtime.close();
|
|
await pool.query(`DROP TABLE IF EXISTS ${table}`).catch(() => undefined);
|
|
await pool.end();
|
|
}
|
|
});
|
|
});
|