Last active
May 3, 2023 14:49
-
-
Save nilscox/c16b33ce00b7a5181c4855e2a7c9735d to your computer and use it in GitHub Desktop.
Event emitter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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