pg-pubsub package
Reliable PostgreSQL LISTEN/NOTIFY for Node.js — with an inter-process lock so a horizontally scaled service handles each notification once.
Start from PgPubSub: construct it with PgPubSubOptions, subscribe channels inside its 'connect' handler, and read messages from the instance's 'message' event or from the per-channel emitter on channels.
Remarks
The problem this solves is that LISTEN/NOTIFY is a broadcast: every listening connection receives every notification, so a service scaled to N replicas handles each message N times. With singleListener on — the default — the replicas compete for a per-channel lock held as a row in PostgreSQL, and only the holder listens. The others stay connected as hot standbys.
That makes delivery at-most-once, and the trade is worth stating plainly: NOTIFY has no backlog, so anything published while no process holds the lock is gone. A clean shutdown releases the lock and a standby takes over at once; an unclean exit leaves the channel unhandled until the next retry, bounded by ACQUIRE_INTERVAL. Payloads are also capped at 8000 bytes by PostgreSQL. Where losing a message is unacceptable, pair this with a durable queue rather than replacing one.
Example
import { type AnyJson, PgPubSub } from '@imqueue/pg-pubsub';
const pubSub = new PgPubSub({ connectionString: process.env.DB_URL });
pubSub.on('connect', async () => {
await pubSub.listen('UserChanged');
});
pubSub.on('message', (channel: string, payload: AnyJson) =>
console.log(channel, payload),
);
await pubSub.connect();
await pubSub.notify('UserChanged', { id: 1 });
Classes
|
Class |
Description |
|---|---|
|
Implements no lock to be used with multi-listener approach | |
|
Implements manageable inter-process locking mechanism over existing PostgreSQL connection for a given It uses periodic locks acquire retries and implements graceful shutdown using By running inside Docker containers this would work flawlessly on implementation auto-scaling services, as docker destroys containers gracefully. Currently, the only known issue could happen only if, for example, database or software (or hardware) in the middle will cause a silent disconnect. For some period of time, despite the fact that there are other live potential listeners some messages can go into void. This time period can be tuned by bypassing wanted Usually you do not need to instantiate this class directly - it will be done by a PgPubSub instances on their needs. Therefore, you may re-use this piece of code in some other implementations, so it is exported as is. | |
|
Implements LISTEN/NOTIFY client for PostgreSQL connections. It is a basic public interface of this library, so the end-user is going to work with this class directly to solve his/her tasks. Construct it with PgPubSubOptions, subscribe channels once connected, then read messages from either the instance's own |
Functions
|
Function |
Description |
|---|---|
|
Channel listener event, occurs whenever the listening channel gets a new payload message. | |
|
| |
|
| |
|
Registers SIGINT/SIGTERM/SIGABRT handlers performing graceful release of all instantiated locks and process exit. Idempotent. Opt-in: importing this package does not take over the process lifecycle by itself - either call this function directly or construct PgPubSub with | |
|
| |
|
| |
|
| |
|
| |
|
| |
|
Serializes given input object to JSON string. On error will return serialized null value | |
|
| |
|
Constructs and returns hash string for a given set of processId, channel and payload. | |
|
| |
|
Deserializes given input JSON string to corresponding JSON value object. On error will return empty object |
Interfaces
|
Interface |
Description |
|---|---|
|
Lock implementation interface to follow | |
|
Represents logger interface suitable to be injected into this library objects | |
|
Represents JSON serializable object | |
|
Extends | |
|
Options accepted as option argument of PgPubSub constructor. It extends |
Variables
|
Variable |
Description |
|---|---|
|
How often, in milliseconds, a process without the lock retries acquiring it. Default 30000. This is the failover bound, and it is asymmetric. A clean shutdown releases the lock and the waiting processes are notified at once, so takeover is near-immediate. But if the lock holder dies WITHOUT releasing — SIGKILL, a lost container, a hard crash — nothing notifies anyone, and the channel stays unhandled until the next retry fires. Any notification published in that window is lost outright, because LISTEN/NOTIFY has no backlog to replay. So this value is the worst-case gap in message handling after an unclean exit. Lower it if that gap matters more than the polling cost of the extra lock acquisition attempts. | |
|
Hard-coded pre-set of PgPubSubOptions | |
|
Default for When on, a message is de-duplicated by CONTENT rather than by listener: a marker row is written for each handled payload, and an identical payload arriving again within UNIQUE_LOCK_TTL is skipped. That protects against duplicate publication, but it also means two legitimately identical notifications collapse into one — which is a correctness problem if your payloads are not unique. | |
|
Default for With it on, an inter-process lock elects exactly ONE process to receive each channel's notifications, which is what makes scaling out safe: LISTEN/NOTIFY delivers to every listening connection, so without the lock every replica handles every message. Turn it off only when you genuinely want a broadcast to all replicas. | |
|
Delay in milliseconds between reconnect attempts after the PostgreSQL connection drops. | |
|
How many reconnect attempts to make before emitting | |
|
Matches the internal prefix PgIpLock puts on its own coordination channels, so a listener can tell lock traffic apart from application notifications. The prefix is repeated rather than nested — hence the | |
|
PostgreSQL schema holding the inter-process lock table, read from Every process competing for the same channel must agree on this name — the lock is a row in that schema, so two deployments configured differently would each elect their own listener and both would handle the same notification. | |
|
How long, in milliseconds, PgPubSub.destroy() waits for a graceful shutdown before giving up. Read from The wait matters because releasing a lock cleanly is what lets a standby take over immediately: the release deletes the lock row, which fires a NOTIFY the waiting processes are listening for. A shutdown that is killed before the release completes falls back to ACQUIRE_INTERVAL instead. | |
|
Time-to-live (seconds) of processed-message markers created by execution locks; expired markers are cleaned up on subsequent unique lock acquisitions |
Type Aliases
|
Type Alias |
Description |
|---|---|
|
Represents any JSON-serializable value | |
|
Represents JSON-serializable array |
Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.