Skip to content

Instantly share code, notes, and snippets.

@nilscox
Last active May 3, 2023 14:49
Show Gist options
  • Save nilscox/c16b33ce00b7a5181c4855e2a7c9735d to your computer and use it in GitHub Desktop.
Save nilscox/c16b33ce00b7a5181c4855e2a7c9735d to your computer and use it in GitHub Desktop.
Event emitter
import { Mock } from 'vitest';
import { Emitter } from './emitter';
enum TestEvent {
move = 'move',
jump = 'jump',
}
type TestEventsMap = {
[TestEvent.move]: 'left' | 'right';
};
class TestEmitter extends Emitter<TestEvent, TestEventsMap> {}
describe('Emitter', () => {
let onJump: Mock<[], void>;
let onMove: Mock<['left' | 'right'], void>;
beforeEach(() => {
onJump = vi.fn();
onMove = vi.fn();
});
it('binds a listener to an event', () => {
const emitter = new TestEmitter();
emitter.addListener(TestEvent.jump, onJump);
emitter.emit(TestEvent.jump);
expect(onJump).toHaveBeenCalledWith(undefined);
expect(onMove).not.toHaveBeenCalled();
});
it('binds a listener to an event with payload', () => {
const emitter = new TestEmitter();
const onMove = vi.fn();
emitter.addListener(TestEvent.move, onMove);
emitter.emit(TestEvent.move, 'left');
expect(onJump).not.toHaveBeenCalled();
expect(onMove).toHaveBeenCalledWith('left');
});
it('removes a listener', () => {
const emitter = new TestEmitter();
const onJump = vi.fn();
emitter.addListener(TestEvent.jump, onJump);
emitter.removeListener(TestEvent.jump, onJump);
emitter.emit(TestEvent.jump);
expect(onJump).not.toHaveBeenCalled();
});
it('removes a listener using the returned callback', () => {
const emitter = new TestEmitter();
const onJump = vi.fn();
const unsubscribe = emitter.addListener(TestEvent.jump, onJump);
unsubscribe();
emitter.emit(TestEvent.jump);
expect(onJump).not.toHaveBeenCalled();
});
it('removes all listeners for an event type', () => {
const emitter = new TestEmitter();
const onJump = vi.fn();
emitter.addListener(TestEvent.jump, onJump);
emitter.addListener(TestEvent.move, onMove);
emitter.removeListeners(TestEvent.jump);
emitter.emit(TestEvent.jump);
emitter.emit(TestEvent.move, 'left');
expect(onJump).not.toHaveBeenCalled();
expect(onMove).toHaveBeenCalled();
});
it('removes all listeners', () => {
const emitter = new TestEmitter();
const onJump = vi.fn();
emitter.addListener(TestEvent.jump, onJump);
emitter.addListener(TestEvent.move, onMove);
emitter.removeListeners();
emitter.emit(TestEvent.jump);
emitter.emit(TestEvent.move, 'left');
expect(onJump).not.toHaveBeenCalled();
expect(onMove).not.toHaveBeenCalled();
});
it('clones an emitter', () => {
const emitter = new TestEmitter();
const clone = emitter.cloneEmitter();
const onJump2 = vi.fn();
emitter.addListener(TestEvent.jump, onJump);
clone.addListener(TestEvent.jump, onJump2);
emitter.emit(TestEvent.jump);
expect(onJump).toHaveBeenCalled();
expect(onJump2).toHaveBeenCalled();
});
it('removes all listeners on a cloned emitter', () => {
const emitter = new TestEmitter();
const clone = emitter.cloneEmitter();
const onJump2 = vi.fn();
emitter.addListener(TestEvent.jump, onJump);
clone.addListener(TestEvent.jump, onJump2);
clone.removeListeners();
emitter.emit(TestEvent.jump);
expect(onJump).toHaveBeenCalled();
expect(onJump2).not.toHaveBeenCalled();
});
});
export type Listener<Event> = (event: Event) => void;
export class Emitter<Type extends string, EventsMap extends Partial<Record<Type, unknown>> = {}> {
private listeners = new MapSet<string, Listener<any>>();
private children = new Set<Emitter<Type, EventsMap>>();
cloneEmitter(): Emitter<Type, EventsMap> {
const child = new Emitter<Type, EventsMap>();
this.children.add(child);
return child;
}
addListener<T extends Type>(type: T, listener: Listener<EventsMap[T]>): () => void {
this.listeners.add(type, listener);
return () => this.removeListener(type, listener);
}
removeListener<T extends Type>(type: T, listener: Listener<EventsMap[T]>) {
this.listeners.get(type)?.delete(listener);
}
removeListeners(type?: Type) {
if (type) {
this.listeners.get(type)?.clear();
} else {
this.listeners.forEach((listener) => listener.clear());
this.listeners.clear();
}
}
emit<T extends Type>(type: T, ...event: T extends keyof EventsMap ? [EventsMap[T]] : []) {
this.listeners.get(type)?.forEach((listener) => listener(event[0]));
for (const child of this.children) {
child.emit(type, ...event);
}
}
}
class MapSet<K, T> extends Map<K, Set<T>> {
add(key: K, value: T) {
if (!this.has(key)) {
this.set(key, new Set());
}
this.get(key)?.add(value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment