/**
 * @packageDocumentation
 *
 * Race an event against an AbortSignal, taking care to remove any event
 * listeners that were added.
 *
 * @example Getting started
 *
 * ```TypeScript
 * import { raceEvent } from 'race-event'
 *
 * const controller = new AbortController()
 * const emitter = new EventTarget()
 *
 * setTimeout(() => {
 *   controller.abort()
 * }, 500)
 *
 * setTimeout(() => {
 *   // too late
 *   emitter.dispatchEvent(new CustomEvent('event'))
 * }, 1000)
 *
 * // throws an AbortError
 * const resolve = await raceEvent(emitter, 'event', controller.signal)
 * ```
 *
 * @example Aborting the promise with an error event
 *
 * ```TypeScript
 * import { raceEvent } from 'race-event'
 *
 * const emitter = new EventTarget()
 *
 * setTimeout(() => {
 *   emitter.dispatchEvent(new CustomEvent('failure', {
 *     detail: new Error('Oh no!')
 *   }))
 * }, 1000)
 *
 * // throws 'Oh no!' error
 * const resolve = await raceEvent(emitter, 'success', AbortSignal.timeout(5000), {
 *   errorEvent: 'failure'
 * })
 * ```
 *
 * @example Customising the thrown AbortError
 *
 * The error message and `.code` property of the thrown `AbortError` can be
 * specified by passing options:
 *
 * ```TypeScript
 * import { raceEvent } from 'race-event'
 *
 * const controller = new AbortController()
 * const emitter = new EventTarget()
 *
 * setTimeout(() => {
 *   controller.abort()
 * }, 500)
 *
 * // throws a Error: Oh no!
 * const resolve = await raceEvent(emitter, 'event', controller.signal, {
 *   errorMessage: 'Oh no!',
 *   errorCode: 'ERR_OH_NO'
 * })
 * ```
 *
 * @example Only resolving on specific events
 *
 * Where multiple events with the same type are emitted, a `filter` function can
 * be passed to only resolve on one of them:
 *
 * ```TypeScript
 * import { raceEvent } from 'race-event'
 *
 * const controller = new AbortController()
 * const emitter = new EventTarget()
 *
 * // throws a Error: Oh no!
 * const resolve = await raceEvent(emitter, 'event', controller.signal, {
 *   filter: (evt: Event) => {
 *     return evt.detail.foo === 'bar'
 *   }
 * })
 * ```
 *
 * @example Terminating early by throwing from the filter
 *
 * You can cause listening for the event to cease and all event listeners to be
 * removed by throwing from the filter:
 *
 * ```TypeScript
 * import { raceEvent } from 'race-event'
 *
 * const controller = new AbortController()
 * const emitter = new EventTarget()
 *
 * // throws Error: Cannot continue
 * const resolve = await raceEvent(emitter, 'event', controller.signal, {
 *   filter: (evt) => {
 *     if (...reasons) {
 *       throw new Error('Cannot continue')
 *     }
 *
 *     return true
 *   }
 * })
 * ```
 */
import { AbortError } from 'abort-error';
/**
 * Race a promise against an abort signal
 */
export async function raceEvent(emitter, eventName, signal, opts) {
    // create the error here so we have more context in the stack trace
    const error = new AbortError(opts?.errorMessage);
    if (opts?.errorCode != null) {
        // @ts-expect-error not a field of AbortError
        error.code = opts.errorCode;
    }
    const errorEvent = opts?.errorEvent ?? 'error';
    if (signal?.aborted === true) {
        return Promise.reject(error);
    }
    return new Promise((resolve, reject) => {
        function removeListeners() {
            removeListener(signal, 'abort', abortListener);
            removeListener(emitter, eventName, eventListener);
            removeListener(emitter, errorEvent, errorEventListener);
        }
        const eventListener = (evt) => {
            try {
                if (opts?.filter?.(evt) === false) {
                    return;
                }
            }
            catch (err) {
                removeListeners();
                reject(err);
                return;
            }
            removeListeners();
            resolve(evt);
        };
        const errorEventListener = (evt) => {
            removeListeners();
            if (evt instanceof Error) {
                reject(evt);
                return;
            }
            reject(evt.detail ?? opts?.error ?? new Error(`The "${opts?.errorEvent}" event was emitted but the event had no '.detail' field. Pass an 'error' option to race-event to change this message.`));
        };
        const abortListener = () => {
            removeListeners();
            reject(error);
        };
        addListener(signal, 'abort', abortListener);
        addListener(emitter, eventName, eventListener);
        addListener(emitter, errorEvent, errorEventListener);
    });
}
function addListener(emitter, event, listener) {
    if (emitter == null) {
        return;
    }
    if (isEventTarget(emitter)) {
        emitter.addEventListener(event, listener);
    }
    else {
        emitter.addListener(event, listener);
    }
}
function removeListener(emitter, event, listener) {
    if (emitter == null) {
        return;
    }
    if (isEventTarget(emitter)) {
        emitter.removeEventListener(event, listener);
    }
    else {
        emitter.removeListener(event, listener);
    }
}
function isEventTarget(emitter) {
    return typeof emitter.addEventListener === 'function' && typeof emitter.removeEventListener === 'function';
}
//# sourceMappingURL=index.js.map