Skip to content

Commit a459820

Browse files
committed
feat(EventEmitter): added simple and fast EventEmitter with Node.js API
1 parent e2f3a02 commit a459820

File tree

3 files changed

+299
-0
lines changed

3 files changed

+299
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// tslint:disable:no-expression-statement
2+
import test from 'ava';
3+
import { EventEmitter } from './event-emitter';
4+
5+
const events = {
6+
CREATED: Symbol('created'),
7+
DESTROYED: 'destroyed'
8+
};
9+
10+
test('emits', t => {
11+
const originatedValue = Symbol('data');
12+
13+
const eventEmitter = new EventEmitter<any>();
14+
15+
eventEmitter.on(events.CREATED, (value, additional) => {
16+
t.is(value, originatedValue);
17+
t.is(additional, 2);
18+
});
19+
20+
eventEmitter.on(events.DESTROYED, (value, additional) => {
21+
t.is(value, originatedValue);
22+
t.is(additional, 2);
23+
});
24+
25+
eventEmitter.emit(events.CREATED, originatedValue, 2);
26+
eventEmitter.emit(events.DESTROYED, originatedValue, 2);
27+
});
28+
29+
test('once', t => {
30+
const originatedValue = Symbol('data');
31+
32+
const eventEmitter = new EventEmitter<typeof originatedValue>();
33+
34+
let invokes = 0;
35+
36+
eventEmitter.once(events.CREATED, (value, additional) => {
37+
t.is(value, originatedValue);
38+
t.is(additional, 2);
39+
40+
invokes++;
41+
});
42+
43+
eventEmitter.emit(events.CREATED, originatedValue, 2);
44+
eventEmitter.emit(events.CREATED, originatedValue, 2);
45+
46+
t.is(invokes, 1);
47+
});
48+
49+
test('off', t => {
50+
const originatedValue = Symbol('data');
51+
52+
const eventEmitter = new EventEmitter<typeof originatedValue>();
53+
54+
let invokes = 0;
55+
56+
const listener = (value, additional) => {
57+
t.is(value, originatedValue);
58+
t.is(additional, 2);
59+
60+
invokes++;
61+
};
62+
63+
eventEmitter.on(events.CREATED, listener);
64+
eventEmitter.emit(events.CREATED, originatedValue, 2);
65+
eventEmitter.emit(events.CREATED, originatedValue, 2);
66+
67+
eventEmitter.off(events.CREATED, listener);
68+
eventEmitter.emit(events.CREATED, originatedValue, 2);
69+
70+
t.is(invokes, 2);
71+
});
72+
73+
test('removeAllListeners', t => {
74+
const originatedValue = Symbol('data');
75+
76+
const eventEmitter = new EventEmitter<typeof originatedValue | number>();
77+
78+
let invokes1 = 0;
79+
let invokes2 = 0;
80+
81+
const listener1 = (value, additional) => {
82+
t.is(value, originatedValue);
83+
t.is(additional, 2);
84+
85+
invokes1++;
86+
};
87+
88+
const listener2 = value => {
89+
t.is(value, 1);
90+
91+
invokes2++;
92+
};
93+
94+
const n = 5;
95+
const m = 5;
96+
97+
for (let i = 0; i < n; ++i) {
98+
eventEmitter.on(events.CREATED, listener1);
99+
eventEmitter.on(events.DESTROYED, listener2);
100+
}
101+
102+
for (let i = 0; i < m; ++i) {
103+
eventEmitter.emit(events.CREATED, originatedValue, 2);
104+
}
105+
106+
t.is(invokes1, n * m);
107+
t.is(invokes2, 0);
108+
109+
// --
110+
eventEmitter.removeAllListeners(events.CREATED);
111+
eventEmitter.emit(events.CREATED, originatedValue, 2);
112+
t.is(invokes1, n * m);
113+
114+
// --
115+
eventEmitter.emit(events.DESTROYED, 1);
116+
t.is(invokes2, m);
117+
eventEmitter.removeAllListeners();
118+
eventEmitter.emit(events.DESTROYED, 1);
119+
t.is(invokes2, m);
120+
});
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
export type TEventName = string | symbol;
2+
export type TEventCallback<T> = (a: T, ...args: any[]) => void;
3+
4+
export interface ListenEvents<T> {
5+
on: (eventName: TEventName, fn: TEventCallback<T>) => EventEmitter<T>;
6+
once: (eventName: TEventName, fn: TEventCallback<T>) => EventEmitter<T>;
7+
off: (eventName: TEventName, fn: TEventCallback<T>) => EventEmitter<T>;
8+
addListener: (
9+
eventName: TEventName,
10+
fn: TEventCallback<T>
11+
) => EventEmitter<T>;
12+
removeListener: (
13+
eventName: TEventName,
14+
fn: TEventCallback<T>
15+
) => EventEmitter<T>;
16+
removeAllListeners: (eventName: TEventName) => EventEmitter<T>;
17+
18+
eventNames: () => TEventName[];
19+
}
20+
21+
export interface EmitEvents<T> {
22+
emit: (eventName: TEventName, value: T, ...args) => boolean;
23+
}
24+
25+
export class EventEmitter<T> implements EmitEvents<T>, ListenEvents<T> {
26+
private listenersMap = new Map<TEventName, Array<Array<TEventCallback<T>>>>();
27+
28+
/**
29+
* @param {TEventName} eventName
30+
* @param {TEventCallback} cb
31+
* @returns {EventEmitter}
32+
*/
33+
public on(eventName: TEventName, cb: TEventCallback<T>): this {
34+
if (!this.hasEventContainer(eventName)) {
35+
this.createEventContainer(eventName);
36+
}
37+
38+
const container = this.getEventContainer(eventName);
39+
40+
container.push([cb]);
41+
42+
return this;
43+
}
44+
45+
/**
46+
* @param {TEventName} eventName
47+
* @param {TEventCallback} cb
48+
* @returns {EventEmitter}
49+
*/
50+
public once(eventName: TEventName, cb: TEventCallback<T>): this {
51+
if (!this.hasEventContainer(eventName)) {
52+
this.createEventContainer(eventName);
53+
}
54+
55+
const container = this.getEventContainer(eventName);
56+
57+
const wrapped = (value: T, ...args) => {
58+
this.off(eventName, wrapped);
59+
cb(value, ...args);
60+
};
61+
62+
container.push([wrapped, cb]);
63+
64+
return this;
65+
}
66+
67+
/**
68+
* @param {TEventName} eventName
69+
* @param {TEventCallback} cb
70+
* @returns {EventEmitter}
71+
*/
72+
public off(eventName: TEventName, cb: TEventCallback<T>): this {
73+
const container = this.getEventContainer(eventName);
74+
75+
if (!container) {
76+
return this;
77+
}
78+
79+
const index = container.findIndex(item => {
80+
return item.includes(cb);
81+
});
82+
83+
if (index >= 0) {
84+
container.splice(index, 1);
85+
}
86+
87+
return this;
88+
}
89+
90+
/**
91+
* @param {TEventName} eventName
92+
* @param {*} value
93+
* @param {*} args
94+
* @returns {boolean}
95+
*/
96+
public emit(eventName: TEventName, value: T, ...args): boolean {
97+
const container = this.getEventContainer(eventName);
98+
99+
if (!container) {
100+
return false;
101+
}
102+
103+
for (const [cb] of container) {
104+
cb(value, ...args);
105+
}
106+
107+
return container.length > 0;
108+
}
109+
110+
/**
111+
* @param {TEventName} eventName
112+
* @param {TEventCallback} cb
113+
* @returns {EventEmitter}
114+
*/
115+
public addListener(eventName: TEventName, cb: TEventCallback<T>): this {
116+
return this.on(eventName, cb);
117+
}
118+
119+
/**
120+
* @param {TEventName} eventName
121+
* @param {TEventCallback} cb
122+
* @returns {EventEmitter}
123+
*/
124+
public removeListener(eventName: TEventName, cb: TEventCallback<T>): this {
125+
return this.off(eventName, cb);
126+
}
127+
128+
/**
129+
* @param {TEventName?} [eventName]
130+
* @returns {EventEmitter}
131+
*/
132+
public removeAllListeners(eventName: TEventName = null): this {
133+
if (eventName) {
134+
this.deleteEventContainer(eventName);
135+
} else {
136+
this.listenersMap.clear();
137+
}
138+
139+
return this;
140+
}
141+
142+
/**
143+
* @returns {TEventName[]}
144+
*/
145+
public eventNames(): TEventName[] {
146+
return [...this.listenersMap.keys()];
147+
}
148+
149+
/**
150+
* @param {TEventName} eventName
151+
*/
152+
private createEventContainer(eventName: TEventName): void {
153+
this.listenersMap.set(eventName, []);
154+
}
155+
156+
/**
157+
* @param {TEventName} eventName
158+
*/
159+
private deleteEventContainer(eventName: TEventName): void {
160+
this.listenersMap.delete(eventName);
161+
}
162+
163+
/**
164+
* @param {TEventName} eventName
165+
*/
166+
private getEventContainer(
167+
eventName: TEventName
168+
): Array<Array<TEventCallback<T>>> {
169+
return this.listenersMap.get(eventName);
170+
}
171+
172+
/**
173+
* @param {TEventName} eventName
174+
*/
175+
private hasEventContainer(eventName: TEventName): boolean {
176+
return this.listenersMap.has(eventName);
177+
}
178+
}

src/lib/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './comparator/comparator';
2+
export * from './event-emitter/event-emitter';

0 commit comments

Comments
 (0)