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

@aria-framework/notify

v0.3.1

Published

Aria App Framework — notify module. In-app notifications: ownership-scoped rows, a per-process SSE hub for live delivery, and a fire-and-forget emitter that needs no request and no actor, so a scheduler can raise one.

Readme

@aria-framework/notify

In-app notifications: durable rows, live delivery to open browsers, and an emitter that a scheduler can call.

Three pieces, in three places, because that is where they have to run:

| | thread | what it is | |---|---|---| | createStore | worker | the SQL, ownership-scoped by user_id | | createHub | main | open SSE responses, in memory | | createEmitter | main | create then push, fire-and-forget |

// worker
const store = notify.createStore({ getDb });          // expose as a model the main thread invokes

// main
const hub = notify.createHub();
const { emit, touch } = notify.createEmitter({ invoke: dbClient.invoke, hub, logger });

emit([userId], { type: 'invoice.overdue', title: 'Invoice #412 is overdue',
                 link: '/invoices/412', entity_type: 'invoice', entity_id: 412 });

No request, no actor

Every emitter in the app this came from took a req and derived the actor from req.session.user. That is right for a reply, an assignment, a mention — a person did something. It is wrong for a scheduler deciding an invoice is overdue, or a webhook recording a payment: no session, no name, nobody to put in front of the title.

So the core takes user ids and a payload; an actor is an optional string. A request-shaped convenience belongs in the app as a thin wrapper. A system-raised notification is then the ordinary case rather than a special one — had this required a request, the scheduler would have grown a second emitter beside it, which is the duplication the extraction exists to prevent.

Fire and forget

emit() returns a promise for tests and for callers that genuinely want to wait, and it never rejects. A notification is never more important than the action that caused it: a bell that fails must not fail a reply or a payment. Failures are logged and that is all. A synchronously throwing invoke is caught too, which the obvious .catch() version misses.

Ownership scoping IS the authorisation

Every read and every write carries WHERE user_id = ?. An id you do not own matches zero rows — the same answer as an id that does not exist, so it leaks nothing either. There is no separate permission check to forget, and moving that scope to a caller-side if would be a silent privilege escalation rather than a visible break. Read it as a constraint on future edits.

Two channels

  • users — "tell these RECIPIENTS something happened". The bell.
  • entities — "tell whoever is LOOKING at this record that it changed". touch(type, id).

The second is not a nicety. Pushes on the first go to recipients, so someone reading a record they do not watch is told nothing — which is the ordinary state of an unassigned ticket or an unowned invoice, and exactly the one most likely to be open when it changes.

Both events are contentless: a type and an id. The client re-fetches through its own permission-checked route, so neither channel can leak a title or a body even if a subscription were wrong, and the app's own view permission stays the single place access is decided. One consumer briefly included an actorUserId "so a viewer can ignore its own action"; no client ever read it, so its only effect was disclosing a staffer's user id to every other viewer.

A push reaches the page that CAUSED it — including mid-navigation

Since there is no actorUserId, a client that must not react to its own change has to know when it made one. That is easy to get wrong in one specific way, and a consumer shipped the mistake:

A form POST is a navigation, but the browser keeps the old page on screen — and its stream open — until the response arrives. So a push written while handling that POST is delivered to the very page that caused it, which then re-fetches, sees its own change, and announces it. The redirect lands a moment later and replaces everything. The consumer's code carried a comment asserting the opposite: that a page which redirects "never sees an event for its own action".

The client-side fix is small — set a flag on submit and ignore events while it is set — but it has to be deliberate, and beforeunload is too late (it fires once the response is already arriving). This is not something the hub can do for you: it writes to open responses and has no idea which one issued the request.

There is also no replay. pushEntity writes to whoever is connected at that instant, so an event a page misses while navigating is simply gone — which is why suppressing it costs nothing.

THE LIMIT: one process

The hub is in memory and per-process. Two app processes behind a load balancer each hold half the connections, so a push from one reaches only its own half — silently, with no error anywhere. Correct for a single process, which is what the current consumers run. A multi-process deployment needs a shared bus and this module is not it.

Stated here rather than discovered, because nothing about the failure looks like a failure.

What stays the app's

Who receives a notification, what the types are called, what the copy says, the migration, the routes, and whether any of it needs a permission. "Who cares about this thing" is domain knowledge — the answer is "the ticket's watchers" in one app and "whoever owns the invoice" in another.

SCHEMA is exported as the column list this module reads and writes. Your migration owns the table and may add to it; nothing here reads a column it was not given.

Wire hub.closeAll() into graceful shutdown. An SSE response is a request that never finishes, so server.close() otherwise waits for a drain that never comes.