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

postal

v3.1.0

Published

Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side.

Readme

postal

Pub/Sub message bus for JavaScript and TypeScript. Wildcard subscriptions, channel-scoped messaging, and zero dependencies.

Install

npm install postal
# or
pnpm add postal
# or
yarn add postal

Quick Start

import { getChannel } from "postal";

const orders = getChannel("orders");

// Subscribe
const unsub = orders.subscribe("item.placed", envelope => {
    console.log(envelope.payload); // { sku: "HOVERBOARD-2015", qty: 1 }
});

// Publish
orders.publish("item.placed", { sku: "HOVERBOARD-2015", qty: 1 });

// Unsubscribe
unsub();

Wildcard Subscriptions

Topics are dot-delimited strings. Two wildcards are supported:

  • * — matches exactly one segment
  • # — matches zero or more segments
const events = getChannel("events");

// Matches "user.created", "user.deleted", "user.updated"
events.subscribe("user.*", envelope => {
    console.log(envelope.topic); // e.g. "user.created"
});

// Matches "order.item.placed", "order.item.cancelled", "order.refund.issued", etc.
events.subscribe("order.#", envelope => {
    console.log(envelope.topic);
});

events.publish("user.created", { id: 42 });
events.publish("order.item.placed", { sku: "DeLorean" });

Typed Channels

Two ways to get full payload type inference on your channels.

Explicit type map

Pass your topic map as a generic to getChannel:

type OrderTopicMap = {
    "item.placed": { sku: string; qty: number };
    "item.cancelled": { sku: string; reason: string };
};

const orders = getChannel<OrderTopicMap>("orders");

// payload is typed as { sku: string; qty: number }
orders.subscribe("item.placed", envelope => {
    console.log(envelope.payload.sku);
});

// TypeScript error — "item.shipped" isn't in the topic map
orders.publish("item.shipped", { sku: "X" });

Registry augmentation

If many files share the same channel, declare the map once via module augmentation and skip the generic at every call site:

// types/postal.d.ts (or anywhere in your project)
import "postal";

declare module "postal" {
    interface ChannelRegistry {
        orders: {
            "item.placed": { sku: string; qty: number };
            "item.cancelled": { sku: string; reason: string };
        };
    }
}
import { getChannel } from "postal";

const orders = getChannel("orders"); // topic map inferred from registry

orders.subscribe("item.placed", envelope => {
    console.log(envelope.payload.sku); // typed!
});

Both approaches produce the same typed Channel. Channels not in the registry (and without an explicit type map) fall back to Record<string, unknown> — no typing required to get started.

Request / Handle (RPC)

Channels support a correlation-based request/response pattern. The requester gets a Promise; the handler's return value resolves it.

Mark topics as RPC by giving them a { request, response } shape in the registry:

declare module "postal" {
    interface ChannelRegistry {
        compute: {
            "fibonacci.calculate": {
                request: { n: number };
                response: { result: number };
            };
        };
    }
}
import { getChannel, PostalTimeoutError, PostalRpcError } from "postal";

const compute = getChannel("compute");

// Register a handler (one per topic per channel)
const unhandle = compute.handle("fibonacci.calculate", envelope => {
    const { n } = envelope.payload;
    return { result: fibonacci(n) };
});

// Send a request
try {
    const { result } = await compute.request("fibonacci.calculate", { n: 10 });
    console.log(result); // 55
} catch (err) {
    if (err instanceof PostalTimeoutError) {
        console.error(`Timed out after ${err.timeout}ms`);
    } else if (err instanceof PostalRpcError) {
        console.error(`Handler threw: ${err.message}`);
    }
}

// Remove the handler
unhandle();

Handlers can be async. The default timeout is 5000ms; pass { timeout: ms } as a third argument to request() to override it.

Wire Taps

Wiretaps observe every envelope flowing through the bus — local publishes, requests, and inbound messages from transports. Useful for logging, debugging, and analytics.

import { addWiretap } from "postal";

const removeWiretap = addWiretap(envelope => {
    console.log(`[${envelope.channel}] ${envelope.topic}`, envelope.payload);
});

// Errors thrown by wiretaps are silently swallowed — they never affect dispatch.

// Remove when done
removeWiretap();

Transports

Transports bridge postal across execution contexts — iframes, web workers, and browser tabs. Register a transport and messages flow transparently between contexts, as if everything were on the same bus.

import { addTransport } from "postal";

// Optionally filter which channels cross the boundary
addTransport(transport, { filter: { channels: ["orders", "notifications"] } });

Available transport packages:

API

| Export | Description | | ----------------------------------- | -------------------------------------------------------------------- | | getChannel(name) | Get or create a singleton channel by name | | addWiretap(callback) | Register a global observer for all envelopes | | addTransport(transport, options?) | Register a transport to bridge messages across contexts | | resetChannels() | Dispose all channels and clear all state — useful for test isolation | | resetWiretaps() | Remove all registered wiretaps | | resetTransports() | Remove all registered transports | | PostalTimeoutError | Thrown when a request() call exceeds its timeout | | PostalRpcError | Thrown when an RPC handler throws — relayed back to the requester | | PostalDisposedError | Thrown when calling methods on a disposed channel |

Documentation

Full documentation, guides, and examples at postal-js.org.

License

MIT