Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EventEmitter realisation #279

Open
wants to merge 25 commits into
base: ee
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e5e3341
improve code style
serrhiy Jan 23, 2025
1f23160
Fix logical problem with 'once' method.
serrhiy Jan 23, 2025
aa434e4
make first argument of listenerCount method optional
serrhiy Jan 23, 2025
ea7bae3
rename arguments of 'on' method
serrhiy Jan 23, 2025
cc7deab
rename arguments of 'once' method
serrhiy Jan 23, 2025
a338f55
madke method 'emit' asynchronous
serrhiy Jan 23, 2025
5e3eb6d
short fix for 'emit' method
serrhiy Jan 23, 2025
82bb30b
rename arguments of 'clear' method
serrhiy Jan 23, 2025
a6b200d
add 'toPromise' method.
serrhiy Jan 23, 2025
f620ab3
add 'toIterator' method.
serrhiy Jan 23, 2025
71c8bf5
add 'eventNames' method
serrhiy Jan 23, 2025
ab48f3e
add 'listeners' method
serrhiy Jan 23, 2025
287ad47
short fix for tests
serrhiy Jan 23, 2025
09aae3c
update metautil.d.ts file
serrhiy Jan 23, 2025
3aaa122
solving conflicts
serrhiy Jan 23, 2025
c6611e6
Make optimisations.
serrhiy Jan 24, 2025
01a02bb
make methods fit the contract decribed here https://github.com/metarh…
serrhiy Jan 24, 2025
14c9ac3
update metautil.d.ts
serrhiy Jan 24, 2025
20de33c
remove getMaxListeners method
serrhiy Jan 28, 2025
a45e236
remove 'once' function because exists 'toPromise' method
serrhiy Jan 28, 2025
f591051
make 'toPromise' method similar to Node.js 'once' (https://nodejs.org…
serrhiy Jan 28, 2025
5a4d6d6
add options for 'toIterator' method with 'signal' field. Usage example:
serrhiy Jan 28, 2025
1749c52
optimisations for 'toPromise' method
serrhiy Jan 28, 2025
6519496
change method order to fix diff
serrhiy Jan 28, 2025
213c05f
applied the tips
serrhiy Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 101 additions & 56 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,129 @@
'use strict';

class EventEmitter {
constructor(options = {}) {
constructor() {
this.events = new Map();
this.maxListeners = options.maxListeners || 10;
this.wrappers = new Map();
this.maxListenersCount = 10;
serrhiy marked this conversation as resolved.
Show resolved Hide resolved
}

emit(name, ...args) {
serrhiy marked this conversation as resolved.
Show resolved Hide resolved
const listeners = this.events.get(name);
if (!listeners) return null;
const promises = listeners.map((fn) => fn(...args));
return Promise.all(promises);
listenerCount(eventName) {
if (!eventName) {
const listeners = [...this.events.values()];
return listeners.reduce((total, cur) => total + cur.size, 0);
}
const listeners = this.events.get(eventName);
return listeners ? listeners.size : 0;
}

on(name, 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}`);
}
on(eventName, listener) {
const { events, maxListenersCount } = this;
if (!events.has(eventName)) events.set(eventName, new Set());
const listeners = this.events.get(eventName);
listeners.add(listener);
serrhiy marked this conversation as resolved.
Show resolved Hide resolved
if (listeners.size <= maxListenersCount) return;
const title = 'MaxListenersExceededWarning';
const warn = 'Possible EventEmitter memory leak detected';
const max = `Current maxListenersCount is ${maxListenersCount}`;
const hint = 'Hint: avoid adding listeners in loops';
console.warn(`${title}: ${warn}. ${max}. ${hint}`);
}

once(name, fn) {
const dispose = (...args) => {
this.off(name, dispose);
return void fn(...args);
once(eventName, listener) {
const wrapper = (...args) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispose

this.off(eventName, wrapper);
return listener(...args);
};
this.on(name, dispose);
wrapper.origin = listener;
this.on(eventName, wrapper);
this.wrappers.set(listener, wrapper);
}

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);
}
emit(eventName, ...args) {
const listeners = this.events.get(eventName);
if (!listeners) return Promise.resolve();
const promises = [...listeners].map((listener) => listener(...args));
serrhiy marked this conversation as resolved.
Show resolved Hide resolved
return Promise.all(promises).then(() => {});
serrhiy marked this conversation as resolved.
Show resolved Hide resolved
}

toPromise(name) {
return new Promise((resolve) => {
this.once(name, resolve);
});
off(eventName, listener) {
if (!listener) return void this.clear(eventName);
const listeners = this.events.get(eventName);
if (!listeners) return;
listeners.delete(listener);
const wrapped = this.wrappers.get(listener);
if (wrapped) {
listeners.delete(wrapped);
this.wrappers.delete(listener);
}
if (listeners.size === 0) this.events.delete(eventName);
}

toIterator(name) {
const stub = () => {};
this.events.on(name, stub);
return {
async next() {},
};
clear(eventName) {
const { events, wrappers } = this;
if (!eventName) {
events.clear();
return void wrappers.clear();
}
const listeners = events.get(eventName);
if (!listeners) return;
for (const listener of listeners) {
wrappers.delete(listener);
}
events.delete(eventName);
}

clear(name) {
if (!name) return void this.events.clear();
this.events.delete(name);
toPromise(eventName, options = {}) {
const { signal = null } = options;
return new Promise((resolve, reject) => {
if (!signal) {
return void this.once(eventName, (...args) => void resolve(args));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need use cases because we can't imagine all developer experience without textual representation

}
let onAbort = null;
const onSuccess = (...args) => {
signal.removeEventListener('abort', onAbort);
resolve(args);
};
onAbort = () => {
this.off(eventName, onSuccess);
const message = 'The operatopn was aborted';
reject(new Error(message, { cause: signal.reason }));
};
this.once(eventName, onSuccess);
signal.addEventListener('abort', onAbort);
Comment on lines +65 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let onAbort = null;
const onSuccess = (...args) => {
signal.removeEventListener('abort', onAbort);
resolve(args);
};
onAbort = () => {
this.off(eventName, onSuccess);
const message = 'The operatopn was aborted';
reject(new Error(message, { cause: signal.reason }));
};
this.once(eventName, onSuccess);
signal.addEventListener('abort', onAbort);
const handlers = {
success: (...args) => {
signal.removeEventListener('abort', handlers.abort);
resolve(args);
},
abort: () => {
this.off(eventName, handlers.success);
const message = 'The operation was aborted';
reject(new Error(message, { cause: signal.reason }));
},
};
this.once(eventName, handlers.success);
signal.addEventListener('abort', handlers.abort);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need research

});
}

listeners(name) {
return this.events.get(name) || [];
toIterator(eventName, options = {}) {
const { signal = null } = options;
const next = () => {
const promise = this.toPromise(eventName, { signal });
return promise.then((value) => ({ done: false, value }));
};
return { [Symbol.asyncIterator]: () => ({ next }) };
}

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

eventNames() {
return Array.from(this.events.keys());
listeners(eventName) {
const result = [];
if (!eventName) {
const eventNames = this.events.keys();
for (const name of eventNames) {
const fns = this.listeners(name);
result.push(...fns);
}
return result;
}
const listeners = this.events.get(eventName);
if (!listeners) return result;
for (const listener of listeners) {
const origin = listener.origin ?? listener;
result.push(origin);
}
return result;
}
}

Expand Down
18 changes: 12 additions & 6 deletions metautil.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,16 +278,22 @@ type Listener = (...args: unknown[]) => void;
type EventName = string | symbol;

export class EventEmitter {
maxListeners: number;
constructor();
emit(eventName: EventName, ...args: unknown[]): Promise<void>;
getMaxListeners(): number;
listenerCount(eventName?: EventName): number;
on(eventName: EventName, listener: Listener): void;
once(eventName: EventName, listener: Listener): void;
emit(eventName: EventName, ...args: unknown[]): Promise<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;
toPromise(
eventName: EventName,
options?: { signal: AbortSignal },
): Promise<unknown>;
toIterator(
eventName: EventName,
options?: { singal: AbortSignal },
): Iterator<unknown>;
eventNames(): EventName[];
listeners(eventName?: EventName): Listener[];
}
Loading