npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pg-event-bus

v1.0.0

Published

PostgreSQL event bus

Readme

pg-event-bus

Typed realtime events over PostgreSQL LISTEN and NOTIFY.

Use this package to propagate cache invalidations and live updates between application processes.

The package owns a dedicated, reconnecting LISTEN connection while your application controls the connection used to publish notifications.

Install

Node.js 22 or newer is required.

npm install pg-event-bus

Create a bus

Creating a bus immediately starts its dedicated PostgreSQL listener.

Provide a publish function that executes pg_notify through your application's database client or ORM.

import { createPgEventBus } from "pg-event-bus"

// Use your application's current database connection.
import { db } from "#db"

const eventBus = createPgEventBus({
  connectionString: databaseUrl,
  channel: "my-app",
  async publish({ channel, payload }) {
    await db.sql`SELECT pg_notify(${channel}, ${payload})`
  },
})

Define an event channel

An event channel maps a typed application key to an internal event name.

The following channel uses a post ID as its key and accepts one payload type.

interface CommentEvent {
  eventType: "created" | "updated" | "deleted"
  commentId: string
}

export const commentEvents = eventBus.defineEventChannel<CommentEvent>(
  postId => `post:${postId}:comment`,
)

Event keys are strings by default. Pass a second generic argument only when a channel uses another key type.

eventBus.defineEventChannel<ChatEvent, SessionKey>(
  session => `chat:${session.userId}:${session.scope}`,
)

Send and receive events

Call send() to publish an event.

await commentEvents.send(postId, {
  eventType: "created",
  commentId,
})

Call on() to consume matching events as an AsyncIterable.

for await (const event of commentEvents.on(postId, signal)) {
  console.log(event.commentId)
}

Pass an AbortSignal to stop the stream when its consumer disconnects.

Listener readiness

Creating a bus starts its dedicated listener in the background.

eventBus.ready resolves after the listener has connected and executed LISTEN. Sending does not depend on this promise because send() uses the injected publish function.

Await it during startup only when a receiving process must not report itself as ready before it can receive notifications. Notifications published before LISTEN becomes active can be missed.

await eventBus.ready

Shutdown

Call close() during application shutdown to stop the PostgreSQL listener and complete active event streams.

await eventBus.close()

Transactions

send() waits for the injected publish function, so always await it.

To make notification delivery depend on a transaction, call send() through a publisher that uses that transaction's connection.

await db.transaction(async () => {
  await updateComment()
  await commentEvents.send(postId, event)
})

PostgreSQL delivers the notification after commit and discards it on rollback.

Dependency injection

Skip this section if your application uses the concrete eventBus directly.

With dependency injection, domain modules usually outlive a particular event bus binding. Create their channel factory from a function that resolves the current EventBus instead of binding channels to one instance.

The following example uses ripple-di, but the same resolver pattern works with another DI container.

Install it separately to follow this example.

npm install ripple-di

Application wiring

Register the production PostgreSQL bus and create the shared defineEventChannel function from its resolver.

import {
  createEventChannelFactory,
  createPgEventBus,
} from "pg-event-bus"
import { defineDependency } from "ripple-di"

export const useEventBus = defineDependency(
  () => createPgEventBus({
    connectionString: databaseUrl,
    channel: "my-app",
    async publish({ channel, payload }) {
      await db.sql`SELECT pg_notify(${channel}, ${payload})`
    },
  }),
  {
    dispose: eventBus => eventBus.close(),
  },
)

export const defineEventChannel = createEventChannelFactory(useEventBus)

Domain channel

Domain modules import the shared factory instead of the concrete PostgreSQL bus.

import { defineEventChannel } from "#events"

export const commentEvents = defineEventChannel<CommentEvent>(
  postId => `post:${postId}:comment`,
)

Test override

Tests can replace the dependency without recreating domain channels that were declared when their modules loaded.

import { provide, withOverrides } from "ripple-di"
import type { EventBus } from "pg-event-bus"

const sent: Array<{ event: string; payload: unknown }> = []

const testEventBus: EventBus = {
  async send(event, payload) {
    sent.push({ event, payload })
  },
  async *on() {},
}

await withOverrides(
  provide(useEventBus, testEventBus),
  () => commentEvents.send("post-id", {
    eventType: "created",
    commentId: "comment-id",
  }),
)

createEventChannelFactory calls useEventBus() when send() or on() runs, not when commentEvents is declared. The operation therefore uses the scoped test binding installed by withOverrides.

Delivery semantics

This package provides best-effort realtime signaling, not a durable queue. Disconnected listeners miss events, reconnects do not replay history, and delivery is not exactly once.

Use an outbox, job queue, or durable broker when every event must be processed.

Payloads use JSON serialization and must be JSON-serializable. Keep them small because PostgreSQL notification payloads must be shorter than 8000 bytes with the default configuration.

PostgreSQL can also coalesce identical channel-and-payload notifications emitted within one transaction.