You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
408 lines
9.3 KiB
408 lines
9.3 KiB
'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'); |
|
|
|
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;
|
|
|