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

event-dispatching

v0.1.0

Published

Dispatching and listening for application events in Typescript

Readme

event-dispatching

✨ This package is a modernized and extended version of pleerock/event-dispatch, refactored for personal use and better extensibility.

In-process application event bus of the server. Domain modules declare subscribers with decorators, services dispatch events through AbstractService.eventDispatching, and the dispatching calls the matching subscriber methods, resolved through the Inversify container.

It is not a queue: no persistence, retry, timeout, transaction or cross-process delivery. Deferred or retryable work belongs to the scheduler / workflows. Events only reach listeners of the same Node.js process.

Public API

| Export | Kind | Status | | --- | --- | --- | | event_subscriber() (aliases eventSubscriber, EventSubscriber) | class decorator | stable | | on(event \| events, options?) (alias On) | method decorator | stable | | EventDispatching: dispatch, asyncDispatch | service | stable | | EventDispatching: on, once, attach, detach, remove, detachAll, getActiveListeners | runtime listeners | stable, used in tests | | TYPES.EventDispatching, TYPES.MetadataRegistry | DI tokens | stable | | MetadataRegistry: init, clear, add*/has*Metadata, getEventListeners | registry | stable, internal use | | MetadataRegistry: getAllHandlers, findAllEventsHandlersByEventName | registry | deprecated | | EventMap, EventOptions, EventSubscriberOptions, IEventListenerDescriptor, IEventMetadata, ISubscriberMetadata, IEventSubscriber, IEventsHandler | types | stable (IEventSubscriber, IEventsHandler: legacy) | | DECORATOR_EVENT_SUBSCRIBER_META_KEY, DECORATOR_ON_META_KEY | metadata keys | stable | | isEmpty(value) | utility (defines which dispatch results are kept) | stable, kept for compatibility | | default export bootstrap(app), module | module entry | stable (loaded through app/modules.json) |

Everything is exported from the package root; there are no deep import paths.

Usage

1. Events and payloads (in the domain module)

Each module owns its event names and payload types. Event names are plain strings ("<module_snake>.<aggregate>.<event>"); they may be stored or logged, so never rename them.

export class UserEvents {
    static readonly USER_CREATED = "app_auth.user.created";
}

export interface UserCreatedEvent {
    user: User;
}

2. A subscriber

import { inject, injectable } from "inversify";
import { event_subscriber, on } from "event-dispatching";
import { AbstractService } from "app-common";

@injectable()
@event_subscriber()
export class CMSContentUserEventSubscriber extends AbstractService {

    constructor(@inject(TYPES.PageManager) private readonly _pageManager: PageManager) {
        super("cms_content.UserEventSubscriber");
    }

    @on(UserEvents.USER_CREATED)
    public async onUserCreated(event: UserCreatedEvent): Promise<void> {
        await this._pageManager.createDefaultPages(event.user);
    }

    // One method, several events
    @on([UserEvents.USER_UPDATED, UserEvents.USER_DELETED])
    public async onUserChanged(event: UserCreatedEvent): Promise<void> { /* ... */ }
}
  • @injectable() is required: the subscriber is resolved through the container.
  • Bind it in the module (options.bind(TYPES.XSubscriber).to(XSubscriber)), as the other modules do.
  • The file must be imported by the module (it is, through the binding), so that its decorators run before the application initializes the modules.
  • Only instance methods declared on the subscriber class are listeners. @on() on a static method, an accessor, or with an empty / non-string event name throws a TypeError when the class is defined. Stacked @on() decorators on one method add up.
  • @on() methods of a class without @event_subscriber() are ignored. Inherited @on() methods are not listeners of the subclass: decorate the subclass method too.
  • EventOptions.authRoles is stored but not read by the dispatching.

3. Dispatching

// In any AbstractService subclass
this.eventDispatching.dispatch(UserEvents.USER_CREATED, { user });

// Await the listeners and read their results
const results = await this.eventDispatching.asyncDispatch(OAuth2Events.OAUTH2_EXPIRED, { oAuth2 });

Outside AbstractService: container.get<EventDispatching>(TYPES.EventDispatching).

Dispatch guarantees

| | dispatch() | asyncDispatch() | | --- | --- | --- | | Returns | after every listener has been called | a promise, after every listener has settled | | Async listeners | started, not awaited (fire and forget) | awaited | | Concurrency | listeners called one after the other | listeners of one event started in order, then awaited together (they run concurrently); events of an array handled one after the other | | Order | runtime listeners (on/once/attach, attachment order), then subscribers in registration order, then methods in declaration order | same start order | | Listener error | reported, other listeners still run | same; the promise never rejects because of a listener | | Async rejection | reported (never an unhandled rejection) | reported | | Result | non-empty results (isEmpty), in call order; pending promises count as empty | non-empty resolved results, in call order | | No listener | no-op, [] | [] | | Throws | only if a subscriber cannot be resolved (message Error instantiating class "X": ..., container error as cause) | rejects in the same case |

  • Errors are reported with console.error("[event-dispatching] Listener <Class.method> failed for event \"<event>\":", error). The original error object (message, stack, cause) is logged as is. The payload is never logged: it can hold users, tokens or sessions.
  • Payload: every listener receives the same object reference. It is neither cloned nor frozen; do not mutate it unless the event contract says so.
  • Nested dispatch: a listener may dispatch other events. With dispatch(), the nested event is fully handled before the outer listener continues.
  • Request context: listeners run in the caller's async context, so AsyncLocalStorage (request context, sessions) is preserved, also after await in a listener.
  • Listener changes during a dispatch (attach, detach, once) apply from the next dispatch. A once listener is detached before it runs: it runs at most once, even if it throws or several dispatches run concurrently.

Lifecycle and DI

  1. Decorators run when a module file is evaluated. They append frozen records to two process-wide lists stored with reflect-metadata on Reflect (DECORATOR_EVENT_SUBSCRIBER_META_KEY, DECORATOR_ON_META_KEY).
  2. bootstrap(app) binds MetadataRegistry (singleton) and EventDispatching (container default scope, singleton in the application) and initializes the registry; onPreInit initializes it again to catch modules imported later. Initialization is idempotent.
  3. First dispatch: the registry resolves every registered subscriber with container.get(SubscriberClass) (the application container uses autobind), in registration order, then indexes the listeners by event. Instances are cached by the registry for its lifetime: a subscriber behaves as a singleton whatever its binding scope, and is a different object from container.get(TYPES.XSubscriber). A resolution failure is thrown to the dispatching's caller and retried at the next dispatch.
  4. Next dispatches read the listeners of the event from the index (no scan, no reflection). Registering new metadata rebuilds the index on the next dispatch.

Listener methods are looked up on the instance at call time, with the instance as this, so injected dependencies (this._manager, this.log, ...) are available.

Typed payloads (optional)

Payloads are any by default. A module can opt in to checked payloads by augmenting EventMap; other events keep working unchanged:

declare module "event-dispatching" {
    interface EventMap {
        "app_auth.user.created": UserCreatedEvent;
    }
}

Testing

  • Bind a fake under TYPES.EventDispatching to record dispatches (see app-auth/__test__/helpers.ts).
  • To test real subscribers, call the default bootstrap({ container }) with a new Container({ defaultScope: "Singleton", autobind: true }), then get TYPES.EventDispatching.
  • The decorator lists are process-wide: save and restore both metadata keys around tests that declare decorated classes (see __tests__/EventDispatching.behavior.test.ts).
  • registry.getEventListeners(event) lists the listeners of an event (frozen descriptors with subscriber and method names, no payload).
pnpm --filter event-dispatching exec vitest run
pnpm --filter event-dispatching exec tsc --project tsconfig.json --noEmit
pnpm --filter event-dispatching run build   # consumers read dist