mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-03 05:55:03 +01:00
feat(utils): add TypedEmitter class
This commit is contained in:
59
packages/utils/src/typedEventEmitter.ts
Normal file
59
packages/utils/src/typedEventEmitter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
120
packages/utils/tests/typedEventEmitter.test.ts
Normal file
120
packages/utils/tests/typedEventEmitter.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user