Skip to main content

Notifications (LISTEN/NOTIFY)

PostgreSQL's LISTEN/NOTIFY mechanism lets a session subscribe to named channels and receive asynchronous messages sent with NOTIFY or pg_notify(). postgrejs wraps this with a channel-based API on both Connection and Pool, plus a raw 'notification' event for anything that does not fit the callback model.

Listening on a Connection​

import { Connection } from 'postgrejs';

const connection = new Connection();
await connection.connect();

await connection.listen('my_channel', msg => {
console.log(msg.channel, msg.payload, msg.processId);
});

// From this or any other session:
await connection.query(`NOTIFY my_channel, 'hello'`);

listen() sends LISTEN <channel> the first time a callback is registered for that channel; further callbacks on the same channel are added locally without another round trip.

ArgumentTypeDefaultDescription
channelstring-Channel name — any name PostgreSQL accepts, up to 63 bytes. See below for how case is handled.
callbackNotificationCallback-Invoked with the NotificationMessage on every matching notification.

Channel name casing​

A channel name is a PostgreSQL identifier, and LISTEN foo and LISTEN Foo are not the same subscription: an unquoted identifier is folded to lower case by the server, so both LISTEN Foo and NOTIFY Foo really mean foo. listen()/unListen() fold a name the same way whenever it looks like it could have been written unquoted, so the key your callback is registered under and the name the server reports back always match. A mixed-case channel used to be accepted but never delivered anything, because the callback was keyed on the original casing while every incoming notification arrived as the folded one:

await connection.listen('MyChannel', msg => console.log(msg.payload));
await connection.query(`NOTIFY MyChannel, 'hi'`); // folds to 'mychannel' too - delivers

A name that could not have been written unquoted (spaces, punctuation) is not folded; it is passed through, quoted, exactly as given.

To stop receiving notifications:

await connection.unListen('my_channel'); // sends UNLISTEN my_channel
await connection.unListenAll(); // sends UNLISTEN *

unListen() is a no-op if no callback is registered for that channel. unListenAll() is also called automatically when the connection closes, so a Connection never leaves a LISTEN registered against the server after it disconnects.

The NotificationMessage shape​

interface NotificationMessage {
processId: number; // backend PID of the NOTIFY-ing session
channel: string;
payload: string;
}

type NotificationCallback = (msg: NotificationMessage) => any;

The raw 'notification' event​

Every notification the connection receives - regardless of whether anything is listen()-ing for it - is also emitted as a 'notification' event on the Connection:

connection.on('notification', msg => {
console.log(`[${msg.processId}] ${msg.channel}: ${msg.payload}`);
});

This fires even for channels you never called listen() for (for example if you issued LISTEN yourself with a raw query() call), while listen() callbacks only fire for their own channel.

Listening on a Pool​

Calling pool.listen() does not borrow one of the pool's regular connections: it opens and keeps one dedicated internal Connection just for LISTEN, created lazily the first time listen() is called. That connection is separate from acquire()/release() traffic, so subscriptions survive however many queries the rest of the pool runs; you do not need to keep a connection acquired to keep listening.

import { Pool } from 'postgrejs';

const pool = new Pool();

await pool.listen('my_channel', msg => {
console.log(msg.payload);
});

// Any pooled connection can trigger it:
await pool.query(`NOTIFY my_channel, 'hello from the pool'`);

pool.unListen(channel) and pool.unListenAll() mirror the Connection methods. Once the last channel is unregistered, the pool closes its internal notification connection; pool.close() also tears it down.

If the notification connection drops unexpectedly (network blip, server restart), the pool reconnects automatically after a short delay and re-issues LISTEN for every channel that still has callbacks registered, so subscriptions resume without any action from the caller.

MethodDescription
pool.listen(channel, callback)Registers callback and opens the internal notification connection if needed.
pool.unListen(channel)Removes callback(s) for channel; closes the internal connection if no channel remains.
pool.unListenAll()Removes every listener and closes the internal connection.

A full round-trip​

import { Connection, Pool } from 'postgrejs';

const listener = new Pool();
const notifier = new Connection();
await notifier.connect();

await listener.listen('orders', msg => {
const order = JSON.parse(msg.payload);
console.log('New order:', order.id);
});

// Elsewhere in the app, or in a trigger via pg_notify():
await notifier.query(
`select pg_notify('orders', $1)`,
{ params: [JSON.stringify({ id: 42 })] },
);

pg_notify(channel, payload) is preferable to the NOTIFY statement when the channel name or payload is a parameter, since NOTIFY does not accept bind parameters for the channel name.

See API: Connection and API: Pool for full method signatures.