Skip to content

Instantly share code, notes, and snippets.

@sealedsins
Created September 14, 2025 17:54
Show Gist options
  • Select an option

  • Save sealedsins/eb5a115074ed710941ba95a69c53f6e0 to your computer and use it in GitHub Desktop.

Select an option

Save sealedsins/eb5a115074ed710941ba95a69c53f6e0 to your computer and use it in GitHub Desktop.
Event Manager
/**
* Sealed Sins, 2023-2025.
*/
import { describe, it, expect, vi } from 'vitest';
import { EventManager } from './event';
describe('EventManager', () => {
it('manages untyped events', () => {
const manager = new EventManager();
const listener = vi.fn();
manager.subscribe('a', listener);
manager.subscribe('b', listener);
manager.dispatch({ type: 'a' });
manager.dispatch({ type: 'b', data: 'payload' });
expect(listener).toHaveBeenNthCalledWith(1, { type: 'a' });
expect(listener).toHaveBeenNthCalledWith(2, { type: 'b', data: 'payload' });
});
it('manages typed events', () => {
const manager = new EventManager<{ type: 'a' } | { type: 'b'; data: string }>();
const listener = vi.fn();
manager.subscribe('a', listener);
manager.subscribe('b', listener);
manager.dispatch({ type: 'a' });
manager.dispatch({ type: 'b', data: 'something' });
expect(listener).toHaveBeenNthCalledWith(1, { type: 'a' });
expect(listener).toHaveBeenNthCalledWith(2, { type: 'b', data: 'something' });
});
});
/**
* Sealed Sins, 2023-2025.
*/
/**
* Generic Event.
*/
export type Event = {
type: string;
data?: unknown;
};
/**
* Event of a given type.
* @typeParam T - Event Union.
* @typeParam K - Event Type.
* @internal
*/
// prettier-ignore
export type EventOfType<T extends Event, K extends T['type']> = (
string extends T['type']
? { type: K } & Omit<T, 'type'>
: Extract<T, { type: K }>
);
/**
* Event Listener.
* @typeParam T - Event Union.
*/
export type EventListener<T extends Event = Event> = {
(event: T): void;
};
/**
* Event Manager.
* @typeParam T - Event Union.
*/
export class EventManager<T extends Event = Event> {
private listenersByType = new Map<T['type'], EventListener<T>[]>();
/**
* Dispatches event.
* @typeParam K - Event Type.
* @param event - Event to dispatch.
*/
public dispatch<K extends T['type']>(event: EventOfType<T, K>): void {
const listeners = this.listeners(event.type);
[...listeners].forEach((listener) => {
listener(event);
});
}
/**
* Subscribes to event of given type.
* @typeParam K - Event Type.
* @param type - Event Type.
* @param fn - Event listener.
* @returns Unsubscribe function.
*/
public subscribe<K extends T['type']>(type: K, fn: EventListener<EventOfType<T, K>>) {
const listeners = this.listeners(type);
listeners.push(fn);
return () => {
const index = listeners.indexOf(fn);
if (index >= 0) {
listeners.splice(index, 1);
}
};
}
/**
* Returns list of listeners of a certain event type.
* @typeParam K - Event Type.
* @param type - Event Type.
* @returns List of listeners.
* @internal
*/
public listeners<K extends T['type']>(type: K) {
if (!this.listenersByType.has(type)) {
this.listenersByType.set(type, []);
}
const listeners = this.listenersByType.get(type);
return listeners as EventListener<EventOfType<T, K>>[];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment