Pub/Sub — LISTEN / NOTIFY
xcon-db carries a pub/sub backbone and exposes it with PostgreSQL's own syntax, over the same pg-wire connection your queries use:
LISTEN jobs; -- subscribe this connection
NOTIFY jobs, 'payload'; -- publish (from any session in the database)
UNLISTEN jobs; -- or UNLISTEN * for everything
Notifications arrive as standard asynchronous messages — psql prints
them, pgx exposes WaitForNotification, libpq PQnotifies. No extra
port, no extra client.
Events are wakeup signals
An event tells you that something changed, never what the truth is.
There is no replay: a connection that was away pulls the current state
first (SHOW SESSIONS, SHOW LOGS, a feed query by x_rev), then
listens. A slow consumer's events are dropped rather than ever slowing
a publisher — correctness always lives in the pull that follows the
wakeup, so a dropped event costs staleness, not wrongness.
System channels
The x_ prefix belongs to the engine — you can LISTEN to these, never
NOTIFY them:
| Channel | Scope | Payload | Who |
|---|---|---|---|
x_logs | node | {"seq":…,"ts":…,"level":…,"message":…} — every log-ring entry | admin |
x_sessions | node | {"event":"connect"|"disconnect"|"statement", …} | admin |
x_data | bound database | {"table":…,"rows":N} — coalesced: at most one event per table per 250 ms window, however hot ingest runs | database user / bound admin |
x_ddl | bound database | {"stmt":…} — schema changes; a migration's shadow stays silent, COMMIT MIGRATION rings once | database user / bound admin |
User channels
Any other channel name is yours. Channels are database-local: the
tenancy wall holds for events exactly as it does for rows — NOTIFY jobs in one database is invisible to every other, whatever the name.
Channels come to exist on first use; LISTEN before the first NOTIFY
works.
-- session A (worker)
LISTEN jobs;
-- session B (producer)
NOTIFY jobs, '{"kind":"import","file":"batch-7.csv"}';
The sync pattern
The intended shape for cache/sync clients (this is how xcon studio's monitors work):
- Photograph — pull the current state through your cursor
(
SELECT … WHERE x_rev > :watermark, orSHOW SESSIONS). - Listen —
LISTEN x_data;and wait. - On wakeup, pull again — the event carries no rows on purpose; your cursor does the truthful reading.
A reconnect just repeats step 1 — nothing is lost, because nothing was entrusted to the stream.