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

@spirex/mediator

v1.0.0

Published

Lightweight mediator for JavaScript and TypeScript that simplifies event-driven architecture, CQRS, and decoupled application design.

Readme

Mediator for JS/TS

NPM Type Definitions NPM Version GitHub License Codecov

@spirex/mediator is a lightweight mediator for JavaScript and TypeScript that simplifies event-driven architecture, CQRS, and decoupled application design.

The library focuses on decoupling application layers by removing direct dependencies between senders and receivers. Instead of calling services or modules directly, application parts communicate through requests and events handled by the mediator.

This approach helps to:

  • reduce tight coupling between modules
  • make application flow easier to reason about
  • simplify refactoring and feature evolution
  • improve testability of business logic

Installation

You can install the package using any popular package manager.

# NPM
npm install @spirex/mediator
# YARN
yarn add @spirex/mediator

Requirements & compatibility

The library has zero runtime dependencies.

It relies on the following built-in JavaScript features: Promise, Map, Set.

These APIs are available in all modern JavaScript environments (ES2015+). If you need to support very old runtimes (for example, legacy browsers), you may need to provide appropriate polyfills.

Module formats

The package supports multiple module systems out of the box:

  • ES Modules (ESM / EJS) – modern standard for JavaScript modules using import and export.
  • CommonJS (CJS) – Node.js module system using require and module.exports.
  • UMD (Universal Module Definition) – works in both browsers and Node.js, compatible with AMD and global scripts.

This makes it usable in modern bundlers, Node.js environments, and browser-based setups.

TypeScript support

Although the library is written in pure JavaScript, it is fully and strictly typed.

  • All public APIs are covered by TypeScript type definitions;
  • No additional setup is required;
  • Works seamlessly in both JS and TS projects.

Quick Start

This example demonstrates the core concepts of the mediator:

  • how to define requests and events;
  • how to handle requests using pure, stateless handlers;
  • how to publish and subscribe to events.
import { defineRequest } from "@spirex/mediator";

type Task = {
    id: number;
    status: 'todo' | 'work' | 'done';
    priority: number;
    summary: string;
}

// Define request with payload type
const CreateTask = defineRequest<Task, {
    summary: string;
    priority: number;
}>();

// Define event to notify about new tasks
const TaskCreated = defineEvent<Task>();

// Create handler for 'CreateTask' request
const CreateTaskHandler = createHandler(
    CreateTask,
    async ({ payload, mediator }) => {
        const task: Task = {
            id: generateId(),
            status: 'todo',
            priority: payload.priority,
            summary: payload.summary,
        };
        await repo.saveTask(task);

        // Notify about new task
        mediator.publish(TaskCreated(task));
        return task;
    },
)

// Create mediator via builder
const mediator = mediatorBuilder()
    .registerHandler(CreateTaskHandler)
    .build()

// Subscribe on new tasks
const dispose = mediator.on(
    TaskCreated,
    ({ payload: task }) => {
        console.log("New task:", task.summary);
    },
)

// Send request to create new task
const newTask = await mediator.send(CreateTask({
    summary: "Make profile screen",
    priority: 5,
}));

Working with the Mediator

The mediator is responsible for delivering messages inside the application.

There are two types of messages:

  • Events — notifications about something that already happened
  • Requests — messages that expect a single result

They serve different purposes and are handled differently.

Mediator Events

What is an Event?

An event represents a fact that has already happened in the system.

Events are used only for notification. They do not return results and do not affect the execution flow of the sender.

Typical examples: "user authorized", "task created", "payment completed", "connection lost".

Defining an Event

An event defines the shape of its payload.

import { defineEvent } from "@spirex/mediator";

const UserAuthorized = defineEvent<{ userId: string }>();

If an event does not carry any data, the payload type can be omitted:

const AppStarted = defineEvent();

Publishing an Event

Events are published through the mediator:

mediator.publish(UserAuthorized({ userId: "123" }));

Important characteristics:

  • publish is synchronous;
  • it does not return anything;
  • it does not require awaiting;
  • events do not need to be registered in the mediator.

Subscribing to an Event

You can subscribe to an event using on method:

const dispose = mediator.on(UserAuthorized, ({ payload, mediator }) => {
    console.log("Authorized:", payload.userId);
});

To unsubscribe, call the returned function: dispose()

You can also subscribe only once:

mediator.once(AppStarted, () => {
    console.log("Application started");
});

Access to the Mediator inside Event Listeners

Every event listener receives a reference to the mediator instance. This allows listeners to trigger side effects in a controlled way:

  • send requests;
  • publish more specific events;
  • coordinate application behavior without tight coupling.
mediator.on(UserAuthorized, async ({ payload, mediator }) => {
    // Load additional data after authorization
    const profile = await mediator.send(
        LoadUserProfile({ userId: payload.userId }),
    );
    userStore.setProfile(profile)

    // Notify that the profile is ready
    mediator.publish(UserProfileReady(profile));
});

Because listeners have access to the mediator:

  • events can remain simple and declarative;
  • complex flows can be built by composition, not branching logic;
  • features can evolve independently without modifying the event source.

Event Listeners Execution Model

  • Event listeners can be synchronous or asynchronous;
  • Each listener is executed independently in its own microtask.

This guarantees that:

  • an error in one listener does not affect others;
  • an error does not propagate back to the event source.

Mediator Requests

What is a Request?

A request represents an explicit operation that produces a result.

Unlike events, which only notify that something happened, a request is used when the caller expects a concrete value and wants to continue execution based on that value. Requests model actions such as creating entities, loading data, performing calculations, or interacting with external systems.

From the caller’s perspective, sending a request is very similar to calling a function. The key difference is that the caller does not know who handles the request or how it is handled.

Defining a Request

A request definition describes two things:

  • the type of the result it produces;
  • the type of data required to execute it.
import { defineRequest } from "@spirex/mediator";

// defineRequest<Result, Payload>()
const CreateUser = defineRequest<User, {
    name: string;
    email: string;
}>();

If no payload is required, the payload type can be omitted:

const GetCurrentUser = defineRequest<User | null>();

The returned value is a request type, not the request itself. Calling it creates a request instance that can be sent through the mediator.

Handling a Request

Every request must have exactly one handler.

A handler defines how the request is executed and how the result is produced. It is created by binding a request type to a handling function.

import { createHandler } from "@spirex/mediator";

const CreateUserHandler = createHandler(
    CreateUser,
    async ({ payload, mediator, abortSignal }) => {
        const user = await repo.createUser(payload);
        return user;
    }
);

The handler function receives a context object. This context contains the request payload, a reference to the mediator, and an optional AbortSignal.

Because the mediator reference is available, a handler can:

  • publish events;
  • send other requests;
  • coordinate more complex flows.

Handlers are expected to be stateless. Any required dependencies should be provided externally, for example via factories or dependency injection.

Registering Handlers

Handlers are registered when building the mediator:

const mediator = mediatorBuilder()
    .registerHandler(CreateUserHandler)
    .build();

Only one handler can exist for a given request type. If another handler for the same request is registered, it replaces the previous one.

Sending a Request

A request is executed by sending it through the mediator:

const user = await mediator.send(
    CreateUser({ 
        name: "Admin",
        email: "[email protected]",
    }),
);

Sending a request always returns a Promise, even if the handler itself is synchronous. This guarantees a consistent execution model and avoids ambiguity at the call site.

If the handler throws an error or rejects, the error is propagated back to the sender. This makes request errors part of normal business logic and allows them to be handled explicitly by the caller.

Cancelling a Request with AbortSignal

When sending a request, you can optionally provide an AbortSignal.

const controller = new AbortController();

await mediator.send(
    DeleteUser({ withName: "Admin" }),
    controller.signal,
);

The provided signal is passed to the request handler through the execution context.

const CreateUserHandler = createHandler(
    CreateUser,
    async ({ payload, abortSignal, mediator }) => {
        const user = await repo.findUserByName(
            mediator.withName,
        );

        if (abortSignal?.aborted) return false;

        const wasDeleted = await repo.deleteUser(
            payload.with,
            { signal: abortSignal },
        );
        return wasDeleted;
    }
);

This allows the handler to react to cancellation and stop execution if the operation has not yet completed. Because AbortSignal is a standard API, it can be used to cancel HTTP requests, timeouts, or any other abortable operation.

If a request handler sends other requests using the same signal, cancellation naturally propagates through the entire execution chain.

This is especially useful when a request represents a complex operation composed of multiple steps. By aborting the root request, the whole chain can be interrupted consistently, without leaving the system in a partially completed state.

Cancellation is cooperative. It is the handler’s responsibility to observe the signal and decide how to react, including rolling back changes if necessary.

Best Practices

This section contains general recommendations that help keep mediator-based code clear, predictable, and easy to evolve.

Naming Requests and Events

Requests should be named as explicit actions and usually start with a verb.

Good examples: CreateUser, UpdateProfile, LoadBooks, GetBookById.

Events should describe something that already happened and are typically named in past tense.

Good examples: UserCreated, ProfileUpdated, BooksLoaded, PaymentFailed.

Clear naming makes message intent obvious without looking at the handler implementation.

Commands & Queries (CQRS)

The library allows you to define Commands and Queries, which are special types of Requests. This aligns well with the CQRS (Command Query Responsibility Segregation) pattern, where:

  • Commands — represent operations that change data (e.g., creating or updating a resource).
  • Queries — safe operations that read data without modifying it (e.g., fetching a list of users).

You can reflect this either in naming:

export const CreateUserCommand =
    defineRequest<User, { name: string }>()

export const CreateTaskCommand =
    defineRequest<Task, { summary: string, priority: number}>()

export const FindUserPurchasesQuery = 
    defineRequest<readonly Purchase[], { userId: number }>()

export const LoadRemoteConfigQuery =
    defineRequest<RemoteConfig>()

Grouping Definitions

Requests and events are usually grouped by feature, not by type.

Instead of collecting all requests or all events in a single file, keep them close to the domain they belong to.

// FILE: user/contracts.ts
export const UserCommand = {
    create: defineRequest<...>(),
    update: defineRequest<...>(),
    delete: defineRequest<...>(),
};
// mediator.send(UserCommand.create({ name: "Admin" }))

export const UserQuery = {
    getById: defineRequest<...>(),
    find: defineRequest<...>(),
}
// mediator.send(UserQuery.find({ group: 'vip' }))

export const UserEvent = {
    created: defineEvent<User>(),
    updated: defineEvent<User>(),
}
// mediator.publish(UserEvent.updated(user))

This reduces cognitive load and helps features evolve independently.

Stateless Handlers

Request handlers should be stateless functions.

They should not store data internally or rely on shared mutable state. Any required dependencies (repositories, gateways, services) should be provided externally, typically via factories or dependency injection.

Exceptions in Handlers & Listeners

Errors thrown inside request handlers are considered part of business logic.

They propagate back to the sender and should be handled where the request is sent. This makes failures explicit and keeps error handling predictable.

Event listener errors, on the other hand, are isolated and should be handled via onEventError.