Skip to content

Instantly share code, notes, and snippets.

@emkis
Last active October 9, 2023 10:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emkis/b275ddbb56052f9a353c05e0e055baac to your computer and use it in GitHub Desktop.
Save emkis/b275ddbb56052f9a353c05e0e055baac to your computer and use it in GitHub Desktop.
Simple TypeScript implementation of the event emitter pattern
import { createEventEmitter } from './event-emitter'
type ExampleEvents = {
add_todo: { id: string; text: string }
complete_todo: { id: string }
delete_todo: { id: string }
}
beforeEach(jest.clearAllMocks)
it('should only call registered event', () => {
const onAddTodo = jest.fn()
const todoEventEmitter = createEventEmitter<ExampleEvents>()
todoEventEmitter.on('add_todo', onAddTodo)
expect(onAddTodo).not.toHaveBeenCalled()
todoEventEmitter.emit('add_todo', { id: '1', text: 'Foo' })
todoEventEmitter.emit('complete_todo', { id: '1' })
todoEventEmitter.emit('delete_todo', { id: '1' })
expect(onAddTodo).toHaveBeenCalledTimes(1)
expect(onAddTodo).toHaveBeenLastCalledWith({ id: '1', text: 'Foo' })
})
it('should not call handler after un-registering', () => {
const onCompleteTodo = jest.fn()
const todoEventEmitter = createEventEmitter<ExampleEvents>()
todoEventEmitter.on('complete_todo', onCompleteTodo)
todoEventEmitter.emit('complete_todo', { id: '2' })
expect(onCompleteTodo).toHaveBeenCalledTimes(1)
expect(onCompleteTodo).toHaveBeenLastCalledWith({ id: '2' })
todoEventEmitter.off('complete_todo', onCompleteTodo)
todoEventEmitter.emit('complete_todo', { id: '3' })
todoEventEmitter.emit('complete_todo', { id: '4' })
expect(onCompleteTodo).toHaveBeenCalledTimes(1)
})
it('should allow registering multiple handlers', () => {
const handlerA = jest.fn()
const handlerB = jest.fn()
const todoEventEmitter = createEventEmitter<ExampleEvents>()
todoEventEmitter.on('delete_todo', handlerA)
todoEventEmitter.on('delete_todo', handlerB)
todoEventEmitter.emit('delete_todo', { id: '5' })
expect(handlerA).toHaveBeenCalledTimes(1)
expect(handlerB).toHaveBeenCalledTimes(1)
})
export type EventType = string;
export type EventsShape = Record<EventType, unknown>;
export type EventHandler<T> = (event: T) => void;
export type EventHandlerArray<T> = Array<EventHandler<T>>;
export type EventEmitter<Events extends EventsShape> = {
on: <TEventKey extends keyof Events>(
event: TEventKey,
handler: EventHandler<Events[TEventKey]>,
) => void;
off: <TEventKey extends keyof Events>(
event: TEventKey,
handler: EventHandler<Events[TEventKey]>,
) => void;
emit: <TEventKey extends keyof Events>(event: TEventKey, payload: Events[TEventKey]) => void;
};
export function createEventEmitter<Events extends EventsShape>(): EventEmitter<Events> {
const listeners = new Map<keyof Events, EventHandlerArray<Events[keyof Events]>>();
return {
on(event, listener) {
const _listener = listener as EventHandler<Events[keyof Events]>;
const eventListeners = listeners.get(event);
eventListeners ? eventListeners.push(_listener) : listeners.set(event, [_listener]);
},
off(event, listener) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
const listenerIndex = eventListeners.indexOf(listener as EventHandler<Events[keyof Events]>);
const isListenerRegistered = listenerIndex !== -1;
if (isListenerRegistered) {
eventListeners.splice(listenerIndex, 1);
}
},
emit(event, options) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
eventListeners.forEach((listener) => listener(options));
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment