Skip to content

Commit

Permalink
Refactor EventEmitter to new contract
Browse files Browse the repository at this point in the history
Refs: #267
  • Loading branch information
tshemsedinov committed Dec 11, 2024
1 parent d90f5ea commit 63fde2f
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 53 deletions.
94 changes: 55 additions & 39 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,85 @@
'use strict';

class EventEmitter {
constructor() {
constructor(options = {}) {
this.events = new Map();
this.maxListenersCount = 10;
this.maxListeners = options.maxListeners || 10;
}

getMaxListeners() {
return this.maxListenersCount;
}

listenerCount(name) {
const event = this.events.get(name);
if (event) return event.size;
return 0;
emit(name, ...args) {
const listeners = this.events.get(name);
if (!listeners) return null;
const promises = listeners.map((fn) => fn(...args));
return Promise.all(promises);
}

on(name, fn) {
const event = this.events.get(name);
if (event) {
event.add(fn);
const tooManyListeners = event.size > this.maxListenersCount;
if (tooManyListeners) {
const name = 'MaxListenersExceededWarning';
const warn = 'Possible EventEmitter memory leak detected';
const max = `Current maxListenersCount is ${this.maxListenersCount}`;
const hint = 'Hint: avoid adding listeners in loops';
console.warn(`${name}: ${warn}. ${max}. ${hint}`);
}
} else {
this.events.set(name, new Set([fn]));
const listeners = this.events.get(name);
if (!listeners) return void this.events.set(name, [fn]);
if (listeners.includes(fn)) {
console.warn(`Duplicate listeners detected: ${fn.name}`);
}
listeners.push(fn);
const tooManyListeners = listeners.size > this.maxListeners;
if (tooManyListeners) {
const name = 'MaxListenersExceededWarning';
const warn = 'Possible EventEmitter memory leak detected';
const max = `Current maxListenersCount is ${this.maxListeners}`;
const hint = 'Hint: avoid adding listeners in loops';
console.warn(`${name}: ${warn}. ${max}. ${hint}`);
}
}

once(name, fn) {
const dispose = (...args) => {
this.remove(name, dispose);
this.off(name, dispose);
return void fn(...args);
};
this.on(name, dispose);
}

emit(name, ...args) {
const event = this.events.get(name);
if (!event) return;
for (const fn of event.values()) {
fn(...args);
off(name, fn) {
const listeners = this.events.get(name);
if (!listeners) return;
const index = listeners.indexOf(fn);
listeners.splice(index, 1);
if (listeners.length === 0) {
this.events.delete(name);
}
}

remove(name, fn) {
const event = this.events.get(name);
if (!event) return;
event.delete(fn);
toPromise(name) {
return new Promise((resolve) => {
this.once(name, resolve);
});
}

toIterator(name) {
const stub = () => {};
this.events.on(name, stub);
return {
async next() {},
};
}

clear(name) {
if (!name) return void this.events.clear();
this.events.delete(name);
}
}

const once = (emitter, name) =>
new Promise((resolve) => {
emitter.once(name, resolve);
});
listeners(name) {
return this.events.get(name) || [];
}

listenerCount(name) {
const listeners = this.events.get(name);
if (listeners) return listeners.length;
return 0;
}

eventNames() {
return Array.from(this.events.keys());
}
}

module.exports = { EventEmitter, once };
module.exports = { EventEmitter };
23 changes: 14 additions & 9 deletions metautil.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,20 @@ export function collect(

// Submodule: Events

type Listener = (...args: unknown[]) => void;
type EventName = string | symbol;

export class EventEmitter {
maxListeners: number;
constructor();
getMaxListeners(): number;
listenerCount(name: string): number;
on(name: string, fn: Function): void;
once(name: string, fn: Function): void;
emit(name: string, ...args: Array<unknown>): void;
remove(name: string, fn: Function): void;
clear(name: string): void;
emit(eventName: EventName, ...args: unknown[]): Promise<void>;
on(eventName: EventName, listener: Listener): void;
once(eventName: EventName, listener: Listener): void;
off(eventName: EventName, listener?: Listener): void;
toPromise(eventName: EventName): Promise<unknown>;
toIterator(eventName: EventName): Iterator<unknown>;
clear(eventName?: EventName): void;
listeners(eventName?: EventName): Listener[];
listenerCount(eventName?: EventName): number;
eventNames(): EventName[];
}

export function once(emitter: EventEmitter, name: string): Promise<unknown>;
10 changes: 5 additions & 5 deletions test/events.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use strict';

const metatests = require('metatests');
const metautil = require('..');
const { EventEmitter } = require('..');

metatests.test('EventEmitter', async (test) => {
const ee = new metautil.EventEmitter();
const ee = new EventEmitter();

test.strictSame(ee.getMaxListeners(), 10);
test.strictSame(ee.maxListeners, 10);
test.assert(ee.events instanceof Map);

let onCount = 0;
Expand Down Expand Up @@ -40,7 +40,7 @@ metatests.test('EventEmitter', async (test) => {
test.strictSame(count, 1);

test.strictSame(ee.listenerCount('name1'), 2);
ee.remove('name1', fn);
ee.off('name1', fn);
test.strictSame(ee.listenerCount('name1'), 1);

ee.emit('name1', 'value');
Expand All @@ -53,7 +53,7 @@ metatests.test('EventEmitter', async (test) => {
ee.emit('name3', 'value');
}, 50);

const result = await metautil.once(ee, 'name3');
const result = await ee.toPromise('name3');
test.strictSame(result, 'value');

ee.clear();
Expand Down

0 comments on commit 63fde2f

Please sign in to comment.