Created
August 31, 2020 12:21
-
-
Save buschtoens/5490dbd6ebd83c7f0ac8fc5c2a094a8a to your computer and use it in GitHub Desktop.
This file contains hidden or 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 { addListener, trigger, removeListener } from './events'; | |
import type { EVENTS } from './events'; | |
class Foo { | |
declare [EVENTS]: { | |
bar(a: boolean, b: string, c: number): void; | |
qux(): void; | |
}; | |
} | |
test('basic test', () => { | |
const foo = new Foo(); | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars ,@typescript-eslint/no-empty-function | |
const barListener = jest.fn((a: boolean, b: string, c: number) => {}); | |
// eslint-disable-next-line @typescript-eslint/no-empty-function | |
const quxListener = jest.fn(() => {}); | |
addListener(foo, 'bar', barListener); | |
addListener(foo, 'bar', barListener); | |
addListener(foo, 'qux', quxListener); | |
expect(barListener).not.toHaveBeenCalled(); | |
expect(quxListener).not.toHaveBeenCalled(); | |
trigger(foo, 'bar', true, 'foo', 1337); | |
expect(barListener).toHaveBeenCalledWith(true, 'foo', 1337); | |
barListener.mockReset(); | |
expect(quxListener).not.toHaveBeenCalled(); | |
trigger(foo, 'qux'); | |
expect(barListener).not.toHaveBeenCalled(); | |
expect(quxListener).toHaveBeenCalledWith(); | |
quxListener.mockReset(); | |
removeListener(foo, 'bar', barListener); | |
trigger(foo, 'bar', false, 'bye', 42); | |
expect(barListener).not.toHaveBeenCalled(); | |
expect(quxListener).not.toHaveBeenCalled(); | |
}); |
This file contains hidden or 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 const EVENTS: unique symbol = Symbol('events'); | |
/** | |
* @note Theoretically this could include `symbol` as well, but TypeScript does | |
* not allow the generic `symbol` as an index signature, so we can't use it in | |
* `Record<K, V>` down below. | |
*/ | |
type EventName = string | number; | |
type Listener = (...args: any[]) => void | boolean; | |
type Events = Record<EventName, Listener>; | |
/** | |
* Every class that is supposed to behave as an event target needs to implement | |
* this interface. To make it a zero-cost abstraction, you can do it using the | |
* `declare` modifier. | |
* | |
* @example | |
* | |
* ```ts | |
* class Foo { | |
* declare [EVENTS]: { | |
* bar(a: boolean, b: string): void; | |
* } | |
* } | |
* ``` | |
*/ | |
interface EventTarget { | |
[EVENTS]: Events; | |
} | |
const EVENT_TARGETS = new WeakMap< | |
EventTarget, | |
Map<EventName, Set<(...args: any[]) => void>> | |
>(); | |
/** | |
* Returns all registered listeners for a given target and event name as a `Set`. | |
*/ | |
function getListenersForEvent( | |
target: EventTarget, | |
event: EventName | |
): Set<EventTarget[typeof EVENTS][EventName]> { | |
if (!EVENT_TARGETS.has(target)) EVENT_TARGETS.set(target, new Map()); | |
const allListeners = EVENT_TARGETS.get(target)!; | |
if (!allListeners.has(event)) allListeners.set(event, new Set()); | |
return allListeners.get(event)!; | |
} | |
/** | |
* Adds a listener for `event` on the given `target`. Calling this function | |
* repeatedly has no effect. A listener is only added once. | |
* | |
* `listener` is called with `target` as the `this` context and receives the | |
* arguments passed to `trigger`, as defined in the `EventTarget` interface. | |
* | |
* The `target` needs to implement the `EventTarget` interface, like so: | |
* | |
* @example | |
* ```ts | |
* class Foo { | |
* declare [EVENTS]: { | |
* bar(a: boolean, b: string): void; | |
* } | |
* } | |
* | |
* const foo = new Foo(); | |
* | |
* addListener(foo, 'bar', (a: boolean, b: string) => { | |
* console.log({ a, b }); | |
* }); | |
* | |
* trigger(foo, 'bar', true, 'hello'); // => logs `{ a: true, b: 'hello' }` | |
* ``` | |
*/ | |
export function addListener< | |
T extends EventTarget, | |
E extends keyof T[typeof EVENTS] & EventName | |
>(target: T, event: E, listener: T[typeof EVENTS][E]): void { | |
const listeners = getListenersForEvent(target, event); | |
listeners.add(listener); | |
} | |
export const on = addListener; | |
/** | |
* Removes a listener for `event` on the given `target`, if it exists. If it | |
* does not exist, nothing happens. | |
* | |
* The `target` needs to implement the `EventTarget` interface, like so: | |
* | |
* @example | |
* ```ts | |
* class Foo { | |
* declare [EVENTS]: { | |
* bar(a: boolean, b: string): void; | |
* } | |
* } | |
* | |
* const foo = new Foo(); | |
* | |
* function listener(a: boolean, b: string) { | |
* console.log({ a, b }); | |
* } | |
* | |
* addListener(foo, 'bar', listener); | |
* | |
* removeListener(foo, 'bar', listener); | |
* | |
* trigger(foo, 'bar', true, 'hello'); // => nothing happens | |
* ``` | |
*/ | |
export function removeListener< | |
T extends EventTarget, | |
E extends keyof T[typeof EVENTS] & EventName | |
>(target: T, event: E, listener: T[typeof EVENTS][E]): void { | |
const listeners = getListenersForEvent(target, event); | |
listeners.delete(listener); | |
} | |
export const off = removeListener; | |
/** | |
* Calls all registered listeners for `event` on `target` in the order that they | |
* were registered in. | |
* | |
* @example | |
* ```ts | |
* class Foo { | |
* declare [EVENTS]: { | |
* bar(a: boolean, b: string): void; | |
* } | |
* } | |
* | |
* const foo = new Foo(); | |
* | |
* addListener(foo, 'bar', (a: boolean, b: string) => { | |
* console.log({ i: 0, a, b }); | |
* }); | |
* addListener(foo, 'bar', (a: boolean, b: string) => { | |
* console.log({ i: 1, a, b }); | |
* }); | |
* | |
* trigger(foo, 'bar', true, 'hello'); | |
* // => { i: 0, a: true, b: 'hello' } | |
* // => { i: 1, a: true, b: 'hello' } | |
* ``` | |
*/ | |
export function trigger< | |
T extends EventTarget, | |
E extends keyof T[typeof EVENTS] & EventName | |
>(target: T, event: E, ...args: Parameters<T[typeof EVENTS][E]>): void { | |
const listeners = getListenersForEvent(target, event); | |
for (const listener of listeners) listener.apply(target, args); | |
} | |
export const emit = trigger; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment