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

@thejob/notify

v1.2.0

Published

Notification pipeline contract + client for The Job: domain services emit facts over Redis Streams, the notification-service turns them into mail.

Readme

@thejob/notify

The notification pipeline package for The Job: the event contract (types, a validator, a routing table), the high-level client domain services publish through, and the Redis Streams producer/consumer that carry the events.

One package, four entry points. Import only the subpath you need, and nothing else reaches your dependency graph, which is why the client is not published separately. Domain services want /client; the notification-service root uses /consumer.

What's in it

| Import | Contents | | --- | --- | | @thejob/notify | NOTIFY_EVENTS_STREAM, NotifyEvent, NotifyEventType, parseNotifyEvent, NOTIFY_ROUTES, routeFor | | @thejob/notify/client | NotificationClient, notifyKeys — see CLIENT.md | | @thejob/notify/producer | createNotifyEventsProducer | | @thejob/notify/consumer | createNotifyEventsConsumer |

/client vs /producer: the client takes a domain fact and derives the event type and idempotency key for you; the producer takes both by hand. Prefer the client unless you are emitting an event it has no method for.

The root entry is zero-dependency (types + a structural validator + the routing table), so importing it to reason about events pulls in no ioredis.

The envelope

interface NotifyEvent {
  type: NotifyEventType;      // 'group.membership_approved' | 'job.expired' | …
  id: string;                 // for logs/tracing — NOT the dedupe key
  occurredAt: string;         // ISO-8601
  recipientUserId: string;    // the user to MAIL
  idempotencyKey: string;     // derived from the immutable domain fact
  data: Record<string, unknown>;  // template variables
}

Two fields are easy to misuse:

recipientUserId is not always the actor. group.join_requested mails the group owner while the actor is the requester. Only the producing service knows that relationship, so the producer resolves it.

idempotencyKey must come from the domain fact, never Date.now() or id. Delivery is at-least-once, so a redelivered event must produce a byte-identical key; email-service's unique index on it is what actually prevents the double-send. A key containing a timestamp defeats the mechanism silently — you just get duplicate mail, with nothing in the logs.

Routing

NOTIFY_ROUTES maps each event type to its template and consent gate:

'group.membership_approved': {
  template: 'group-membership-approved',
  category: 'notifications',
  channel: 'groupActivity',
}

It is typed as Record<NotifyEventType, NotifyRoute>, so adding an event type without routing it is a compile error rather than a silently unsent email.

category must match the template's category in email-service's registry — that is what drives the compliance footer. A template registered as marketing but routed here as notifications would ship without its required footer blocks.

Durability model

The consumer is a Redis consumer group:

  • Each event is delivered to the group once; a service reads the backlog it missed while down.
  • An entry is XACK'd only after its handler resolves. A throwing handler leaves it in the Pending Entries List, so it is not lost. This is the retry that email-service's single-send endpoint does not provide.
  • A periodic XAUTOCLAIM re-drives entries stuck past claimMinIdleMs (e.g. a crash mid-handler), giving at-least-once delivery.
  • Retry is bounded. After maxDeliveries attempts (default 5) the entry is copied to thejob:notify.events.dead and ACKed. Without a bound, one permanently-failing entry is re-driven forever and, since XAUTOCLAIM works oldest-first, delays every entry behind it. Pass maxDeliveries: 0 to retry forever.

So handlers must be idempotent, and must distinguish two cases: fail → throw (gets retried); correctly decline → resolve (retrying would never help).

Producers use XADD … MAXLEN ~ 10000, so the stream self-trims.

Consumer groups are cursors

ensureGroup() creates at '0', meaning a new group name replays the entire stream. That is right for a mirror consumer but wrong for anything that sends mail. Renaming a live group has the same effect. Verify with XINFO GROUPS thejob:notify.events.

Adding an event type

  1. Add it to NotifyEventType and NOTIFY_EVENT_TYPES in src/events.ts.
  2. Add its route to NOTIFY_ROUTES in src/routing.ts (compile error until you do).
  3. Confirm the template exists in email-service's registry with a matching category.
  4. Add a notifyKeys builder and a named method in src/client/ so producers state the fact rather than hand-building the key.

There is no second copy of the contract to keep in sync: src/client/ imports the envelope and type union from src/events.ts, so the client cannot publish something the consumer's validator would reject. That used to be step 4, policed by a wire-compat.test.ts; both are gone.

Why the client is a subpath, not its own package

It was briefly two packages (@thejob/notify + @thejob/notification-client), on the theory that external producers must not pull in the consumer machinery. Subpath exports already guarantee that: importing /client reaches client/ and events.ts only. The split bought nothing and cost a hand-synced copy of the wire format, so it was merged. Both consumers had ended up installing both packages anyway.