@quilted/events
Advanced tools
Comparing version 0.1.18 to 1.0.0
@@ -1,18 +0,5 @@ | ||
export type AbortBehavior = 'throws' | 'returns'; | ||
export declare class NestedAbortController extends AbortController { | ||
private readonly cleanup; | ||
constructor(parent?: AbortSignal); | ||
abort(reason?: any): void; | ||
} | ||
export declare function anyAbortSignal(...signals: readonly AbortSignal[]): AbortSignal; | ||
export declare function raceAgainstAbortSignal<T>(race: () => Promise<T>, { signal, ifAborted, }: { | ||
signal: AbortSignal; | ||
ifAborted?(): void | Promise<void>; | ||
}): Promise<T>; | ||
export declare class AbortError extends Error { | ||
static test(error: unknown): error is AbortError; | ||
readonly code = "ABORT_ERR"; | ||
readonly name = "AbortError"; | ||
constructor(message?: string); | ||
} | ||
export { AbortError } from './abort/AbortError.ts'; | ||
export { NestedAbortController } from './abort/NestedAbortController.ts'; | ||
export { TimedAbortController } from './abort/TimedAbortController.ts'; | ||
export { raceAgainstAbortSignal } from './abort/race.ts'; | ||
//# sourceMappingURL=abort.d.ts.map |
@@ -1,44 +0,200 @@ | ||
import type { AbortBehavior } from './abort.ts'; | ||
export type EmitterHandler<T = unknown> = (event: T) => void; | ||
export interface Emitter<Events = {}> { | ||
on<Event extends keyof Events>(type: Event, options: { | ||
once: true; | ||
signal?: never; | ||
abort?: never; | ||
}): Promise<Events[Event]>; | ||
on<Event extends keyof Events, Abort extends AbortBehavior = 'returns'>(type: Event, options: { | ||
once: true; | ||
signal: AbortSignal; | ||
abort?: Abort; | ||
}): Promise<Abort extends 'returns' ? Events[Event] | undefined : Events[Event]>; | ||
on<Event extends keyof Events>(type: Event, options?: { | ||
once?: boolean; | ||
import type { AbortBehavior, EventHandler, EventHandlerMap, EventTarget } from './types.ts'; | ||
/** | ||
* Internal events triggered by an `EventEmitter` as handlers are | ||
* added and removed. | ||
*/ | ||
export interface EmitterEmitterInternalEvents<Events extends EventHandlerMap = {}> { | ||
add: { | ||
readonly event: keyof Events; | ||
readonly all: Set<EventHandler<Events[keyof Events]>>; | ||
readonly handler: EventHandler<Events[keyof Events]>; | ||
}; | ||
remove: { | ||
readonly event: keyof Events; | ||
readonly all: Set<EventHandler<Events[keyof Events]>>; | ||
readonly handler: EventHandler<Events[keyof Events]>; | ||
}; | ||
} | ||
/** | ||
* An object that can emit events. This is a minimal alternative to the `EventTarget` | ||
* interface available in most JavaScript environments. Unlike the callback functions | ||
* you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support | ||
* event propagation, but does provide convenient `once()` and `on()` methods for | ||
* converting event listeners into into `Promise`s and `AsyncGenerator`s. | ||
*/ | ||
export declare class EventEmitter<Events extends EventHandlerMap = {}> { | ||
/** | ||
* A map containing the event handlers registered for events on this | ||
* emitter. You should not directly mutate this map, but you can use it | ||
* to introspect the number of handlers currently registered. | ||
*/ | ||
readonly all: Map<keyof Events, Set<EventHandler<Events[keyof Events]>>>; | ||
/** | ||
* A reference to an `EventTarget` that is being wrapped with this `EventEmitter`. | ||
* As handlers are added for events on the emitter, matching events are listened | ||
* for lazily on the `EventTarget`. This is useful for converting event producers | ||
* in existing environments into objects that can be used with `once()` and `on()`. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* const emitter = new EventEmitter(button); | ||
* // emitter.eventTarget === button | ||
* | ||
* const click = await emitter.once('click'); | ||
* console.log('clicked!', click); | ||
*/ | ||
readonly eventTarget: EventTarget<Events> | undefined; | ||
/** | ||
* An `EventEmitter` that triggers events when handlers are added | ||
* or removed from this emitter. This is useful for debugging, and for | ||
* building higher-level abstractions that need to know when handlers | ||
* are registered for a given event. | ||
*/ | ||
get internal(): Pick<EventEmitter<Events>, 'on' | 'once'>; | ||
private _internal?; | ||
private handleEvent; | ||
constructor(eventTarget?: EventTarget<Events>); | ||
/** | ||
* Listens to an `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns an `AsyncGenerator` | ||
* that yields each event as it is emitted. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns An `AsyncGenerator` that yields each event as it is emitted. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* for await (const message of emitter.on('message')) { | ||
* console.log('message', message); | ||
* } | ||
*/ | ||
on<Event extends keyof Events>(event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the iterator to complete. If set to `'reject'`, the iterator | ||
* will throw an `AbortError` when the signal is aborted instead. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): AsyncGenerator<Events[Event], void, void>; | ||
on<Event extends keyof Events>(type: Event, handler: EmitterHandler<Events[Event]>, options?: { | ||
once?: boolean; | ||
/** | ||
* Listens for `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* @param handler The function to call when the event is triggered. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* emitter.on('message', (message) => { | ||
* console.log('message', message); | ||
* }); | ||
*/ | ||
on<Event extends keyof Events>(event: Event, handler: EventHandler<Events[Event]>, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
abort?: never; | ||
}): void; | ||
emit<Event extends keyof Events>(type: Event, event: Events[Event]): void; | ||
emit<Event extends keyof Events>(type: undefined extends Events[Event] ? Event : never): void; | ||
/** | ||
* Listens for the next matching `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns A `Promise` that resolves when the next matching event is triggered. | ||
* The promise resolves with the value of the first argument passed to the event. | ||
* If you pass the `signal` option, the promise may also resolve with `undefined` | ||
* (in case the signal is aborted before the event is emitted). If you also set | ||
* the `abort` option to `'reject'`, the promise will instead reject with an | ||
* `AbortError` when the signal is aborted. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* const nextMessage = await emitter.once('message'); | ||
* console.log('message', nextMessage); | ||
*/ | ||
once<Event extends keyof Events>(event: Event, options: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. When set to `'reject'`, | ||
* the promise will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort: 'reject'; | ||
}): Promise<Events[Event]>; | ||
once<Event extends keyof Events>(event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: never; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): Promise<Events[Event]>; | ||
once<Event extends keyof Events>(event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): Promise<Events[Event] | undefined>; | ||
/** | ||
* Listens for the next matching `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* @param handler The function to call when the event is triggered. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* emitter.once('message', (message) => { | ||
* console.log('message', message); | ||
* }); | ||
*/ | ||
once<Event extends keyof Events>(event: Event, handler: EventHandler<Events[Event]>, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): void; | ||
/** | ||
* Emits an event with the given `data`. This will trigger all handlers | ||
* listening for the provided `event`. | ||
*/ | ||
emit<Event extends keyof Events>(event: Event, data?: Events[Event]): void; | ||
} | ||
export declare function createEmitter<Events = {}>(internal?: Emitter<EmitterInternalEvents<Events>>): Emitter<Events>; | ||
export interface EmitterInternalEvents<Events = {}> { | ||
add: { | ||
event: keyof Events; | ||
handler: EmitterHandler<unknown>; | ||
all: Set<EmitterHandler<unknown>>; | ||
}; | ||
remove: { | ||
event: keyof Events; | ||
handler: EmitterHandler<unknown>; | ||
all: Set<EmitterHandler<unknown>>; | ||
}; | ||
} | ||
export interface EmitterWithInternals<Events = {}> extends Emitter<Events> { | ||
readonly internal: Emitter<EmitterInternalEvents<Events>>; | ||
} | ||
export declare function createEmitterWithInternals<Events = {}>(): EmitterWithInternals<Events>; | ||
/** | ||
* Creates an object that can emit events. This is a minimal alternative to the `EventTarget` | ||
* interface available in most JavaScript environments. Unlike the callback functions | ||
* you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support | ||
* event propagation, but does provide convenient `once()` and `on()` methods for | ||
* converting event listeners into into `Promise`s and `AsyncGenerator`s. | ||
*/ | ||
export declare function createEventEmitter<Events extends EventHandlerMap = {}>(): EventEmitter<Events>; | ||
//# sourceMappingURL=emitter.d.ts.map |
export { on } from './on.ts'; | ||
export { once } from './once.ts'; | ||
export { AbortError, NestedAbortController, anyAbortSignal, raceAgainstAbortSignal, type AbortBehavior, } from './abort.ts'; | ||
export { addListener } from './listeners.ts'; | ||
export { createEmitter, createEmitterWithInternals } from './emitter.ts'; | ||
export type { Emitter, EmitterHandler, EmitterInternalEvents, EmitterWithInternals, } from './emitter.ts'; | ||
export { TimedAbortController, sleep } from './timeouts.ts'; | ||
export type { EventTarget, EventTargetOn, EventTargetAddEventListener, EventTargetFunction, } from './types.ts'; | ||
export { AbortError, NestedAbortController, TimedAbortController, raceAgainstAbortSignal, } from './abort.ts'; | ||
export { addEventHandler } from './handler.ts'; | ||
export { EventEmitter, createEventEmitter, type EmitterEmitterInternalEvents, } from './emitter.ts'; | ||
export { sleep } from './sleep.ts'; | ||
export type { AbortBehavior, EventHandler, EventHandlerMap, EventTarget, EventTargetOn, EventTargetAddEventListener, EventTargetFunction, } from './types.ts'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,7 +0,31 @@ | ||
import type { AbortBehavior } from './abort.ts'; | ||
import type { EventTarget } from './types.ts'; | ||
export declare function on<EventMap = Record<string, unknown>, Event extends keyof EventMap = keyof EventMap, Target extends EventTarget<EventMap> = EventTarget<EventMap>>(emitter: Target, event: Event, options?: { | ||
import type { EventTarget, AbortBehavior, EventHandlerMap } from './types.ts'; | ||
/** | ||
* Listens to an `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns an `AsyncGenerator` | ||
* that yields each event as it is emitted. | ||
* | ||
* @param target The target to add the event handler to. | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns An `AsyncGenerator` that yields each event as it is emitted. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* | ||
* for await (const event of on(button, 'click')) { | ||
* console.log('clicked!', event); | ||
* } | ||
*/ | ||
export declare function on<Events extends EventHandlerMap = Record<string, unknown>, Event extends keyof Events = keyof Events>(target: EventTarget<Events>, event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the iterator to complete. If set to `'reject'`, the iterator | ||
* will throw an `AbortError` when the signal is aborted instead. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): AsyncIterator<EventMap[Event], void, void> & AsyncIterable<EventMap[Event]>; | ||
}): AsyncGenerator<Events[Event], void, void>; | ||
//# sourceMappingURL=on.d.ts.map |
@@ -1,8 +0,60 @@ | ||
import type { AbortBehavior } from './abort.ts'; | ||
import type { EventTarget } from './types.ts'; | ||
export declare function once<EventMap = Record<string, unknown>, Event extends keyof EventMap = keyof EventMap, Target extends EventTarget<EventMap> = EventTarget<EventMap>>(target: Target, event: Event): Promise<EventMap[Event]>; | ||
export declare function once<EventMap = Record<string, unknown>, Event extends keyof EventMap = keyof EventMap, Target extends EventTarget<EventMap> = EventTarget<EventMap>, Abort extends AbortBehavior = 'returns'>(target: Target, event: Event, options: { | ||
import type { AbortBehavior, EventHandlerMap, EventTarget } from './types.ts'; | ||
/** | ||
* Listens for the next `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns a `Promise` | ||
* that resolves when the next matching event is triggered. | ||
* | ||
* @param target The target to add the event handler to. | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns A `Promise` that resolves when the next matching event is triggered. | ||
* The promise resolves with the value of the first argument passed to the event. | ||
* If you pass the `signal` option, the promise may also resolve with `undefined` | ||
* (in case the signal is aborted before the event is emitted). If you also set | ||
* the `abort` option to `'reject'`, the promise will instead reject with an | ||
* `AbortError` when the signal is aborted. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* | ||
* const nextClickEvent = await once(button, 'click'); | ||
* console.log('clicked!', nextClickEvent); | ||
*/ | ||
export declare function once<Events extends EventHandlerMap = Record<string, unknown>, Event extends keyof Events = keyof Events>(target: EventTarget<Events>, event: Event, options: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. When set to `'reject'`, | ||
* the promise will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort: 'reject'; | ||
}): Promise<Events[Event]>; | ||
export declare function once<Events extends EventHandlerMap = Record<string, unknown>, Event extends keyof Events = keyof Events>(target: EventTarget<Events>, event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
abort?: Abort; | ||
}): Promise<Abort extends 'returns' ? EventMap[Event] | undefined : EventMap[Event]>; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): Promise<Events[Event] | undefined>; | ||
export declare function once<Events extends EventHandlerMap = Record<string, unknown>, Event extends keyof Events = keyof Events>(target: EventTarget<Events>, event: Event, options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: never; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}): Promise<Events[Event]>; | ||
//# sourceMappingURL=once.d.ts.map |
@@ -1,3 +0,26 @@ | ||
export interface EventTargetAddEventListener<EventMap = Record<string, unknown>> { | ||
addEventListener<Event extends keyof EventMap>(event: Event, listener: (...args: EventMap[Event] extends any[] ? EventMap[Event] : [EventMap[Event]]) => void, options?: { | ||
/** | ||
* How to handle an `AbortSignal` being aborted. `'resolve'` means that | ||
* an asynchronous operation should resolve with `undefined` if the signal | ||
* aborts before the asynchronous operation completes. `'reject'`, on the | ||
* other hand, means that the asynchronous operation should reject with | ||
* an `AbortError` if the signal aborts before the asynchronous operation | ||
* completes. | ||
*/ | ||
export type AbortBehavior = 'reject' | 'resolve'; | ||
/** | ||
* A function that handles an event. | ||
*/ | ||
export type EventHandler<Argument = unknown> = (arg: Argument) => void; | ||
/** | ||
* A map of event names to the data types that they receive. | ||
*/ | ||
export type EventHandlerMap = { | ||
[key: string]: any; | ||
}; | ||
/** | ||
* An object that can listen for events using an `addEventListener()` | ||
* method. | ||
*/ | ||
export interface EventTargetAddEventListener<Events extends EventHandlerMap = Record<string, unknown>> { | ||
addEventListener<Event extends keyof Events>(event: Event, listener: EventHandler<Events[Event]>, options?: { | ||
once?: boolean; | ||
@@ -7,8 +30,14 @@ signal?: AbortSignal; | ||
} | ||
export interface EventTargetOn<EventMap = Record<string, unknown>> { | ||
on<Event extends keyof EventMap>(event: Event, listener: (...args: EventMap[Event] extends any[] ? EventMap[Event] : [EventMap[Event]]) => void): void; | ||
off<Event extends keyof EventMap>(event: Event, listener: (...args: EventMap[Event] extends any[] ? EventMap[Event] : [EventMap[Event]]) => void): void; | ||
/** | ||
* An object that can listen for events using `on()` and `off()` methods. | ||
*/ | ||
export interface EventTargetOn<Events extends EventHandlerMap = Record<string, unknown>> { | ||
on<Event extends keyof Events>(event: Event, listener: EventHandler<Events[Event]>): void; | ||
off<Event extends keyof Events>(event: Event, listener: EventHandler<Events[Event]>): void; | ||
} | ||
export interface EventTargetFunction<EventMap = Record<string, unknown>> { | ||
<Event extends keyof EventMap>(event: Event, listener: (...args: EventMap[Event] extends any[] ? EventMap[Event] : [EventMap[Event]]) => void, options?: { | ||
/** | ||
* A function that can listen to events with the arguments passed to it. | ||
*/ | ||
export interface EventTargetFunction<Events extends EventHandlerMap = Record<string, unknown>> { | ||
<Event extends keyof Events>(event: Event, listener: EventHandler<Events[Event]>, options?: { | ||
once?: boolean; | ||
@@ -18,3 +47,7 @@ signal?: AbortSignal; | ||
} | ||
export type EventTarget<EventMap = Record<string, unknown>> = EventTargetAddEventListener<EventMap> | EventTargetOn<EventMap> | EventTargetFunction<EventMap>; | ||
/** | ||
* Any kind of object that can listen for events that this library | ||
* understands. | ||
*/ | ||
export type EventTarget<Events extends EventHandlerMap = Record<string, unknown>> = EventTargetAddEventListener<Events> | EventTargetOn<Events> | EventTargetFunction<Events>; | ||
//# sourceMappingURL=types.d.ts.map |
# @quilted/events | ||
## 1.0.0 | ||
### Major Changes | ||
- [#587](https://github.com/lemonmade/quilt/pull/587) [`1180dde2`](https://github.com/lemonmade/quilt/commit/1180dde278793006b8ae153804130cad6dab36c2) Thanks [@lemonmade](https://github.com/lemonmade)! - First major version for `@quilted/events` | ||
## 0.1.18 | ||
@@ -4,0 +10,0 @@ |
{ | ||
"name": "@quilted/events", | ||
"description": "", | ||
"description": "Tiny helpers for working with events in any JavaScript environment", | ||
"type": "module", | ||
"version": "0.1.18", | ||
"version": "1.0.0", | ||
"license": "MIT", | ||
@@ -7,0 +7,0 @@ "engines": { |
# `@quilted/events` | ||
Tiny helpers for working with events in any JavaScript environment. | ||
## Installation | ||
```bash | ||
# npm | ||
npm install @quilted/events --save | ||
# pnpm | ||
pnpm install @quilted/events --save | ||
# yarn | ||
yarn add @quilted/events | ||
``` | ||
## Usage | ||
### Emitting and listening for events | ||
This library provides an `EventEmitter` class, which is the main utility you’ll use to work with events. It’s similar to the browser’s [`EventTarget` interface](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget), but instead of accepting only callbacks to listen for events, it turns events into other JavaScript types. | ||
To get started, you can create a new `EventEmitter` instance, passing it a type argument that describes the events that can be triggered, and their allowed data type: | ||
```ts | ||
import {EventEmitter} from '@quilted/events'; | ||
const events = new EventEmitter<{message: string}>(); | ||
``` | ||
Unlike `EventTarget`’s single `addEventListener()` method, the `EventEmitter` class provides two different methods for dealing with events. The `on()` method returns an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that will yield the data for each matching event: | ||
```ts | ||
import {EventEmitter} from '@quilted/events'; | ||
const events = new EventEmitter<{message: string}>(); | ||
for await (const message of events.on('message')) { | ||
console.log('Message received:', message); | ||
} | ||
``` | ||
The `once()` method, on the other hand, returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that will resolve with the data for the first matching event: | ||
```ts | ||
import {EventEmitter} from '@quilted/events'; | ||
const events = new EventEmitter<{message: string}>(); | ||
const message = await events.once('message'); | ||
console.log('Message received:', message); | ||
``` | ||
Both `on()` and `once()` accept an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) option as their second argument, which allows you to cancel the listener. By default, aborting `on()` will cause the async generator to end stop yielding values, and will cause `once()` to resolve its promise with `undefined`. However, you can also pass an `abort` option set to `'reject'` in order to have these method instead reject with `AbortError`s: | ||
```ts | ||
import {EventEmitter} from '@quilted/events'; | ||
const events = new EventEmitter<{message: string}>(); | ||
const abortController = new AbortController(); | ||
// Abort this listener in 10 seconds | ||
setTimeout(() => { | ||
abortController.abort(); | ||
}, 10_000); | ||
try { | ||
const message = await events.once('message', { | ||
signal: abortController.signal, | ||
abort: 'reject', | ||
}); | ||
console.log('Message received:', message); | ||
} catch (error) { | ||
console.log('Promise rejected:', error); | ||
} | ||
``` | ||
When working in Node, the browser, and other environments, you may already have an object capable of receiving events, and want to convert those events to promises and async generators, like the `EventEmitter` class. You can wrap any object conforming to the [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) or [Node.js `EventEmitter`](https://nodejs.org/api/events.html#class-eventemitter) interfaces with an `EventEmitter` to get this functionality: | ||
```ts | ||
import {EventEmitter} from '@quilted/events'; | ||
// HTML elements implement `EventTarget` | ||
const button = document.querySelector('button'); | ||
const events = new EventEmitter(button); | ||
for await (const event of events.on('click')) { | ||
console.log('Button clicked!', event); | ||
} | ||
``` | ||
You can also use the `on()` and `once()` functions provided by this library to do one-off event listeners: | ||
```ts | ||
import {once} from '@quilted/events'; | ||
const button = document.querySelector('button'); | ||
const event = await once(button, 'click'); | ||
console.log('Button clicked!', event); | ||
``` |
@@ -1,96 +0,4 @@ | ||
export type AbortBehavior = 'throws' | 'returns'; | ||
export class NestedAbortController extends AbortController { | ||
private readonly cleanup = new AbortController(); | ||
constructor(parent?: AbortSignal) { | ||
super(); | ||
parent?.addEventListener('abort', () => this.abort(), { | ||
signal: this.cleanup.signal, | ||
}); | ||
} | ||
abort(reason?: any) { | ||
this.cleanup.abort(); | ||
super.abort(reason); | ||
} | ||
} | ||
export function anyAbortSignal(...signals: readonly AbortSignal[]) { | ||
const controller = new AbortController(); | ||
const abortedSignal = signals.find((signal) => signal.aborted); | ||
if (abortedSignal) { | ||
controller.abort(abortedSignal.reason); | ||
} else { | ||
for (const signal of signals) { | ||
signal.addEventListener('abort', () => controller.abort(signal.reason), { | ||
signal: controller.signal, | ||
}); | ||
} | ||
} | ||
return controller.signal; | ||
} | ||
export async function raceAgainstAbortSignal<T>( | ||
race: () => Promise<T>, | ||
{ | ||
signal, | ||
ifAborted, | ||
}: {signal: AbortSignal; ifAborted?(): void | Promise<void>}, | ||
): Promise<T> { | ||
const raceAbort = new AbortController(); | ||
const result = await Promise.race([racer(), abortRacer()]); | ||
return result as T; | ||
async function racer() { | ||
try { | ||
const result = await race(); | ||
return result; | ||
} finally { | ||
raceAbort.abort(); | ||
} | ||
} | ||
async function abortRacer() { | ||
await new Promise<void>((resolve, reject) => { | ||
signal.addEventListener( | ||
'abort', | ||
async () => { | ||
try { | ||
if (ifAborted) await ifAborted(); | ||
reject(new AbortError()); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}, | ||
{signal: raceAbort.signal}, | ||
); | ||
raceAbort.signal.addEventListener( | ||
'abort', | ||
() => { | ||
resolve(); | ||
}, | ||
{signal}, | ||
); | ||
}); | ||
} | ||
} | ||
// @see https://github.com/nodejs/node/blob/master/lib/internal/errors.js#L822-L834 | ||
export class AbortError extends Error { | ||
static test(error: unknown): error is AbortError { | ||
return error != null && (error as any).code === 'ABORT_ERR'; | ||
} | ||
readonly code = 'ABORT_ERR'; | ||
readonly name = 'AbortError'; | ||
constructor(message = 'The operation was aborted') { | ||
super(message); | ||
} | ||
} | ||
export {AbortError} from './abort/AbortError.ts'; | ||
export {NestedAbortController} from './abort/NestedAbortController.ts'; | ||
export {TimedAbortController} from './abort/TimedAbortController.ts'; | ||
export {raceAgainstAbortSignal} from './abort/race.ts'; |
import {on as onEvent} from './on.ts'; | ||
import {once as onceEvent} from './once.ts'; | ||
import type {AbortBehavior} from './abort.ts'; | ||
import {addEventHandler} from './handler.ts'; | ||
import type { | ||
AbortBehavior, | ||
EventHandler, | ||
EventHandlerMap, | ||
EventTarget, | ||
EventTargetFunction, | ||
} from './types.ts'; | ||
export type EmitterHandler<T = unknown> = (event: T) => void; | ||
export interface Emitter<Events = {}> { | ||
on<Event extends keyof Events>( | ||
type: Event, | ||
options: {once: true; signal?: never; abort?: never}, | ||
): Promise<Events[Event]>; | ||
on<Event extends keyof Events, Abort extends AbortBehavior = 'returns'>( | ||
type: Event, | ||
options: {once: true; signal: AbortSignal; abort?: Abort}, | ||
): Promise< | ||
Abort extends 'returns' ? Events[Event] | undefined : Events[Event] | ||
>; | ||
on<Event extends keyof Events>( | ||
type: Event, | ||
options?: {once?: boolean; signal?: AbortSignal; abort?: AbortBehavior}, | ||
): AsyncGenerator<Events[Event], void, void>; | ||
on<Event extends keyof Events>( | ||
type: Event, | ||
handler: EmitterHandler<Events[Event]>, | ||
options?: {once?: boolean; signal?: AbortSignal; abort?: never}, | ||
): void; | ||
emit<Event extends keyof Events>(type: Event, event: Events[Event]): void; | ||
emit<Event extends keyof Events>( | ||
type: undefined extends Events[Event] ? Event : never, | ||
): void; | ||
/** | ||
* Internal events triggered by an `EventEmitter` as handlers are | ||
* added and removed. | ||
*/ | ||
export interface EmitterEmitterInternalEvents< | ||
Events extends EventHandlerMap = {}, | ||
> { | ||
add: { | ||
readonly event: keyof Events; | ||
readonly all: Set<EventHandler<Events[keyof Events]>>; | ||
readonly handler: EventHandler<Events[keyof Events]>; | ||
}; | ||
remove: { | ||
readonly event: keyof Events; | ||
readonly all: Set<EventHandler<Events[keyof Events]>>; | ||
readonly handler: EventHandler<Events[keyof Events]>; | ||
}; | ||
} | ||
export function createEmitter<Events = {}>( | ||
internal?: Emitter<EmitterInternalEvents<Events>>, | ||
): Emitter<Events> { | ||
const handlerMap = new Map<keyof Events, Set<any>>(); | ||
/** | ||
* An object that can emit events. This is a minimal alternative to the `EventTarget` | ||
* interface available in most JavaScript environments. Unlike the callback functions | ||
* you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support | ||
* event propagation, but does provide convenient `once()` and `on()` methods for | ||
* converting event listeners into into `Promise`s and `AsyncGenerator`s. | ||
*/ | ||
export class EventEmitter<Events extends EventHandlerMap = {}> { | ||
/** | ||
* A map containing the event handlers registered for events on this | ||
* emitter. You should not directly mutate this map, but you can use it | ||
* to introspect the number of handlers currently registered. | ||
*/ | ||
readonly all = new Map< | ||
keyof Events, | ||
Set<EventHandler<Events[keyof Events]>> | ||
>(); | ||
return { | ||
on, | ||
emit<Event extends keyof Events>(event: Event, data?: Events[Event]) { | ||
const handlers = handlerMap.get(event); | ||
/** | ||
* A reference to an `EventTarget` that is being wrapped with this `EventEmitter`. | ||
* As handlers are added for events on the emitter, matching events are listened | ||
* for lazily on the `EventTarget`. This is useful for converting event producers | ||
* in existing environments into objects that can be used with `once()` and `on()`. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* const emitter = new EventEmitter(button); | ||
* // emitter.eventTarget === button | ||
* | ||
* const click = await emitter.once('click'); | ||
* console.log('clicked!', click); | ||
*/ | ||
readonly eventTarget: EventTarget<Events> | undefined; | ||
if (handlers) { | ||
for (const handler of Array.from(handlers)) { | ||
handler(data); | ||
} | ||
} | ||
}, | ||
}; | ||
/** | ||
* An `EventEmitter` that triggers events when handlers are added | ||
* or removed from this emitter. This is useful for debugging, and for | ||
* building higher-level abstractions that need to know when handlers | ||
* are registered for a given event. | ||
*/ | ||
get internal(): Pick<EventEmitter<Events>, 'on' | 'once'> { | ||
if (this._internal) { | ||
return this._internal; | ||
} else { | ||
const internal = new EventEmitter<EmitterEmitterInternalEvents<Events>>(); | ||
this._internal = internal; | ||
return internal; | ||
} | ||
} | ||
function on<Event extends keyof Events>( | ||
event: Event, | ||
argOne?: any, | ||
argTwo?: any, | ||
): any { | ||
if (typeof argOne !== 'function') { | ||
const signal = argOne?.signal; | ||
const abort = argOne?.abort; | ||
const once = argOne?.once ?? false; | ||
private _internal?: EventEmitter<EmitterEmitterInternalEvents<Events>>; | ||
return once | ||
? onceEvent<Events>(on, event, {signal, abort}) | ||
: onEvent<Events>(on, event, {signal, abort}); | ||
} | ||
private handleEvent: EventTargetFunction<Events> = ( | ||
event, | ||
handler: any, | ||
options, | ||
) => { | ||
const signal = options?.signal; | ||
const once = options?.once; | ||
const handler = argOne as EmitterHandler<Events[Event]>; | ||
const signal = argTwo?.signal as AbortSignal | undefined; | ||
const once = argTwo?.once as boolean | undefined; | ||
let handlers = handlerMap.get(event); | ||
let handlers = this.all.get(event); | ||
const signalAbort = signal && new AbortController(); | ||
@@ -78,11 +98,11 @@ | ||
if (handlers == null) return; | ||
handlers.delete(normalizedHandler); | ||
internal?.emit('remove', {event, handler: handler as any, all: handlers}); | ||
if (handlers!.size === 0) handlerMap.delete(event); | ||
this._internal?.emit('remove', {event, all: handlers, handler}); | ||
handlers.delete(handler); | ||
if (handlers.size === 0) this.all.delete(event); | ||
}; | ||
const normalizedHandler = once | ||
const normalizedHandler: EventHandler<any> = once | ||
? (...args: any[]) => { | ||
remove(); | ||
(handler as any)(...args); | ||
handler(...args); | ||
} | ||
@@ -96,37 +116,251 @@ : handler; | ||
handlers.add(normalizedHandler); | ||
handlerMap.set(event, handlers); | ||
this.all.set(event, handlers); | ||
} | ||
internal?.emit('add', {event, handler: handler as any, all: handlers}); | ||
this._internal?.emit('add', {event, all: handlers, handler}); | ||
signal?.addEventListener('abort', remove, {signal: signalAbort!.signal}); | ||
}; | ||
constructor(eventTarget?: EventTarget<Events>) { | ||
this.on = this.on.bind(this); | ||
this.once = this.once.bind(this); | ||
this.emit = this.emit.bind(this); | ||
if (eventTarget) { | ||
this.eventTarget = eventTarget; | ||
const abortMap = new Map<keyof Events, AbortController>(); | ||
this.internal.on('add', ({event: eventName, all}) => { | ||
if (all.size !== 1) return; | ||
const abort = new AbortController(); | ||
abortMap.set(eventName, abort); | ||
addEventHandler( | ||
eventTarget, | ||
eventName, | ||
(event) => { | ||
this.emit(eventName, event); | ||
}, | ||
{ | ||
signal: abort.signal, | ||
}, | ||
); | ||
}); | ||
this.internal.on('remove', ({event: eventName, all}) => { | ||
const abort = all.size === 0 ? abortMap.get(eventName) : undefined; | ||
if (abort == null) return; | ||
abort.abort(); | ||
abortMap.delete(eventName); | ||
}); | ||
} | ||
} | ||
} | ||
export interface EmitterInternalEvents<Events = {}> { | ||
add: { | ||
event: keyof Events; | ||
handler: EmitterHandler<unknown>; | ||
all: Set<EmitterHandler<unknown>>; | ||
}; | ||
remove: { | ||
event: keyof Events; | ||
handler: EmitterHandler<unknown>; | ||
all: Set<EmitterHandler<unknown>>; | ||
}; | ||
} | ||
/** | ||
* Listens to an `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns an `AsyncGenerator` | ||
* that yields each event as it is emitted. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns An `AsyncGenerator` that yields each event as it is emitted. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* for await (const message of emitter.on('message')) { | ||
* console.log('message', message); | ||
* } | ||
*/ | ||
on<Event extends keyof Events>( | ||
event: Event, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
export interface EmitterWithInternals<Events = {}> extends Emitter<Events> { | ||
readonly internal: Emitter<EmitterInternalEvents<Events>>; | ||
} | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the iterator to complete. If set to `'reject'`, the iterator | ||
* will throw an `AbortError` when the signal is aborted instead. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): AsyncGenerator<Events[Event], void, void>; | ||
export function createEmitterWithInternals< | ||
Events = {}, | ||
>(): EmitterWithInternals<Events> { | ||
const internal = createEmitter<EmitterInternalEvents<Events>>(); | ||
const emitter = createEmitter(internal); | ||
/** | ||
* Listens for `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* @param handler The function to call when the event is triggered. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* emitter.on('message', (message) => { | ||
* console.log('message', message); | ||
* }); | ||
*/ | ||
on<Event extends keyof Events>( | ||
event: Event, | ||
handler: EventHandler<Events[Event]>, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
abort?: never; | ||
}, | ||
): void; | ||
on<Event extends keyof Events>( | ||
event: Event, | ||
argOne?: any, | ||
argTwo?: any, | ||
): any { | ||
return typeof argOne === 'function' | ||
? this.handleEvent(event, argOne, argTwo) | ||
: onEvent<Events>(this.handleEvent, event, argOne); | ||
} | ||
(emitter as any).internal = internal; | ||
/** | ||
* Listens for the next matching `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns A `Promise` that resolves when the next matching event is triggered. | ||
* The promise resolves with the value of the first argument passed to the event. | ||
* If you pass the `signal` option, the promise may also resolve with `undefined` | ||
* (in case the signal is aborted before the event is emitted). If you also set | ||
* the `abort` option to `'reject'`, the promise will instead reject with an | ||
* `AbortError` when the signal is aborted. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* const nextMessage = await emitter.once('message'); | ||
* console.log('message', nextMessage); | ||
*/ | ||
once<Event extends keyof Events>( | ||
event: Event, | ||
options: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal: AbortSignal; | ||
return emitter as any; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. When set to `'reject'`, | ||
* the promise will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort: 'reject'; | ||
}, | ||
): Promise<Events[Event]>; | ||
once<Event extends keyof Events>( | ||
event: Event, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: never; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): Promise<Events[Event]>; | ||
once<Event extends keyof Events>( | ||
event: Event, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): Promise<Events[Event] | undefined>; | ||
/** | ||
* Listens for the next matching `event` to be triggered. | ||
* | ||
* @param event The name of the event you are targeting. | ||
* @param handler The function to call when the event is triggered. | ||
* | ||
* @example | ||
* const emitter = new EventEmitter<{message: string}>(); | ||
* | ||
* emitter.once('message', (message) => { | ||
* console.log('message', message); | ||
* }); | ||
*/ | ||
once<Event extends keyof Events>( | ||
event: Event, | ||
handler: EventHandler<Events[Event]>, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): void; | ||
once<Event extends keyof Events>( | ||
event: Event, | ||
argOne?: any, | ||
argTwo?: any, | ||
): any { | ||
return typeof argOne === 'function' | ||
? this.handleEvent(event, argOne, {...argTwo, once: true}) | ||
: onceEvent<Events>(this.handleEvent, event, argOne); | ||
} | ||
/** | ||
* Emits an event with the given `data`. This will trigger all handlers | ||
* listening for the provided `event`. | ||
*/ | ||
emit<Event extends keyof Events>(event: Event, data?: Events[Event]) { | ||
const handlers = this.all.get(event); | ||
if (handlers) { | ||
for (const handler of Array.from(handlers)) { | ||
handler(data as any); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Creates an object that can emit events. This is a minimal alternative to the `EventTarget` | ||
* interface available in most JavaScript environments. Unlike the callback functions | ||
* you provide to `EventTarget.addEventListener()`, `EventEmitter` does not support | ||
* event propagation, but does provide convenient `once()` and `on()` methods for | ||
* converting event listeners into into `Promise`s and `AsyncGenerator`s. | ||
*/ | ||
export function createEventEmitter< | ||
Events extends EventHandlerMap = {}, | ||
>(): EventEmitter<Events> { | ||
return new EventEmitter<Events>(); | ||
} |
@@ -6,16 +6,16 @@ export {on} from './on.ts'; | ||
NestedAbortController, | ||
anyAbortSignal, | ||
TimedAbortController, | ||
raceAgainstAbortSignal, | ||
type AbortBehavior, | ||
} from './abort.ts'; | ||
export {addListener} from './listeners.ts'; | ||
export {createEmitter, createEmitterWithInternals} from './emitter.ts'; | ||
export type { | ||
Emitter, | ||
EmitterHandler, | ||
EmitterInternalEvents, | ||
EmitterWithInternals, | ||
export {addEventHandler} from './handler.ts'; | ||
export { | ||
EventEmitter, | ||
createEventEmitter, | ||
type EmitterEmitterInternalEvents, | ||
} from './emitter.ts'; | ||
export {TimedAbortController, sleep} from './timeouts.ts'; | ||
export {sleep} from './sleep.ts'; | ||
export type { | ||
AbortBehavior, | ||
EventHandler, | ||
EventHandlerMap, | ||
EventTarget, | ||
@@ -22,0 +22,0 @@ EventTargetOn, |
@@ -1,28 +0,56 @@ | ||
import {addListener} from './listeners.ts'; | ||
import type {AbortBehavior} from './abort.ts'; | ||
import {addEventHandler} from './handler.ts'; | ||
import {AbortError, NestedAbortController} from './abort.ts'; | ||
import type {EventTarget} from './types.ts'; | ||
import type {EventTarget, AbortBehavior, EventHandlerMap} from './types.ts'; | ||
// @see https://github.com/nodejs/node/blob/master/lib/events.js#L1012-L1019 | ||
/** | ||
* Listens to an `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns an `AsyncGenerator` | ||
* that yields each event as it is emitted. | ||
* | ||
* @param target The target to add the event handler to. | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns An `AsyncGenerator` that yields each event as it is emitted. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* | ||
* for await (const event of on(button, 'click')) { | ||
* console.log('clicked!', event); | ||
* } | ||
*/ | ||
export function on< | ||
EventMap = Record<string, unknown>, | ||
Event extends keyof EventMap = keyof EventMap, | ||
Target extends EventTarget<EventMap> = EventTarget<EventMap>, | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
Event extends keyof Events = keyof Events, | ||
>( | ||
emitter: Target, | ||
target: EventTarget<Events>, | ||
event: Event, | ||
options?: {signal?: AbortSignal; abort?: AbortBehavior}, | ||
): AsyncIterator<EventMap[Event], void, void> & AsyncIterable<EventMap[Event]> { | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the iterator to complete. If set to `'reject'`, the iterator | ||
* will throw an `AbortError` when the signal is aborted instead. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): AsyncGenerator<Events[Event], void, void> { | ||
const signal = options?.signal; | ||
const abortBehavior = options?.abort ?? 'returns'; | ||
const abortBehavior = options?.abort ?? 'resolve'; | ||
if (signal?.aborted) { | ||
if (abortBehavior === 'returns') { | ||
if (abortBehavior === 'resolve') { | ||
return noop(); | ||
} else { | ||
throw new AbortError(); | ||
return noopThrow(new AbortError()); | ||
} | ||
} | ||
const listenerAbortController = new NestedAbortController(signal); | ||
const listenerAbortController = signal | ||
? new NestedAbortController(signal) | ||
: new AbortController(); | ||
const signalAbortController = signal && new AbortController(); | ||
@@ -39,4 +67,3 @@ | ||
const iterator: AsyncIterator<EventMap[Event], void, void> & | ||
AsyncIterable<EventMap[Event]> = { | ||
const iterator: AsyncGenerator<Events[Event], void, void> = { | ||
next() { | ||
@@ -95,3 +122,3 @@ // First, we consume all unread events | ||
addListener(emitter, event as any, eventHandler, { | ||
addEventHandler(target, event as any, eventHandler, { | ||
signal: listenerAbortController.signal, | ||
@@ -101,3 +128,3 @@ }); | ||
if (signal) { | ||
addListener(signal, 'abort', abortListener, { | ||
addEventHandler(signal, 'abort', abortListener, { | ||
once: true, | ||
@@ -111,3 +138,3 @@ signal: signalAbortController!.signal, | ||
function abortListener() { | ||
if (abortBehavior === 'returns') { | ||
if (abortBehavior === 'resolve') { | ||
iterator.return!(); | ||
@@ -149,1 +176,6 @@ } else { | ||
} | ||
// eslint-disable-next-line require-yield | ||
async function* noopThrow(error: Error) { | ||
throw error; | ||
} |
@@ -1,40 +0,113 @@ | ||
import {addListener} from './listeners.ts'; | ||
import type {AbortBehavior} from './abort.ts'; | ||
import {addEventHandler} from './handler.ts'; | ||
import {AbortError, NestedAbortController} from './abort.ts'; | ||
import type {EventTarget} from './types.ts'; | ||
import type {AbortBehavior, EventHandlerMap, EventTarget} from './types.ts'; | ||
/** | ||
* Listens for the next `event` on the `target` object. Instead of providing a callback | ||
* function to run when the event is emitted, this function returns a `Promise` | ||
* that resolves when the next matching event is triggered. | ||
* | ||
* @param target The target to add the event handler to. | ||
* @param event The name of the event you are targeting. | ||
* | ||
* @returns A `Promise` that resolves when the next matching event is triggered. | ||
* The promise resolves with the value of the first argument passed to the event. | ||
* If you pass the `signal` option, the promise may also resolve with `undefined` | ||
* (in case the signal is aborted before the event is emitted). If you also set | ||
* the `abort` option to `'reject'`, the promise will instead reject with an | ||
* `AbortError` when the signal is aborted. | ||
* | ||
* @example | ||
* const button = document.querySelector('button'); | ||
* | ||
* const nextClickEvent = await once(button, 'click'); | ||
* console.log('clicked!', nextClickEvent); | ||
*/ | ||
export async function once< | ||
EventMap = Record<string, unknown>, | ||
Event extends keyof EventMap = keyof EventMap, | ||
Target extends EventTarget<EventMap> = EventTarget<EventMap>, | ||
>(target: Target, event: Event): Promise<EventMap[Event]>; | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
Event extends keyof Events = keyof Events, | ||
>( | ||
target: EventTarget<Events>, | ||
event: Event, | ||
options: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. When set to `'reject'`, | ||
* the promise will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort: 'reject'; | ||
}, | ||
): Promise<Events[Event]>; | ||
export async function once< | ||
EventMap = Record<string, unknown>, | ||
Event extends keyof EventMap = keyof EventMap, | ||
Target extends EventTarget<EventMap> = EventTarget<EventMap>, | ||
Abort extends AbortBehavior = 'returns', | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
Event extends keyof Events = keyof Events, | ||
>( | ||
target: Target, | ||
target: EventTarget<Events>, | ||
event: Event, | ||
options: {signal?: AbortSignal; abort?: Abort}, | ||
): Promise< | ||
Abort extends 'returns' ? EventMap[Event] | undefined : EventMap[Event] | ||
>; | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): Promise<Events[Event] | undefined>; | ||
export async function once< | ||
EventMap = Record<string, unknown>, | ||
Event extends keyof EventMap = keyof EventMap, | ||
Target extends EventTarget<EventMap> = EventTarget<EventMap>, | ||
Abort extends AbortBehavior = 'returns', | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
Event extends keyof Events = keyof Events, | ||
>( | ||
target: Target, | ||
target: EventTarget<Events>, | ||
event: Event, | ||
options?: {signal?: AbortSignal; abort?: Abort}, | ||
): Promise< | ||
Abort extends 'returns' ? EventMap[Event] | undefined : EventMap[Event] | ||
> { | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: never; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): Promise<Events[Event]>; | ||
export async function once< | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
Event extends keyof Events = keyof Events, | ||
>( | ||
target: EventTarget<Events>, | ||
event: Event, | ||
options?: { | ||
/** | ||
* An `AbortSignal` to cancel listening to the event. | ||
*/ | ||
signal?: AbortSignal; | ||
/** | ||
* How to handle the `AbortSignal` being aborted. Defaults to `'resolve'`, | ||
* which causes the promise to resolve with `undefined` if the signal is | ||
* aborted before the next matching event. If set to `'reject'`, the promise | ||
* will instead reject with an `AbortError` in this case. | ||
*/ | ||
abort?: AbortBehavior; | ||
}, | ||
): Promise<Events[Event] | undefined> { | ||
const signal = options?.signal; | ||
const abortBehavior = options?.abort ?? 'returns'; | ||
const abortBehavior = options?.abort ?? 'return'; | ||
if (signal?.aborted) { | ||
if (abortBehavior === 'returns') { | ||
if (abortBehavior === 'return') { | ||
return undefined as any; | ||
@@ -46,6 +119,9 @@ } else { | ||
const listenerAbortController = new NestedAbortController(signal); | ||
const signalAbortController = signal && new NestedAbortController(signal); | ||
const listenerAbortController = signal | ||
? new NestedAbortController(signal) | ||
: new AbortController(); | ||
return new Promise<EventMap[Event] | void>((resolve, reject) => { | ||
const signalAbortController = signal && new AbortController(); | ||
return new Promise<Events[Event] | void>((resolve, reject) => { | ||
const resolver = (...args: any[]) => { | ||
@@ -56,3 +132,3 @@ signalAbortController?.abort(); | ||
addListener(target, event as any, resolver, { | ||
addEventHandler(target, event as any, resolver, { | ||
once: true, | ||
@@ -63,9 +139,9 @@ signal: listenerAbortController.signal, | ||
if (signal) { | ||
addListener( | ||
addEventHandler( | ||
signal, | ||
'abort', | ||
() => { | ||
listenerAbortController.abort(); | ||
signalAbortController!.abort(); | ||
if (abortBehavior === 'returns') { | ||
if (abortBehavior === 'return') { | ||
resolve(); | ||
@@ -79,3 +155,3 @@ } else { | ||
} | ||
}) as Promise<EventMap[Event]>; | ||
}) as Promise<Events[Event]>; | ||
} |
@@ -0,11 +1,31 @@ | ||
/** | ||
* How to handle an `AbortSignal` being aborted. `'resolve'` means that | ||
* an asynchronous operation should resolve with `undefined` if the signal | ||
* aborts before the asynchronous operation completes. `'reject'`, on the | ||
* other hand, means that the asynchronous operation should reject with | ||
* an `AbortError` if the signal aborts before the asynchronous operation | ||
* completes. | ||
*/ | ||
export type AbortBehavior = 'reject' | 'resolve'; | ||
/** | ||
* A function that handles an event. | ||
*/ | ||
export type EventHandler<Argument = unknown> = (arg: Argument) => void; | ||
/** | ||
* A map of event names to the data types that they receive. | ||
*/ | ||
export type EventHandlerMap = {[key: string]: any}; | ||
/** | ||
* An object that can listen for events using an `addEventListener()` | ||
* method. | ||
*/ | ||
export interface EventTargetAddEventListener< | ||
EventMap = Record<string, unknown>, | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
> { | ||
addEventListener<Event extends keyof EventMap>( | ||
addEventListener<Event extends keyof Events>( | ||
event: Event, | ||
listener: ( | ||
...args: EventMap[Event] extends any[] | ||
? EventMap[Event] | ||
: [EventMap[Event]] | ||
) => void, | ||
listener: EventHandler<Events[Event]>, | ||
options?: {once?: boolean; signal?: AbortSignal}, | ||
@@ -15,29 +35,27 @@ ): void; | ||
export interface EventTargetOn<EventMap = Record<string, unknown>> { | ||
on<Event extends keyof EventMap>( | ||
/** | ||
* An object that can listen for events using `on()` and `off()` methods. | ||
*/ | ||
export interface EventTargetOn< | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
> { | ||
on<Event extends keyof Events>( | ||
event: Event, | ||
listener: ( | ||
...args: EventMap[Event] extends any[] | ||
? EventMap[Event] | ||
: [EventMap[Event]] | ||
) => void, | ||
listener: EventHandler<Events[Event]>, | ||
): void; | ||
off<Event extends keyof EventMap>( | ||
off<Event extends keyof Events>( | ||
event: Event, | ||
listener: ( | ||
...args: EventMap[Event] extends any[] | ||
? EventMap[Event] | ||
: [EventMap[Event]] | ||
) => void, | ||
listener: EventHandler<Events[Event]>, | ||
): void; | ||
} | ||
export interface EventTargetFunction<EventMap = Record<string, unknown>> { | ||
<Event extends keyof EventMap>( | ||
/** | ||
* A function that can listen to events with the arguments passed to it. | ||
*/ | ||
export interface EventTargetFunction< | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
> { | ||
<Event extends keyof Events>( | ||
event: Event, | ||
listener: ( | ||
...args: EventMap[Event] extends any[] | ||
? EventMap[Event] | ||
: [EventMap[Event]] | ||
) => void, | ||
listener: EventHandler<Events[Event]>, | ||
options?: {once?: boolean; signal?: AbortSignal}, | ||
@@ -47,5 +65,11 @@ ): void; | ||
export type EventTarget<EventMap = Record<string, unknown>> = | ||
| EventTargetAddEventListener<EventMap> | ||
| EventTargetOn<EventMap> | ||
| EventTargetFunction<EventMap>; | ||
/** | ||
* Any kind of object that can listen for events that this library | ||
* understands. | ||
*/ | ||
export type EventTarget< | ||
Events extends EventHandlerMap = Record<string, unknown>, | ||
> = | ||
| EventTargetAddEventListener<Events> | ||
| EventTargetOn<Events> | ||
| EventTargetFunction<Events>; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
163693
80
2757
1
101
1