Skip to main content

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:

ChannelScopePayloadWho
x_logsnode{"seq":…,"ts":…,"level":…,"message":…} — every log-ring entryadmin
x_sessionsnode{"event":"connect"|"disconnect"|"statement", …}admin
x_databound database{"table":…,"rows":N}coalesced: at most one event per table per 250 ms window, however hot ingest runsdatabase user / bound admin
x_ddlbound database{"stmt":…} — schema changes; a migration's shadow stays silent, COMMIT MIGRATION rings oncedatabase 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):

  1. Photograph — pull the current state through your cursor (SELECT … WHERE x_rev > :watermark, or SHOW SESSIONS).
  2. ListenLISTEN x_data; and wait.
  3. 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.