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

comes

v0.0.4

Published

Simple Event System Communication

Readme

comes - Communication Event System

A simple, lightweight event system for TypeScript/JavaScript applications. It provides address-based pub/sub communication with built-in value caching, lazy loaders, and interceptors.

Installation

npm install comes

Quick Start

import { es } from 'comes';

// Send a value to an address
await es.send('user/name', 'Alice');

// Listen for values on an address
const unregister = es.listen('user/name', (value) => {
  console.log(value); // "Alice" (immediately, from cache)
});

// Stop listening
unregister();

Core Concepts

  • Address: A string key that identifies an event channel (e.g. 'user/name', 'app/status').
  • Value caching: The last value sent to each address is cached. When a new listener registers, it immediately receives the cached value.
  • Interceptors: Middleware functions that can transform, validate, or monitor values before they reach listeners.
  • Loaders: Lazy data fetchers that run automatically when the first listener registers on an address that has no value yet.

API

Importing

// Use the default singleton instance
import { es } from 'comes';

// Or create your own isolated instance
import { EventSystem } from 'comes';
const es = new EventSystem();

send<T>(id: string, event: T): Promise<T>

Sends a value to all listeners of the given address. Interceptors are executed first (in order), then each listener is called with the resulting value. The final value (after interceptors) is cached.

Returns the value after interceptor transformations.

await es.send('counter', 1);
await es.send('user/profile', { name: 'Alice', age: 30 });

If an interceptor or listener throws an error, send will throw that error.


listen(id: string, listener: EventListenerType): () => void

Registers a listener on the given address. Returns an unregister function to remove the listener later.

Behavior on registration:

  • If a value was already sent to this address, the listener is immediately called with the cached value.
  • If no value exists but a loader is configured, the loader is triggered to fetch the initial value.
  • If neither exists, the listener simply waits for the next send.
// Basic usage
const unregister = es.listen('counter', (value) => {
  console.log('Counter:', value);
});

// Later, stop listening
unregister();

unlisten(id: string, listener: EventListenerType): void

Removes a specific listener from the given address. This is useful when you need to unregister inside the listener itself, where the unregister function returned by listen may not be assigned yet.

const listener = (value: string) => {
  es.unlisten('my-event', listener); // safe to call here
  console.log('Received once:', value);
};
es.listen('my-event', listener);

get(id: string): ES_ValueType

Returns a reference to the internal state object for the given address. Useful for inspecting the current cached value, the listener list, or the loader state.

await es.send('status', 'online');

const data = es.get('status');
console.log(data.last);       // "online"
console.log(data.date);       // Date when last value was sent
console.log(data.listeners);  // Array of registered listeners

ES_ValueType fields:

| Field | Type | Description | |---|---|---| | last | any | The last value sent to this address | | listeners | EventListenerType[] | Array of registered listeners | | date | Date \| undefined | Timestamp of the last send. undefined means no value was sent yet | | loader | Function \| undefined | The loader function, if configured | | loaderCatch | Function \| undefined | The loader error handler, if configured | | loaderProm | Promise \| undefined | The loader promise, while the loader is running |


addInter(id: string, interceptor: EventInterceptorType): () => void

Adds an interceptor for the given address. Interceptors are called before listeners, in the order they were added. Each interceptor receives the value, can transform it, and must return the (possibly modified) value for the next interceptor in the chain.

Returns an unregister function to remove the interceptor.

Special address "": An interceptor registered with an empty string "" as the address is called for every event on the EventSystem, regardless of address.

// Transform values before listeners receive them
es.addInter('counter', async (id, event, es) => {
  return event * 2; // double the value
});

es.listen('counter', (value) => {
  console.log(value); // 20
});

await es.send('counter', 10);

Interceptor chain:

es.addInter('price', async (id, event, es) => {
  return event + 1; // first: add 1
});

es.addInter('price', async (id, event, es) => {
  return event * 10; // second: multiply by 10
});

await es.send('price', 5); // (5 + 1) * 10 = 60

Global interceptor (logging example):

es.addInter('', async (id, event, es) => {
  console.log(`[${id}]`, event);
  return event; // pass through unchanged
});

If an interceptor throws an error, the chain stops and listeners are not called.


removeInter(id: string, interceptor: EventInterceptorType): void

Removes a specific interceptor from the given address.

const interceptor = async (id: string, event: number, es: EventSystem) => {
  return event + 1;
};

es.addInter('counter', interceptor);
es.removeInter('counter', interceptor);

setLoader(id: string, loader, loaderCatch?): void

Configures a loader function for the given address. The loader is a lazy data fetcher that runs automatically when:

  1. The first listener registers on an address, and
  2. No value has been sent to that address yet.

If a value was already sent before the first listener, the loader is never called.

The loader function receives the address id as the first argument, and should call es.send(id, value) to deliver the loaded value.

es.setLoader('user/profile', async (id) => {
  const response = await fetch('/api/profile');
  const data = await response.json();
  es.send(id, data);
});

// The loader runs automatically when the first listener registers
es.listen('user/profile', (profile) => {
  console.log(profile); // data from API
});

With error handler (loaderCatch):

The optional third argument is an error handler invoked when the loader throws. It has three possible behaviors:

  • Return a value: The loader resolves successfully with that value (sent to listeners).
  • Return undefined (or no return): The loader rejects with the original error.
  • Throw a new error: The loader rejects with the new error, and the original error is attached as error.loaderEx.
es.setLoader(
  'user/profile',
  async (id) => {
    throw new Error('Network error');
  },
  async (id, error) => {
    console.warn('Loader failed:', error.message);
    return { name: 'Guest' }; // fallback value sent to listeners
  }
);

setLoaderCatch(id: string, loaderCatch): void

Sets (or replaces) the error handler for the loader of a given address, independently from setLoader.

es.setLoaderCatch('user/profile', async (id, error) => {
  return { name: 'Default' }; // fallback value
});

load<T>(id: string, ...args: any[]): Promise<T>

Manually triggers the loader for the given address. Extra arguments are forwarded to the loader function.

If no loader is configured, returns the current cached value immediately.

es.setLoader('data', async (id, page, limit) => {
  const res = await fetch(`/api/data?page=${page}&limit=${limit}`);
  es.send(id, await res.json());
});

// Manually trigger with parameters
await es.load('data', 1, 20);

Types

// Listener function signature
type EventListenerType<T = any> = (event: T) => void;

// Interceptor function signature
type EventInterceptorType<T = any> = (id: string, event: T, es: EventSystem) => T;