Listeners Pattern
The Listeners pattern manages multiple event handlers and notifies them with error isolation. When a listener throws an error, other listeners continue executing.
Type Definition
/**
* Creates a listener management system for registering multiple event handlers and
* notifying them asynchronously with error isolation.
*
* @template T - Tuple type representing the arguments passed to listeners
* @template E - Type of errors that can be thrown by listeners (defaults to unknown)
* @param onError - Optional error handler called when a listener throws an error
* @returns Tuple containing [addListener, notifyListeners] functions
*/
function newListeners<T extends unknown[], E = unknown>(
onError?: (error: E) => void,
): [
/**
* Registers a new listener function.
* @param listener - Function to call when notifying listeners
* @returns Cleanup function to remove this specific listener
*/
addListener: (listener: (...args: T) => void | Promise<void>) => () => void,
/**
* Notifies all registered listeners with the provided arguments.
* Continues execution even if individual listeners throw errors.
* @param args - Arguments to pass to all listeners
*/
notifyListeners: (...args: T) => Promise<void>,
];
Features:
- Type-safe argument passing to listeners
- Error isolation prevents one failure from stopping others
- Supports synchronous and asynchronous listeners
- Returns cleanup functions for memory management
- Sequential execution (not concurrent)
Usage Patterns
Basic event management:
const [addListener, notifyListeners] = newListeners<[string, number]>();
// Register listeners
const removeListener1 = addListener((message, count) => {
console.log(`Listener 1: ${message} (${count})`);
});
const removeListener2 = addListener(async (message, count) => {
await delay(100);
console.log(`Listener 2: ${message} (${count})`);
});
// Notify all listeners
await notifyListeners("Hello", 42);
// Cleanup
removeListener1();
removeListener2();
Reactive data model:
class UserModel {
private _users: User[] = [];
addListener: (listener: (users: User[]) => void) => () => void;
protected notifyListeners: (users: User[]) => Promise<void>;
constructor() {
[this.addListener, this.notifyListeners] = newListeners<[User[]]>();
}
get users() {
return [...this._users];
}
addUser(user: User) {
this._users.push(user);
this.notifyListeners(this.users);
}
removeUser(id: string) {
this._users = this._users.filter(u => u.id !== id);
this.notifyListeners(this.users);
}
onChange(listener: (users: User[]) => void) {
return this.addListener(listener);
}
}
// Usage
const userModel = new UserModel();
const unsubscribe = userModel.onChange((users) => {
console.log('Users updated:', users.length);
});
userModel.addUser({ id: '1', name: 'Alice' });
unsubscribe();
Error handling:
const [addListener, notifyListeners] = newListeners<[string]>((error) => {
console.error('Listener error:', error.message);
errorTracker.report(error);
});
// Register listeners that might fail
addListener((message) => {
if (message === 'error') {
throw new Error('Simulated error');
}
console.log('Good listener:', message);
});
addListener((message) => {
console.log('Always works:', message);
});
// Both listeners are called, error is handled
await notifyListeners('error');
Basic Implementation
function newListeners<T extends unknown[], E = unknown>(
onError: (error: E) => void = console.error,
): [
addListener: (listener: (...args: T) => void | Promise<void>) => () => void,
notifyListeners: (...args: T) => Promise<void>,
] {
const listeners: {
[listenerId: number]: (...args: T) => Promise<void>;
} = {};
let listenerId = 0;
function addListener(
listener: (...args: T) => void | Promise<void>,
): () => void {
const id = listenerId++;
listeners[id] = async (...args: T) => {
try {
await listener?.(...args);
} catch (e) {
onError(e as E);
}
}
return () => {
delete listeners[id];
};
}
async function notifyListeners(...args: T): Promise<void> {
for (const listener of Object.values(listeners)) {
await listener(...args);
}
}
return [addListener, notifyListeners];
}