|
|
'use strict';
const anyMap = new WeakMap(); const eventsMap = new WeakMap(); const producersMap = new WeakMap(); const anyProducer = Symbol('anyProducer'); const resolvedPromise = Promise.resolve();
const listenerAdded = Symbol('listenerAdded'); const listenerRemoved = Symbol('listenerRemoved');
function assertEventName(eventName) { if (typeof eventName !== 'string' && typeof eventName !== 'symbol') { throw new TypeError('eventName must be a string or a symbol'); } }
function assertListener(listener) { if (typeof listener !== 'function') { throw new TypeError('listener must be a function'); } }
function getListeners(instance, eventName) { const events = eventsMap.get(instance); if (!events.has(eventName)) { events.set(eventName, new Set()); }
return events.get(eventName); }
function getEventProducers(instance, eventName) { const key = typeof eventName === 'string' || typeof eventName === 'symbol' ? eventName : anyProducer; const producers = producersMap.get(instance); if (!producers.has(key)) { producers.set(key, new Set()); }
return producers.get(key); }
function enqueueProducers(instance, eventName, eventData) { const producers = producersMap.get(instance); if (producers.has(eventName)) { for (const producer of producers.get(eventName)) { producer.enqueue(eventData); } }
if (producers.has(anyProducer)) { const item = Promise.all([eventName, eventData]); for (const producer of producers.get(anyProducer)) { producer.enqueue(item); } } }
function iterator(instance, eventNames) { eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
let isFinished = false; let flush = () => {}; let queue = [];
const producer = { enqueue(item) { queue.push(item); flush(); }, finish() { isFinished = true; flush(); } };
for (const eventName of eventNames) { getEventProducers(instance, eventName).add(producer); }
return { async next() { if (!queue) { return {done: true}; }
if (queue.length === 0) { if (isFinished) { queue = undefined; return this.next(); }
await new Promise(resolve => { flush = resolve; });
return this.next(); }
return { done: false, value: await queue.shift() }; },
async return(value) { queue = undefined;
for (const eventName of eventNames) { getEventProducers(instance, eventName).delete(producer); }
flush();
return arguments.length > 0 ? {done: true, value: await value} : {done: true}; },
[Symbol.asyncIterator]() { return this; } }; }
function defaultMethodNamesOrAssert(methodNames) { if (methodNames === undefined) { return allEmitteryMethods; }
if (!Array.isArray(methodNames)) { throw new TypeError('`methodNames` must be an array of strings'); }
for (const methodName of methodNames) { if (!allEmitteryMethods.includes(methodName)) { if (typeof methodName !== 'string') { throw new TypeError('`methodNames` element must be a string'); }
throw new Error(`${methodName} is not Emittery method`); } }
return methodNames; }
const isListenerSymbol = symbol => symbol === listenerAdded || symbol === listenerRemoved;
class Emittery { static mixin(emitteryPropertyName, methodNames) { methodNames = defaultMethodNamesOrAssert(methodNames); return target => { if (typeof target !== 'function') { throw new TypeError('`target` must be function'); }
for (const methodName of methodNames) { if (target.prototype[methodName] !== undefined) { throw new Error(`The property \`${methodName}\` already exists on \`target\``); } }
function getEmitteryProperty() { Object.defineProperty(this, emitteryPropertyName, { enumerable: false, value: new Emittery() }); return this[emitteryPropertyName]; }
Object.defineProperty(target.prototype, emitteryPropertyName, { enumerable: false, get: getEmitteryProperty });
const emitteryMethodCaller = methodName => function (...args) { return this[emitteryPropertyName][methodName](...args); };
for (const methodName of methodNames) { Object.defineProperty(target.prototype, methodName, { enumerable: false, value: emitteryMethodCaller(methodName) }); }
return target; }; }
constructor() { anyMap.set(this, new Set()); eventsMap.set(this, new Map()); producersMap.set(this, new Map()); }
on(eventNames, listener) { assertListener(listener);
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; for (const eventName of eventNames) { assertEventName(eventName); getListeners(this, eventName).add(listener);
if (!isListenerSymbol(eventName)) { this.emit(listenerAdded, {eventName, listener}); } }
return this.off.bind(this, eventNames, listener); }
off(eventNames, listener) { assertListener(listener);
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; for (const eventName of eventNames) { assertEventName(eventName); getListeners(this, eventName).delete(listener);
if (!isListenerSymbol(eventName)) { this.emit(listenerRemoved, {eventName, listener}); } } }
once(eventNames) { return new Promise(resolve => { const off = this.on(eventNames, data => { off(); resolve(data); }); }); }
events(eventNames) { eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; for (const eventName of eventNames) { assertEventName(eventName); }
return iterator(this, eventNames); }
async emit(eventName, eventData) { assertEventName(eventName);
enqueueProducers(this, eventName, eventData);
const listeners = getListeners(this, eventName); const anyListeners = anyMap.get(this); const staticListeners = [...listeners]; const staticAnyListeners = isListenerSymbol(eventName) ? [] : [...anyListeners];
await resolvedPromise; await Promise.all([ ...staticListeners.map(async listener => { if (listeners.has(listener)) { return listener(eventData); } }), ...staticAnyListeners.map(async listener => { if (anyListeners.has(listener)) { return listener(eventName, eventData); } }) ]); }
async emitSerial(eventName, eventData) { assertEventName(eventName);
const listeners = getListeners(this, eventName); const anyListeners = anyMap.get(this); const staticListeners = [...listeners]; const staticAnyListeners = [...anyListeners];
await resolvedPromise; /* eslint-disable no-await-in-loop */ for (const listener of staticListeners) { if (listeners.has(listener)) { await listener(eventData); } }
for (const listener of staticAnyListeners) { if (anyListeners.has(listener)) { await listener(eventName, eventData); } } /* eslint-enable no-await-in-loop */ }
onAny(listener) { assertListener(listener); anyMap.get(this).add(listener); this.emit(listenerAdded, {listener}); return this.offAny.bind(this, listener); }
anyEvent() { return iterator(this); }
offAny(listener) { assertListener(listener); this.emit(listenerRemoved, {listener}); anyMap.get(this).delete(listener); }
clearListeners(eventNames) { eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
for (const eventName of eventNames) { if (typeof eventName === 'string' || typeof eventName === 'symbol') { getListeners(this, eventName).clear();
const producers = getEventProducers(this, eventName);
for (const producer of producers) { producer.finish(); }
producers.clear(); } else { anyMap.get(this).clear();
for (const listeners of eventsMap.get(this).values()) { listeners.clear(); }
for (const producers of producersMap.get(this).values()) { for (const producer of producers) { producer.finish(); }
producers.clear(); } } } }
listenerCount(eventNames) { eventNames = Array.isArray(eventNames) ? eventNames : [eventNames]; let count = 0;
for (const eventName of eventNames) { if (typeof eventName === 'string') { count += anyMap.get(this).size + getListeners(this, eventName).size + getEventProducers(this, eventName).size + getEventProducers(this).size; continue; }
if (typeof eventName !== 'undefined') { assertEventName(eventName); }
count += anyMap.get(this).size;
for (const value of eventsMap.get(this).values()) { count += value.size; }
for (const value of producersMap.get(this).values()) { count += value.size; } }
return count; }
bindMethods(target, methodNames) { if (typeof target !== 'object' || target === null) { throw new TypeError('`target` must be an object'); }
methodNames = defaultMethodNamesOrAssert(methodNames);
for (const methodName of methodNames) { if (target[methodName] !== undefined) { throw new Error(`The property \`${methodName}\` already exists on \`target\``); }
Object.defineProperty(target, methodName, { enumerable: false, value: this[methodName].bind(this) }); } } }
const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor');
// Subclass used to encourage TS users to type their events.
Emittery.Typed = class extends Emittery {}; Object.defineProperty(Emittery.Typed, 'Typed', { enumerable: false, value: undefined });
Object.defineProperty(Emittery, 'listenerAdded', { value: listenerAdded, writable: false, enumerable: true, configurable: false }); Object.defineProperty(Emittery, 'listenerRemoved', { value: listenerRemoved, writable: false, enumerable: true, configurable: false });
module.exports = Emittery;
|