metautil
Version:
Metarhia utilities
181 lines (154 loc) • 4.37 kB
JavaScript
'use strict';
const DONE = { done: true, value: undefined };
class EventIterator {
#resolvers = [];
#emitter = null;
#eventName = '';
#listener = null;
#onerror = null;
#done = false;
constructor(emitter, eventName) {
this.#emitter = emitter;
this.#eventName = eventName;
this.#listener = (value) => {
for (const resolver of this.#resolvers) {
resolver.resolve({ done: this.#done, value });
}
};
emitter.on(eventName, this.#listener);
this.#onerror = (error) => {
for (const resolver of this.#resolvers) {
resolver.reject(error);
}
this.#finalize();
};
emitter.on('error', this.#onerror);
}
next() {
return new Promise((resolve, reject) => {
if (this.#done) return void resolve(DONE);
this.#resolvers.push({ resolve, reject });
});
}
#finalize() {
if (this.#done) return;
this.#done = true;
this.#emitter.off(this.#eventName, this.#listener);
this.#emitter.off('error', this.#onerror);
for (const resolver of this.#resolvers) {
resolver.resolve(DONE);
}
this.#resolvers.length = 0;
}
async return() {
this.#finalize();
return DONE;
}
async throw() {
this.#finalize();
return DONE;
}
}
class EventIterable {
#emitter = null;
#eventName = '';
constructor(emitter, eventName) {
this.#emitter = emitter;
this.#eventName = eventName;
}
[Symbol.asyncIterator]() {
return new EventIterator(this.#emitter, this.#eventName);
}
}
class Emitter {
#events = new Map();
#maxListeners = 10;
constructor(options = {}) {
this.#maxListeners = options.maxListeners ?? 10;
}
emit(eventName, value) {
const event = this.#events.get(eventName);
if (!event) {
if (eventName !== 'error') return Promise.resolve();
throw new Error('Unhandled error');
}
const on = event.on.slice();
const promises = on.map(async (fn) => fn(value));
if (event.once.size > 0) {
const len = event.on.length;
const on = new Array(len);
let index = 0;
for (let i = 0; i < len; i++) {
const listener = event.on[i];
if (!event.once.has(listener)) on[index++] = listener;
}
if (index === 0) {
this.#events.delete(eventName);
return Promise.resolve();
}
on.length = index;
this.#events.set(eventName, { on, once: new Set() });
}
return Promise.all(promises).then(() => undefined);
}
#addListener(eventName, listener, once) {
let event = this.#events.get(eventName);
if (!event) {
const on = [listener];
event = { on, once: once ? new Set(on) : new Set() };
this.#events.set(eventName, event);
} else {
if (event.on.includes(listener)) {
throw new Error('Duplicate listeners detected');
}
event.on.push(listener);
if (once) event.once.add(listener);
}
if (event.on.length > this.#maxListeners) {
throw new Error(
`MaxListenersExceededWarning: Possible memory leak. ` +
`Current maxListeners is ${this.#maxListeners}.`,
);
}
}
on(eventName, listener) {
this.#addListener(eventName, listener, false);
}
once(eventName, listener) {
this.#addListener(eventName, listener, true);
}
off(eventName, listener) {
if (!listener) return void this.#events.delete(eventName);
const event = this.#events.get(eventName);
if (!event) return;
const index = event.on.indexOf(listener);
if (index > -1) event.on.splice(index, 1);
event.once.delete(listener);
}
toPromise(eventName) {
return new Promise((resolve) => {
this.once(eventName, resolve);
});
}
toAsyncIterable(eventName) {
return new EventIterable(this, eventName);
}
clear(eventName) {
if (!eventName) return void this.#events.clear();
this.#events.delete(eventName);
}
listeners(eventName) {
if (!eventName) throw new Error('Expected eventName');
const event = this.#events.get(eventName);
return event ? event.on : [];
}
listenerCount(eventName) {
if (!eventName) throw new Error('Expected eventName');
const event = this.#events.get(eventName);
return event ? event.on.length : 0;
}
eventNames() {
return Array.from(this.#events.keys());
}
}
module.exports = { Emitter };