radiate
v0.3.2
Published
A minimal, type-safe event emitter for TypeScript with zero dependencies
Maintainers
Readme
radiate
A minimal, type-safe event emitter for TypeScript with zero dependencies.
Features
- Type-safe - Full TypeScript support with typed event maps and listeners
- Zero dependencies - Lightweight and tree-shakeable
- Simple API - Easy to use with methods like
on(),off(),once(), andemit() - Keyed events - Support for events partitioned by ID/key with wildcard listeners
- Error handling - Optional error handler for production environments
- Flexible - Use as a base class or compose it into your classes
- ESM only - Modern ES modules for optimal bundle size
Installation
npm install radiate
# or
pnpm add radiate
# or
yarn add radiateUsage
Basic Example
import { Emitter } from 'radiate';
// Define your event types
type MyEvents = {
'item:added': { id: string; name: string };
'item:removed': string;
'cleared': void;
};
const emitter = new Emitter<MyEvents>();
// Subscribe to events
const unsubscribe = emitter.on('item:added', (item) => {
console.log('Added:', item);
});
// Emit events
emitter.emit('item:added', { id: '1', name: 'Item 1' });
// Unsubscribe when done
unsubscribe();Using the Factory Function
import { createEmitter } from 'radiate';
// Define your event types
type MyEvents = {
'item:added': { id: string; name: string };
'item:removed': string;
'cleared': void;
};
// Create an emitter using the factory function
const emitter = createEmitter<MyEvents>();
// Use it the same way as a class instance
emitter.on('item:added', (item) => {
console.log('Added:', item);
});
emitter.emit('item:added', { id: '1', name: 'Item 1' });Extending the Emitter
import { Emitter } from 'radiate';
type MyEvents = {
'item:added': { id: string; name: string };
'item:removed': string;
'cleared': void;
};
class MyStore extends Emitter<MyEvents> {
addItem(item: { id: string; name: string }) {
// ... add logic
this.emit('item:added', item);
}
removeItem(id: string) {
// ... remove logic
this.emit('item:removed', id);
}
clear() {
// ... clear logic
this.emit('cleared');
}
}
const store = new MyStore();
store.on('item:added', (item) => {
console.log('Item added:', item);
});One-time Listeners
emitter.once('cleared', () => {
console.log('This will only fire once');
});
emitter.emit('cleared'); // Logs: "This will only fire once"
emitter.emit('cleared'); // Nothing happensError Handling
import { Emitter } from 'radiate';
type MyEvents = {
'item:added': { id: string; name: string };
};
const emitter = new Emitter<MyEvents>({
errorHandler: (event, error, listener) => {
console.error(`Error in ${String(event)}:`, error);
// Send to error tracking service
// Sentry.captureException(error);
},
});
// This error will be caught by the error handler
emitter.on('item:added', (item) => {
throw new Error('Something went wrong');
});
emitter.emit('item:added', { id: '1', name: 'Item 1' });Create Subscription Pattern
import { Emitter } from 'radiate';
class Store {
private $ = new Emitter<{ 'count:changed': number }>();
private count = 0;
// Create subscription function once
subscribe = this.$.createSubscription('count:changed', () => this.count);
increment() {
this.count++;
this.$.emit('count:changed', this.count);
}
}
const store = new Store();
// Subscriber receives current value immediately
store.subscribe((count) => console.log('Count:', count));
// Logs: "Count: 0" immediately upon subscription
store.increment(); // Logs: "Count: 1"Checking Listeners
// Check if there are listeners for an event
if (emitter.hasListeners('item:added')) {
// Prepare expensive data only if someone is listening
const expensiveData = await prepareData();
emitter.emit('item:added', expensiveData);
}
// Get the number of listeners
const count = emitter.listenerCount('item:added');
console.log(`There are ${count} listeners`);Removing Listeners
const listener = (item) => console.log('Listener 1:', item);
emitter.on('item:added', listener);
// Remove a specific listener
emitter.off('item:added', listener);
// Remove all listeners for a specific event
emitter.removeAllListeners('item:added');
// Remove all listeners for all events
emitter.removeAllListeners();Keyed Events
Keyed events allow you to partition events by an ID/key. This is useful for scenarios like entity-specific events where you want to listen to updates for a specific entity.
import { Emitter, KeyedEventMap } from 'radiate';
// Define regular events
type MyEvents = {
'user:login': { userId: string; timestamp: number };
'app:ready': void;
};
// Define keyed events with their ID and data types
type MyKeyedEvents = {
'entity:updated': { id: string; data: { name: string; value: number } };
'transaction:status': { id: number; data: 'pending' | 'completed' | 'failed' };
};
// Create emitter with both event types
const emitter = new Emitter<MyEvents, MyKeyedEvents>();
// Listen to a specific entity by ID
const unsub = emitter.onKeyed('entity:updated', 'entity-123', (data) => {
// data is { name: string; value: number }
console.log('Entity 123 updated:', data);
});
// Listen to ALL entities (wildcard)
emitter.onKeyed('entity:updated', '*', (key, data) => {
// key is string, data is { name: string; value: number }
console.log(`Entity ${key} updated:`, data);
});
// Emit to a specific key (notifies both specific and wildcard listeners)
emitter.emitKeyed('entity:updated', 'entity-123', { name: 'Test', value: 42 });
// One-time keyed listener
emitter.onceKeyed('transaction:status', 12345, (status) => {
console.log('Transaction completed with status:', status);
});
// Emit with number ID
emitter.emitKeyed('transaction:status', 12345, 'completed');
// Check for keyed listeners
if (emitter.hasKeyedListeners('entity:updated', 'entity-123')) {
// There are listeners for this specific entity
}
// Get listener count (includes wildcard listeners)
const count = emitter.keyedListenerCount('entity:updated', 'entity-123');
// Remove all keyed listeners for a specific key
emitter.removeAllKeyedListeners('entity:updated', 'entity-123');
// Remove all keyed listeners for an event
emitter.removeAllKeyedListeners('entity:updated');
// Remove all keyed listeners
emitter.removeAllKeyedListeners();Different ID Types
Each keyed event can have its own ID type:
type MyKeyedEvents = {
'user:action': { id: string; data: ActionData }; // string IDs
'order:update': { id: number; data: OrderData }; // number IDs
'session:event': { id: `session-${string}`; data: SessionData }; // template literal IDs
};API Reference
createEmitter<Events, KeyedEvents>(options?)
Factory function to create a new Emitter instance. This is an alternative to using new Emitter().
function createEmitter<Events, KeyedEvents>(options?: EmitterOptions<Events>): Emitter<Events, KeyedEvents>options.errorHandler- Optional error handler function for catching listener errors- Returns a new
Emitterinstance
class Emitter<Events, KeyedEvents>
Constructor
constructor(options?: EmitterOptions<Events>)options.errorHandler- Optional error handler function for catching listener errors
Regular Event Methods
on(event, listener)- Subscribe to an event, returns an unsubscribe functionoff(event, listener)- Unsubscribe a specific listener from an eventonce(event, listener)- Subscribe to an event for a single emissionemit(event, data)- Emit an event to all subscribed listenerscreateSubscription(event, getCurrentValue)- Create a subscription function that calls the listener immediately with the current value, then subscribes for future emissionshasListeners(event)- Check if there are any listeners for an eventlistenerCount(event)- Get the number of listeners for an eventremoveAllListeners(event?)- Remove all listeners for a specific event or all events
Keyed Event Methods
onKeyed(event, key, listener)- Subscribe to a keyed event for a specific key, returns an unsubscribe functiononKeyed(event, '*', listener)- Subscribe to all emissions of a keyed event (wildcard), listener receives(key, data)offKeyed(event, key, listener)- Unsubscribe a specific listener from a keyed eventonceKeyed(event, key, listener)- Subscribe to a keyed event for a single emissionemitKeyed(event, key, data)- Emit a keyed event to specific key listeners and wildcard listenerscreateKeyedSubscription(event, getCurrentValue)- Create a subscription factory for keyed events that calls the listener immediately with the current value, then subscribes for future emissionshasKeyedListeners(event, key?)- Check if there are any listeners for a keyed event (optionally for a specific key)keyedListenerCount(event, key?)- Get the number of listeners for a keyed event (optionally for a specific key)removeAllKeyedListeners(event?, key?)- Remove keyed listeners for a specific event/key or all keyed listeners
Types
type Listener<T = unknown> = (data: T) => void;
type ErrorHandler<Events> = (
event: keyof Events,
error: Error,
listener: Listener<unknown>,
) => void;
type EventMap<T extends Record<string, unknown>> = T;
// Keyed event map - each entry has { id: IdType; data: DataType }
type KeyedEventMap<T extends Record<string, { id: unknown; data: unknown }>> = T;
// Extract ID type from keyed event definition
type KeyedEventId<T> = T extends { id: infer Id; data: unknown } ? Id : never;
// Extract data type from keyed event definition
type KeyedEventData<T> = T extends { id: unknown; data: infer Data } ? Data : never;
// Listener for specific key (receives only data)
type KeyedListener<Data> = (data: Data) => void;
// Listener for wildcard (receives key and data)
type WildcardKeyedListener<Id, Data> = (key: Id, data: Data) => void;
type Unsubscribe = () => void;
interface EmitterOptions<Events> {
errorHandler?: ErrorHandler<Events>;
}License
MIT
