race-event
Version:
Race an event against an AbortSignal
170 lines • 4.59 kB
JavaScript
/**
* @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
* }
* })
* ```
*/
/**
* An abort error class that extends error
*/
export class AbortError extends Error {
type;
code;
constructor(message, code) {
super(message ?? 'The operation was aborted');
this.type = 'aborted';
this.name = 'AbortError';
this.code = code ?? 'ABORT_ERR';
}
}
/**
* 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, opts?.errorCode);
if (signal?.aborted === true) {
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
function removeListeners() {
signal?.removeEventListener('abort', abortListener);
emitter.removeEventListener(eventName, eventListener);
if (opts?.errorEvent != null) {
emitter.removeEventListener(opts.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();
reject(evt.detail);
};
const abortListener = () => {
removeListeners();
reject(error);
};
signal?.addEventListener('abort', abortListener);
emitter.addEventListener(eventName, eventListener);
if (opts?.errorEvent != null) {
emitter.addEventListener(opts.errorEvent, errorEventListener);
}
});
}
//# sourceMappingURL=index.js.map