"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventEmitter = exports.createEmitter = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const index_js_1 = require("../util/index.js");
/**
 * Create an {@link ISubscribable} that can be instructed to emit events via the {@link IEmitter} interface.
 *
 * A class can delegate handling {@link ISubscribable} to the returned value while using it to emit the events.
 * See also `EventEmitter` which be used as a base class to implement {@link ISubscribable} via extension.
 * @internal
 */
function createEmitter(noListeners) {
    return new ComposableEventEmitter(noListeners);
}
exports.createEmitter = createEmitter;
/**
 * Provides an API for subscribing to and listening to events.
 *
 * @remarks Classes wishing to emit events may either extend this class or compose over it.
 *
 * @example Extending this class
 *
 * ```typescript
 * interface MyEvents {
 *   "loaded": () => void;
 * }
 *
 * class MyClass extends EventEmitter<MyEvents> {
 *   private load() {
 *     this.emit("loaded");
 *   }
 * }
 * ```
 *
 * @example Composing over this class
 *
 * ```typescript
 * class MyClass implements ISubscribable<MyEvents> {
 *   private readonly events = EventEmitter.create<MyEvents>();
 *
 *   private load() {
 *     this.events.emit("loaded");
 *   }
 *
 *   public on<K extends keyof MyEvents>(eventName: K, listener: MyEvents[K]): () => void {
 *     return this.events.on(eventName, listener);
 *   }
 * }
 * ```
 */
class EventEmitter {
    // Because this is protected and not public, calling this externally (not from a subclass) makes sending events to the constructed instance impossible.
    // Instead, use the static `create` function to get an instance which allows emitting events.
    constructor(noListeners) {
        this.noListeners = noListeners;
        // TODO: because the inner data-structure here is a set, adding the same callback twice does not error,
        // but only calls it once, and unsubscribing will stop calling it all together.
        // This is surprising since it makes subscribing and unsubscribing not inverses (but instead both idempotent).
        // This might be desired, but if so the documentation should indicate it.
        this.listeners = new Map();
    }
    emit(eventName, ...args) {
        const listeners = this.listeners.get(eventName);
        if (listeners !== undefined) {
            const argArray = args; // TODO: Current TS (4.5.5) cannot spread `args` into `listener()`, but future versions (e.g. 4.8.4) can.
            // This explicitly copies listeners so that new listeners added during this call to emit will not receive this event.
            for (const listener of [...listeners]) {
                // If listener has been unsubscribed while invoking other listeners, skip it.
                if (listeners.has(listener)) {
                    listener(...argArray);
                }
            }
        }
    }
    emitAndCollect(eventName, ...args) {
        const listeners = this.listeners.get(eventName);
        if (listeners !== undefined) {
            const argArray = args;
            const resultArray = [];
            for (const listener of listeners.values()) {
                resultArray.push(listener(...argArray));
            }
            return resultArray;
        }
        return [];
    }
    /**
     * Register an event listener.
     * @param eventName - the name of the event
     * @param listener - the handler to run when the event is fired by the emitter
     * @returns a function which will deregister the listener when run.
     * This function will error if called more than once.
     * @privateRemarks
     * TODO:
     * invoking the returned callback can error even if its only called once if the same listener was provided to two calls to "on".
     * This behavior is not documented and its unclear if its a bug or not: see note on listeners.
     */
    on(eventName, listener) {
        (0, index_js_1.getOrCreate)(this.listeners, eventName, () => new Set()).add(listener);
        return () => this.off(eventName, listener);
    }
    off(eventName, listener) {
        const listeners = this.listeners.get(eventName) ??
            // TODO: consider making this (and assert below) a usage error since it can be triggered by users of the public API: maybe separate those use cases somehow?
            (0, index_js_1.fail)("Event has no listeners. Event deregistration functions may only be invoked once.");
        (0, core_utils_1.assert)(listeners.delete(listener), 0x4c1 /* Listener does not exist. Event deregistration functions may only be invoked once. */);
        if (listeners.size === 0) {
            this.listeners.delete(eventName);
            this.noListeners?.(eventName);
        }
    }
    hasListeners(eventName) {
        if (eventName === undefined) {
            return this.listeners.size !== 0;
        }
        return this.listeners.has(eventName);
    }
}
exports.EventEmitter = EventEmitter;
// This class exposes the constructor and the `emit` method of `EventEmitter`, elevating them from protected to public
class ComposableEventEmitter extends EventEmitter {
    constructor(noListeners) {
        super(noListeners);
    }
    emit(eventName, ...args) {
        return super.emit(eventName, ...args);
    }
    emitAndCollect(eventName, ...args) {
        return super.emitAndCollect(eventName, ...args);
    }
}
//# sourceMappingURL=events.js.map