UNPKG

evnty

Version:

Async-first, reactive event handling library for complex event flows in browser and Node.js

1 lines 16.3 kB
{"version":3,"sources":["../src/event.ts"],"sourcesContent":["import { Callback, Listener, FilterFunction, Predicate, Mapper, Reducer, Action, Fn, Emitter, MaybePromise, Promiseable } from './types.js';\nimport { Disposer } from './async.js';\nimport { Signal } from './signal.js';\nimport { ListenerRegistry } from './listener-registry.js';\nimport { DispatchResult } from './dispatch-result.js';\n\nexport type Unsubscribe = Action;\n\n/**\n * @internal\n */\nexport class EventIterator<T> implements AsyncIterator<T, void, void> {\n #signal: Signal<T>;\n\n constructor(signal: Signal<T>) {\n this.#signal = signal;\n }\n\n async next(): Promise<IteratorResult<T, void>> {\n try {\n const value = await this.#signal.receive();\n return { value, done: false };\n } catch {\n return { value: undefined, done: true };\n }\n }\n\n async return(): Promise<IteratorResult<T, void>> {\n return { value: undefined, done: true };\n }\n}\n\n/**\n * Multi-listener event emitter with async support.\n * All registered listeners are called for each emission, and their return\n * values are collected in a DispatchResult. Supports async iteration and\n * an `onDispose` callback for cleanup.\n *\n * Differs from:\n * - Signal: Event has persistent listeners; Signal is promise-based (receive per round)\n * - Sequence: Event broadcasts to all listeners; Sequence is a single-consumer queue\n *\n * @template T - The type of value emitted to listeners (event payload)\n * @template R - The return type of listener functions\n */\nexport class Event<T = unknown, R = unknown> implements Emitter<T, DispatchResult<void | R>>, Promiseable<T>, Promise<T>, Disposable, AsyncIterable<T> {\n #listeners = new ListenerRegistry<[T], R | void>();\n #signal = new Signal<T>();\n #disposer: Disposer;\n #onDispose?: Callback;\n #sink?: Fn<[T], DispatchResult<void | R>>;\n\n readonly [Symbol.toStringTag] = 'Event';\n\n /**\n * Creates a new event.\n *\n * @param onDispose - A function to call on the event disposal.\n *\n * @example\n * ```typescript\n * // Create a click event.\n * const clickEvent = new Event<[x: number, y: number], void>();\n * clickEvent.on(([x, y]) => console.log(`Clicked at ${x}, ${y}`));\n * clickEvent.emit([10, 20]);\n * ```\n */\n constructor(onDispose?: Callback) {\n this.#onDispose = onDispose;\n this.#disposer = new Disposer(this);\n }\n\n /**\n * Returns a bound emit function for use as a callback.\n * Useful for passing to other APIs that expect a function.\n *\n * @example\n * ```typescript\n * const event = new Event<string>();\n * someApi.onMessage(event.sink);\n * ```\n */\n get sink(): Fn<[T], DispatchResult<void | R>> {\n return (this.#sink ??= this.emit.bind(this));\n }\n\n /**\n * DOM EventListener interface compatibility.\n * Allows the event to be used directly with addEventListener.\n */\n handleEvent(event: T): void {\n this.emit(event);\n }\n\n /**\n * The number of listeners for the event.\n */\n get size(): number {\n return this.#listeners.size;\n }\n\n /**\n * Emits a value to all registered listeners.\n * Each listener is called with the value and their return values are collected.\n *\n * @param value - The value to emit to all listeners.\n * @returns A DispatchResult containing all listener return values.\n *\n * @example\n * ```typescript\n * const event = new Event<string, number>();\n * event.on(str => str.length);\n * const result = event.emit('hello');\n * await result.all(); // [5]\n * ```\n */\n emit(value: T): DispatchResult<void | R> {\n this.#signal.emit(value);\n return new DispatchResult(this.#listeners.dispatch(value));\n }\n\n /**\n * Checks if the given listener is NOT registered for this event.\n *\n * @param listener - The listener function to check against the registered listeners.\n * @returns `true` if the listener is not already registered; otherwise, `false`.\n *\n * @example\n * ```typescript\n * // Check if a listener is not already added\n * if (event.lacks(myListener)) {\n * event.on(myListener);\n * }\n * ```\n */\n lacks(listener: Listener<T, R>): boolean {\n return !this.#listeners.has(listener);\n }\n\n /**\n * Checks if the given listener is registered for this event.\n *\n * @param listener - The listener function to check.\n * @returns `true` if the listener is currently registered; otherwise, `false`.\n *\n * @example\n * ```typescript\n * // Verify if a listener is registered\n * if (event.has(myListener)) {\n * console.log('Listener is already registered');\n * }\n * ```\n */\n has(listener: Listener<T, R>): boolean {\n return this.#listeners.has(listener);\n }\n\n /**\n * Removes a specific listener from this event.\n *\n * @param listener - The listener to remove.\n * @returns The event instance for chaining.\n *\n * @example\n * ```typescript\n * // Remove a listener\n * event.off(myListener);\n * ```\n */\n off(listener: Listener<T, R>): this {\n this.#listeners.off(listener);\n return this;\n }\n\n /**\n * Registers a listener that is called on every emission.\n *\n * @param listener - The function to call when the event occurs.\n * @returns A function that removes this listener when called.\n *\n * @example\n * ```typescript\n * const unsubscribe = event.on((data) => {\n * console.log('Event data:', data);\n * });\n * ```\n */\n on(listener: Listener<T, R>): Unsubscribe {\n this.#listeners.on(listener);\n return () => void this.off(listener);\n }\n\n /**\n * Registers a listener that is called only once on the next emission, then auto-removed.\n *\n * @param listener - The listener to trigger once.\n * @returns A function that removes this listener when called (if it hasn't fired yet).\n *\n * @example\n * ```typescript\n * const cancel = event.once((data) => {\n * console.log('Received data once:', data);\n * });\n * ```\n */\n once(listener: Listener<T, R>): Unsubscribe {\n this.#listeners.once(listener);\n return () => void this.off(listener);\n }\n\n /**\n * Removes all listeners from the event.\n * Does not dispose the event - new listeners can still be added after clearing.\n *\n * @returns The event instance for chaining.\n */\n clear(): this {\n this.#listeners.clear();\n return this;\n }\n\n /**\n * Waits for the next emission and returns the emitted value.\n *\n * @returns A promise that resolves with the next emitted value.\n */\n receive(): Promise<T> {\n return this.#signal.receive();\n }\n\n then<OK = T, ERR = never>(onfulfilled?: Fn<[T], MaybePromise<OK>> | null, onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<OK | ERR> {\n return this.receive().then(onfulfilled, onrejected);\n }\n\n catch<ERR = never>(onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<T | ERR> {\n return this.receive().catch(onrejected);\n }\n\n finally(onfinally?: Action | null): Promise<T> {\n return this.receive().finally(onfinally);\n }\n\n /**\n * Waits for the next emission via `receive()` and wraps the outcome in a\n * `PromiseSettledResult` - always resolves, never rejects.\n *\n * @returns A promise that resolves with the settled result.\n *\n * @example\n * ```typescript\n * const result = await event.settle();\n * if (result.status === 'fulfilled') {\n * console.log('Value:', result.value);\n * } else {\n * console.error('Reason:', result.reason);\n * }\n * ```\n */\n settle(): Promise<PromiseSettledResult<T>> {\n return this.receive().then(\n (value) => ({ status: 'fulfilled', value }) as const,\n (reason: unknown) => ({ status: 'rejected', reason }) as const,\n );\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T, void, void> {\n return new EventIterator(this.#signal);\n }\n\n dispose(): void {\n this[Symbol.dispose]();\n }\n\n [Symbol.dispose](): void {\n if (this.#disposer[Symbol.dispose]()) {\n this.#signal[Symbol.dispose]();\n this.#listeners.clear();\n void this.#onDispose?.();\n }\n }\n}\n\nexport type EventParameters<T> = T extends Event<infer P, any> ? P : never;\n\nexport type EventReturn<T> = T extends Event<any, infer R> ? R : never;\n\nexport type AllEventsParameters<T extends Event<any, any>[]> = { [K in keyof T]: EventParameters<T[K]> }[number];\n\nexport type AllEventsResults<T extends Event<any, any>[]> = { [K in keyof T]: EventReturn<T[K]> }[number];\n\n/**\n * Merges multiple events into a single event that triggers whenever any source triggers.\n * Disposing the merged event unsubscribes from all sources.\n *\n * @param events - The events to merge.\n * @returns A new Event that forwards emissions from all sources.\n *\n * @example\n * ```typescript\n * const mouseEvent = createEvent<MouseEvent>();\n * const keyboardEvent = createEvent<KeyboardEvent>();\n * const inputEvent = merge(mouseEvent, keyboardEvent);\n * inputEvent.on(event => console.log('Input event:', event));\n * ```\n */\nexport const merge = <Events extends Event<any, any>[]>(...events: Events): Event<AllEventsParameters<Events>, AllEventsResults<Events>> => {\n const mergedEvent = new Event<AllEventsParameters<Events>, AllEventsResults<Events>>(() => {\n for (const event of events) {\n event.off(mergedEvent.sink);\n }\n });\n\n for (const event of events) {\n event.on(mergedEvent.sink);\n }\n return mergedEvent;\n};\n\n/**\n * Creates a periodic event that emits an incrementing counter (starting from 0) at a fixed interval.\n * Disposing the event clears the interval.\n *\n * @param interval - The interval in milliseconds.\n * @returns An Event that triggers at the specified interval.\n *\n * @example\n * ```typescript\n * const tickEvent = createInterval(1000);\n * tickEvent.on(tickNumber => console.log('Tick:', tickNumber));\n * ```\n */\nexport const createInterval = <R = unknown>(interval: number): Event<number, R> => {\n let counter = 0;\n const intervalEvent = new Event<number, R>(() => clearInterval(timerId));\n const timerId: ReturnType<typeof setInterval> = setInterval(() => {\n intervalEvent.emit(counter++);\n }, interval);\n return intervalEvent;\n};\n\n/**\n * Creates a new Event instance for multi-listener event handling.\n *\n * @example\n * ```typescript\n * const messageEvent = createEvent<string>();\n * messageEvent.on(msg => console.log('Received:', msg));\n * messageEvent.emit('Hello'); // All listeners receive 'Hello'\n *\n * // Listeners can return values, collected via DispatchResult\n * const validateEvent = createEvent<string, boolean>();\n * validateEvent.on(str => str.length > 0);\n * validateEvent.on(str => str.length < 100);\n * const results = await validateEvent.emit('test').all(); // [true, true]\n * ```\n */\nexport const createEvent = <T = unknown, R = unknown>(): Event<T, R> => new Event<T, R>();\n\nexport default createEvent;\n\n/**\n * Extracts the listener function type from an Event type.\n * Useful for type-safe listener definitions.\n *\n * @template E - The Event type to extract the listener type from\n *\n * @example\n * ```typescript\n * type MyEvent = Event<string, boolean>;\n * type MyListener = EventHandler<MyEvent>; // (value: string) => boolean | Promise<boolean>\n * ```\n */\nexport type EventHandler<E> = E extends Event<infer T, infer R> ? Listener<T, R> : never;\n\n/**\n * Extracts a filter function type for an Event's parameters.\n * Used for creating type-safe event filters.\n *\n * @template E - The Event type to create a filter for\n */\nexport type EventFilter<E> = FilterFunction<EventParameters<E>>;\n\n/**\n * Extracts a predicate function type for an Event's parameters.\n * Used for type narrowing with event values.\n *\n * @template E - The Event type to create a predicate for\n * @template P - The narrowed type that the predicate validates\n */\nexport type EventPredicate<E, P extends EventParameters<E>> = Predicate<EventParameters<E>, P>;\n\n/**\n * Extracts a mapper function type for transforming Event parameters.\n * Used for creating type-safe event value transformations.\n *\n * @template E - The Event type to create a mapper for\n * @template M - The target type to map event values to\n */\nexport type EventMapper<E, M> = Mapper<EventParameters<E>, M>;\n\n/**\n * Extracts a reducer function type for Event parameters.\n * Used for creating type-safe event value reducers.\n *\n * @template E - The Event type to create a reducer for\n * @template R - The accumulator type for the reduction\n */\nexport type EventReducer<E, R> = Reducer<EventParameters<E>, R>;\n"],"names":["Event","EventIterator","createEvent","createInterval","merge","signal","next","value","receive","done","undefined","return","ListenerRegistry","Signal","Symbol","toStringTag","onDispose","Disposer","sink","emit","bind","handleEvent","event","size","DispatchResult","dispatch","lacks","listener","has","off","on","once","clear","then","onfulfilled","onrejected","catch","finally","onfinally","settle","status","reason","asyncIterator","dispose","events","mergedEvent","interval","counter","intervalEvent","clearInterval","timerId","setInterval"],"mappings":"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"}