comes
v0.0.4
Published
Simple Event System Communication
Maintainers
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 comesQuick 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 listenersES_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 = 60Global 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:
- The first listener registers on an address, and
- 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;