feat(utils): add TypedEmitter class

This commit is contained in:
Szymon Lesisz
2023-05-10 13:34:36 +02:00
committed by martin
parent 340bcf00f4
commit 12ef633192
2 changed files with 179 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
/*
This file should not be exported from index to prevent missing dependency/polyfill error.
EventEmitter is accessible in nodejs but requires polyfills in web builds.
use:
import { TypedEmitter } from '@trezor/utils/lib/typedEventEmitter';
example:
type EventMap = {
obj: { id: string };
primitive: boolean | number | string | symbol;
noArgs: undefined;
multipleArgs: (a: number, b: string, c: boolean) => void;
[type: `dynamic/${string}`]: boolean;
};
*/
import { EventEmitter } from 'events';
type EventMap = Record<string, any>;
type EventKey<T extends EventMap> = string & keyof T;
type IsUnion<T, U extends T = T> = T extends unknown ? ([U] extends [T] ? 0 : 1) : 2;
// NOTE: case 1. looks like case 4. but works differently. the order matters
type EventReceiver<T> = IsUnion<T> extends 1
? (event: T) => void // 1. use union payload
: T extends (...args: any[]) => any
? T // 2. use custom callback
: T extends undefined
? () => void // 3. enforce empty params
: (event: T) => void; // 4. default
export interface TypedEmitter<T extends EventMap> {
on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
once<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
addListener<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
prependListener<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
prependOnceListener<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
removeListener<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): this;
removeAllListeners<K extends EventKey<T>>(event?: K): this;
emit<K extends EventKey<T>>(eventName: K, ...params: Parameters<EventReceiver<T[K]>>): boolean;
listeners<K extends EventKey<T>>(eventName: K): EventReceiver<T[K]>[];
rawListeners<K extends EventKey<T>>(eventName: K): EventReceiver<T[K]>[];
}
export class TypedEmitter<T extends EventMap> extends EventEmitter {
// implement at least one function
listenerCount<K extends EventKey<T>>(eventName: K) {
return super.listenerCount(eventName);
}
}

View File

@@ -0,0 +1,120 @@
import { TypedEmitter } from '../src/typedEventEmitter';
type PayloadUnion = { foo: number } | { bar: string };
type Events = {
obj: { id: string };
bool: boolean;
nr: number;
str: string;
union: PayloadUnion;
unionMultiple: (a: PayloadUnion, b: PayloadUnion) => Promise<void>;
noArgs: undefined;
multipleArgs: (a: number, b: (success: boolean, err?: Error) => void) => void;
[type: `dynamic/${string}`]: [(str: string) => boolean];
};
describe('typedEventEmitter', () => {
const emitter = new TypedEmitter<Events>();
beforeEach(() => {
emitter.removeAllListeners();
});
it('object payload', () => {
emitter.once('obj', obj => {
expect(typeof obj.id).toBe('string');
});
emitter.emit('obj', { id: 'id' });
emitter.off('obj', obj => {
expect(typeof obj.id).toBe('string');
});
emitter.removeAllListeners('obj');
// @ts-expect-error
emitter.emit('obj');
// @ts-expect-error
emitter.emit('obj', 1);
});
it('no payload', () => {
// @ts-expect-error
emitter.on('noArgs', arg => {
expect(typeof arg).toBe('undefined');
});
emitter.emit('noArgs');
emitter.removeAllListeners('noArgs');
});
it('boolean payload', () => {
emitter.once('bool', bool => {
expect(typeof bool).toBe('boolean');
});
emitter.emit('bool', true);
// @ts-expect-error
emitter.emit('bool');
// @ts-expect-error
emitter.emit('bool', 1);
emitter.on('nr', nr => expect(nr).toBe(1));
const asyncCb = (nr: number) => Promise.resolve(expect(nr).toBe(1));
emitter.on('nr', asyncCb);
});
it('union payload', () => {
emitter.on('union', m => {
if (!m) return;
if ('foo' in m) {
expect(m.foo).toBe(1);
}
if ('bar' in m) {
expect(m.bar).toBe('bar');
}
});
const p = emitter.listenerCount('union') > 0 ? { foo: 1 } : { bar: 'bar' };
emitter.emit('union', p);
emitter.emit('unionMultiple', p, p);
emitter.emit('union', { foo: 1 });
emitter.emit('unionMultiple', { foo: 1 }, { bar: 'bar' });
emitter.emit('union', { bar: 'bar' });
// @ts-expect-error
emitter.emit('union');
// @ts-expect-error
emitter.emit('union', { foo: 1, err: 'err' });
});
it('multiple arguments', () => {
// @ts-expect-error
emitter.emit('multipleArgs');
// @ts-expect-error
emitter.emit('multipleArgs', true);
// @ts-expect-error
emitter.emit('multipleArgs', [1, () => {}]);
emitter.on('multipleArgs', (nr, callback) => {
expect(typeof nr).toBe('number');
expect(typeof callback).toBe('function');
callback(false, new Error('a'));
});
emitter.emit('multipleArgs', 1, (_a, _b) => {});
});
it('dynamic event name', () => {
emitter.on('dynamic/abc', ([cb]) => {
const resp = cb('foo');
expect(typeof resp).toBe('boolean');
});
emitter.emit('dynamic/abc', [(str: string) => typeof str === 'string']);
// @ts-expect-error
emitter.emit('dynamic', (str: string) => typeof str === 'string');
});
});