Source

emitter.js

class EventEntry {
    constructor() {
        this.children = new Map()
        this.handlers = []
        this.allBelow = []
    }

    getChild(key) {
        let result
        if (this.children.has(key)) return this.children.get(key)
        this.children.set(key, (result = new EventEntry()))
        return result
    }

    get all() {
        return (this._all = this._all || new EventEntry())
    }
}

function prepareNames(names, separator) {
    if (typeof name === "string") {
        return names.split(",")
    } else if (Array.isArray(names)) {
        return names.map((name) => name.split(",")).flat(Infinity)
    } else {
        throw new Error("Invalid pattern" + names)
    }
}

/**
 * @callback HandlePreparer
 *
 * @param {Array<Function>} handlers - the handlers being used
 * @return an updated array or the original array sorted
 */

/**
 * @interface ConstructorParams
 * @property {string} [delimiter=.] - a character which delimits parts of an event pattern
 * @property {string} [wildcard=*] - a wildcard indicator used to handle any parts of a pattern
 * @property {string} [separator=,] - a character to separate multiple events in the same pattern
 * @property {HandlePreparer} [prepareHandlers=v=>v] - a function to modify the handlers just before raising,
 * this is the combined set of all of the handlers that will be raised.
 * @property {HandlePreparer} [storeHandlers=v=>v] - a function to modify or sort the handlers before storing,

 */

/**
 * Event emitter with wild card support and delimited entries.
 */
export class Events {
    /**
     * Constructs an event emitter
     * @param {ConstructorParams} [props] - parameters to configure the emitter
     */
    constructor({
        delimiter = ".",
        wildcard = "*",
        separator = ",",
        prepareHandlers = (v) => v,
        storeHandlers = (v) => v
    } = {}) {
        this.delimiter = delimiter
        this.wildcard = wildcard
        this.separator = separator
        this.doubleWild = `${wildcard}${wildcard}`
        this.events = new EventEntry()
        this.prepareHandlers = prepareHandlers
        this.storeHandlers = storeHandlers
    }

    /**
     * Adds an event listener with wildcards etc
     * @instance
     * @memberOf Events
     * @param {string|Array<string>} names - the event patterns to handle
     * @param {Function} handler - the handler for the pattern
     */
    on(names, handler) {
        for (let name of prepareNames(names, this.separator)) {
            const parts = name.split(this.delimiter)
            let scan = this.events
            for (let i = 0, l = parts.length; i < l; i++) {
                const part = parts[i]
                switch (part) {
                    case this.wildcard:
                        scan = scan.all
                        break
                    case this.doubleWild:
                        scan.allBelow.push(handler)
                        scan.allBelow = this.storeHandlers(scan.allBelow)
                        return
                    default:
                        scan = scan.getChild(part)
                        break
                }
            }
            scan.handlers.push(handler)
            scan.handlers = this.storeHandlers(scan.handlers)
        }
    }

    /**
     * Add an event listener that will fire only once, if multiple
     * patterns are provided it will only fire on the first one
     * @param {string|Array<string>} name - the event pattern to listen for
     * @param {Function} handler - the function to invoke
     */
    once(name, handler) {
        const self = this
        self.on(name, process)

        function process(...params) {
            self.off(name, process)
            handler(...params)
        }
    }

    /**
     * Removes a listener from a pattern
     * @param {string|Array<string>} names - the pattern(s) of the handler to remove
     * @param {Function} [handler] - the handler to remove, or all handlers
     */
    off(names, handler) {
        for (let name of prepareNames(names, this.separator)) {
            const parts = name.split(this.delimiter)
            let scan = this.events
            for (let i = 0, l = parts.length; i < l; i++) {
                const part = parts[i]
                switch (part) {
                    case this.wildcard:
                        scan = scan.all
                        break
                    case this.doubleWild: {
                        if (handler === undefined) {
                            scan.allBelow = []
                            return
                        }
                        const idx = scan.allBelow.indexOf(handler)
                        if (idx === -1) return
                        scan.allBelow.splice(idx, 1)
                        return
                    }
                    default:
                        scan = scan.getChild(part)
                        break
                }
            }

            if (handler !== undefined) {
                const idx = scan.handlers.indexOf(handler)
                if (idx === -1) return
                scan.handlers.splice(idx, 1)
            } else {
                scan.handlers = []
            }
        }
    }

    _emit(scan, parts, index, handlers) {
        if (index >= parts.length) {
            handlers.push(...scan.handlers)
            return
        }
        handlers.push(...scan.allBelow)
        this._emit(scan.all, parts, index + 1, handlers)
        this._emit(scan.getChild(parts[index]), parts, index + 1, handlers)
    }

    _callHandlers(handlerList, params) {
        for (const handler of handlerList) {
            handler.apply(this, params)
        }
    }

    async _callHandlersAsync(handlerList, params) {
        for (const handler of handlerList) {
            await handler.apply(this, params)
        }
    }

    async _callHandlersAsyncAtOnce(handlerList, params) {
        const promises = []
        for (const handler of handlerList) {
            promises.push(Promise.resolve(handler.apply(this, params)))
        }
        await Promise.all(promises)
    }

    /**
     * Emits an event synchronously
     * @param {string} event - the event to emit
     * @param {...params} params - the parameters to call the event with
     * @returns {Array<any>} - an array of the parameters the event was called with
     */
    emit(event, ...params) {
        const handlers = []
        this.event = event
        const parts = event.split(this.delimiter)
        this._emit(this.events, parts, 0, handlers)
        const toExecute = this.prepareHandlers(handlers)
        this._callHandlers(toExecute, params)
        return params
    }

    /**
     * Emits events asynchronously, in order, sequentially
     * @param {string} event - the event to emit
     * @param {...params} params - the parameters to call the event with
     * @returns {Array<any>} - an array of the parameters the event was called with
     */
    async emitAsync(event, ...params) {
        const handlers = []
        this.event = event
        const parts = event.split(this.delimiter)
        this._emit(this.events, parts, 0, handlers)
        const toExecute = this.prepareHandlers(handlers)
        await this._callHandlersAsync(toExecute, params)
        return params
    }

    /**
     * Emits events asynchronously, in parallel
     * @param {string} event - the event to emit
     * @param {...params} params - the parameters to call the event with
     * @returns {Array<any>} - an array of the parameters the event was called with
     */
    async emitAtOnce(event, ...params) {
        const handlers = []
        this.event = event
        const parts = event.split(this.delimiter)
        this._emit(this.events, parts, 0, handlers)
        const toExecute = this.prepareHandlers(handlers)
        await this._callHandlersAsyncAtOnce(toExecute, params)
        return params
    }
}

Events.prototype.addEventListener = Events.prototype.on
Events.prototype.removeEventListener = Events.prototype.off
Events.prototype.addListener = Events.prototype.on
Events.prototype.removeListener = Events.prototype.off

export default Events