zanith

LISTEN / NOTIFY

Postgres' built-in pubsub. The engine ships three helpers — listen, unlisten, notify — that produce CompiledQuery objects you hand to an adapter. Channel names are quoted as identifiers; payloads are bound as parameters.

The three helpers

ExportSQL
listen(channel)LISTEN ""
unlisten(channel)UNLISTEN ""
unlisten('*')UNLISTEN * — drop every subscription on this connection.
notify(channel, payload?)SELECT pg_notify($1, $2) — payload is bound as a parameter.

notify uses pg_notify(channel, payload) rather than the bare NOTIFY channel, 'payload'syntax — the function form lets us bind the payload as a real parameter, which the bare form doesn't support.

Subscribing

TS
import { listen, unlisten } from 'zanith';
 
// Subscribe — runs on whichever connection the adapter hands out.
await adapter.execute(listen('user_changes'));
 
// The adapter is responsible for surfacing notifications. With pg, the
// underlying client emits a 'notification' event:
const client = adapter.rawClient(); // pg-specific helper
client.on('notification', (msg) => {
console.log(`channel=${msg.channel} payload=${msg.payload}`);
});
 
// Stop listening:
await adapter.execute(unlisten('user_changes'));
// Or drop everything on this connection:
await adapter.execute(unlisten('*'));

Publishing

TS
import { notify } from 'zanith';
 
// Wake every subscriber to 'user_changes'. Payload is optional.
await adapter.execute(notify('user_changes'));
 
// Carry a payload — bound as a parameter, not interpolated.
await adapter.execute(notify('user_changes', JSON.stringify({ userId: 42, action: 'updated' })));
 
// Inside a transaction — recipients see the notification only after COMMIT.
await db.transaction(async (tx) => {
await tx.user.update({ where: { id: 42 }, data: { lastSeen: new Date() } });
await tx.executeRaw(...buildSqlForNotify('user_changes', '{"userId":42}'));
});

Postgres queues notifications until the enclosing transaction commits. If you want the wake-up to fire alongside the data change, send the notify from inside the same transaction.

Payload size

Postgres caps notification payloads at 8000 bytes. Use the payload to carry an id and a hint, then have the subscriber fetch the full record — never try to ship a row through it.

TS
// Right.
notify('user_changes', JSON.stringify({ id: userId, op: 'updated' }));
 
// Wrong — will throw above 8KB and is fragile even below.
notify('user_changes', JSON.stringify(fullUserRecord));