UNPKG

evnty

Version:

0-Deps, simple, fast, for browser and node js reactive anonymous event library

982 lines (907 loc) 35 kB
import { MaybePromise, Callback, Listener, FilterFunction, Predicate, Filter, Mapper, AsyncGenerable, Reducer, Expander } from './types.js'; import { Callable } from './callable.js'; import { Sequence } from './sequence.js'; export * from './types.js'; export * from './callable.js'; export * from './signal.js'; export * from './sequence.js'; /** * Removes a listener from the provided array of listeners. It searches for the listener and removes all instances of it from the array. * This method ensures that the listener is fully unregistered, preventing any residual calls to a potentially deprecated handler. * * @internal * @param {unknown[]} listeners - The array of listeners from which to remove the listener. * @param {unknown} listener - The listener function to remove from the list of listeners. * @returns {boolean} - Returns `true` if the listener was found and removed, `false` otherwise. * * @template T - The type of the event that listeners are associated with. * @template R - The type of the return value that listeners are expected to return. * * ```typescript * // Assuming an array of listeners for click events * const listeners = [onClickHandler1, onClickHandler2]; * const wasRemoved = removeListener(listeners, onClickHandler1); * console.log(wasRemoved); // Output: true * ``` */ export const removeListener = (listeners: unknown[], listener: unknown): boolean => { let index = listeners.indexOf(listener); const wasRemoved = index !== -1; while (~index) { listeners.splice(index, 1); index = listeners.indexOf(listener); } return wasRemoved; }; /** * @internal * Creates a promise that resolves after a specified timeout. If an `AbortSignal` is provided and triggered, * the timeout is cleared, and the promise resolves to `false`. * * @param {number} timeout - The time in milliseconds to wait before resolving the promise. * @param {AbortSignal} [signal] - An optional `AbortSignal` that can abort the timeout. * @returns {Promise<boolean>} A promise that resolves to `true` if the timeout completed, or `false` if it was aborted. * * @example * ```typescript * const controller = new AbortController(); * setTimeout(() => controller.abort(), 500); * const result = await setTimeoutAsync(1000, controller.signal); * console.log(result); // false * ``` */ export const setTimeoutAsync = (timeout: number, signal?: AbortSignal): Promise<boolean> => new Promise<boolean>((resolve) => { const timerId = setTimeout(resolve, timeout, true); signal?.addEventListener('abort', () => { clearTimeout(timerId); resolve(false); }); }); /* * @internal */ export interface AsyncIterableCombinators<T> { filter<U extends T>(filter: Filter<T, U>): AsyncIterableCombinators<Awaited<U>>; map<U>(mapper: Mapper<T, U>): AsyncIterableCombinators<Awaited<U>>; reduce<U>(reducer: Reducer<T, U>): AsyncIterableCombinators<Awaited<U>>; expand<U>(expander: Mapper<T, U[]>): AsyncIterableCombinators<Awaited<U>>; pipe<U>(expander: AsyncGenerable<T, U>): AsyncIterableCombinators<Awaited<U>>; } /* * @internal */ export interface EventSource<T> extends Callable<[T], boolean>, Promise<T>, AsyncIterable<T> { next(): Promise<T>; } /** * @internal */ /** * @internal */ export class Unsubscribe extends Callable<[], MaybePromise<void>> { private _done = false; constructor(callback: Callback) { super(async () => { this._done = true; await callback(); }); } get done() { return this._done; } pre(callback: Callback): Unsubscribe { return new Unsubscribe(async () => { await callback(); await this(); }); } post(callback: Callback): Unsubscribe { return new Unsubscribe(async () => { await this(); await callback(); }); } countdown(count: number): Unsubscribe { return new Unsubscribe(async () => { if (!--count) { await this(); } }); } } enum HookType { Add, Remove, } /** * A class representing an anonymous event that can be listened to or triggered. * * @template T - The event type. * @template R - The return type of the event. */ export class Event<T = unknown, R = unknown> extends Callable<[T], Promise<(void | Awaited<R>)[]>> implements AsyncIterable<T>, PromiseLike<T> { /** * The array of listeners for the event. */ private listeners: Listener<T, R>[]; private hooks: Array<(listener: Listener<T, R> | void, type: HookType) => void> = []; private _disposed = false; /** * A function that disposes of the event and its listeners. */ readonly dispose: Callback; /** * Creates a new event. * * @param dispose - A function to call on the event disposal. * * ```typescript * // Create a click event. * const clickEvent = new Event<[x: number, y: number], void>(); * clickEvent.on(([x, y]) => console.log(`Clicked at ${x}, ${y}`)); * ``` */ constructor(dispose?: Callback) { const listeners: Listener<T, R>[] = []; // passes listeners exceptions to catch method super((value: T): Promise<(void | Awaited<R>)[]> => Promise.all(listeners.map(async (listener) => listener(await value)))); this.listeners = listeners; this.dispose = () => { this._disposed = true; void this.clear(); void this._error?.dispose(); void dispose?.(); }; } private _error?: Event<unknown>; /** * Error event that emits errors. * * @returns {Event<unknown>} The error event. */ get error(): Event<unknown> { return (this._error ??= new Event<unknown>()); } /** * The number of listeners for the event. * * @readonly * @type {number} */ get size(): number { return this.listeners.length; } /** * Checks if the event has been disposed. * @returns {boolean} `true` if the event has been disposed; otherwise, `false`. */ get disposed(): boolean { return this._disposed; } /** * Checks if the given listener is NOT registered for this event. * * @param listener - The listener function to check against the registered listeners. * @returns `true` if the listener is not already registered; otherwise, `false`. * * ```typescript * // Check if a listener is not already added * if (event.lacks(myListener)) { * event.on(myListener); * } * ``` */ lacks(listener: Listener<T, R>): boolean { return this.listeners.indexOf(listener) === -1; } /** * Checks if the given listener is registered for this event. * * @param listener - The listener function to check. * @returns `true` if the listener is currently registered; otherwise, `false`. * * ```typescript * // Verify if a listener is registered * if (event.has(myListener)) { * console.log('Listener is already registered'); * } * ``` */ has(listener: Listener<T, R>): boolean { return this.listeners.indexOf(listener) !== -1; } /** * Removes a specific listener from this event. * * @param listener - The listener to remove. * @returns The event instance, allowing for method chaining. * * ```typescript * // Remove a listener * event.off(myListener); * ``` */ off(listener: Listener<T, R>): this { if (removeListener(this.listeners, listener) && this.hooks.length) { [...this.hooks].forEach((spy) => spy(listener, HookType.Remove)); } return this; } /** * Registers a listener that gets triggered whenever the event is emitted. * This is the primary method for adding event handlers that will react to the event being triggered. * * @param listener - The function to call when the event occurs. * @returns An object that can be used to unsubscribe the listener, ensuring easy cleanup. * * ```typescript * // Add a listener to an event * const unsubscribe = event.on((data) => { * console.log('Event data:', data); * }); * ``` */ on(listener: Listener<T, R>): Unsubscribe { this.listeners.push(listener); if (this.hooks.length) { [...this.hooks].forEach((spy) => spy(listener, HookType.Add)); } return new Unsubscribe(() => { void this.off(listener); }); } /** * Adds a listener that will be called only once the next time the event is emitted. * This method is useful for one-time notifications or single-trigger scenarios. * * @param listener - The listener to trigger once. * @returns An object that can be used to remove the listener if the event has not yet occurred. * * ```typescript * // Register a one-time listener * const onceUnsubscribe = event.once((data) => { * console.log('Received data once:', data); * }); * ``` */ once(listener: Listener<T, R>): Unsubscribe { const oneTimeListener = (event: T) => { void this.off(oneTimeListener); return listener(event); }; return this.on(oneTimeListener); } /** * Removes all listeners from the event, effectively resetting it. This is useful when you need to * cleanly dispose of all event handlers to prevent memory leaks or unwanted triggers after certain conditions. * * @returns {this} The instance of the event, allowing for method chaining. * * ```typescript * const myEvent = new Event(); * myEvent.on(data => console.log(data)); * myEvent.clear(); // Clears all listeners * ``` */ clear(): this { this.listeners.splice(0); if (this.hooks.length) { [...this.hooks].forEach((spy) => spy(undefined, HookType.Remove)); } return this; } /** * Enables the `Event` to be used in a Promise chain, resolving with the first emitted value. * * @template OK - The type of the fulfilled value returned by `onfulfilled` (defaults to the event's type). * @template ERR - The type of the rejected value returned by `onrejected` (defaults to `never`). * @param onfulfilled - A function called when the event emits its first value. * @param onrejected - A function called if an error occurs before the event emits. * @returns A Promise that resolves with the result of `onfulfilled` or `onrejected`. * * ```typescript * const clickEvent = new Event<[number, number]>(); * await clickEvent; * ``` */ then<OK = T, ERR = never>( onfulfilled?: ((value: T) => OK | PromiseLike<OK>) | null, onrejected?: ((reason: unknown) => ERR | PromiseLike<ERR>) | null, ): Promise<OK | ERR> { const subscribers: Unsubscribe[] = []; const promise = new Promise<T>((resolve, reject) => { subscribers.push(this.once(resolve)); subscribers.push(this.error.once(reject)); }); const unsubscribe = async <U>(value: U) => { await Promise.all(subscribers.map((u) => u())); return value; }; return promise .then(onfulfilled, onrejected) .then(unsubscribe) .catch(async (error) => { throw await unsubscribe(error); }); } /** * Waits for the event to settle, returning a `PromiseSettledResult`. * * @returns {Promise<PromiseSettledResult<T>>} A promise that resolves with the settled result. * * @example * ```typescript * const result = await event.settle(); * if (result.status === 'fulfilled') { * console.log('Event fulfilled with value:', result.value); * } else { * console.error('Event rejected with reason:', result.reason); * } * ``` */ async settle(): Promise<PromiseSettledResult<T>> { return await Promise.allSettled([this.promise]).then(([settled]) => settled); } /** * A promise that resolves with the first emitted value from this event. * * @returns {Promise<T>} The promise value. */ get promise(): Promise<T> { return this.then(); } /** * Makes this event iterable using `for await...of` loops. * * @returns An async iterator that yields values as they are emitted by this event. * * ```typescript * // Assuming an event that emits numbers * const numberEvent = new Event<number>(); * (async () => { * for await (const num of numberEvent) { * console.log('Number:', num); * } * })(); * await numberEvent(1); * await numberEvent(2); * await numberEvent(3); * ``` */ [Symbol.asyncIterator](): AsyncIterator<T> { const ctrl = new AbortController(); const sequence = new Sequence<T>(ctrl.signal); const emitEvent = (value: T) => { sequence(value); }; const unsubscribe = this.on(emitEvent).pre(() => { ctrl.abort('done'); }); const spy: (typeof this.hooks)[number] = (target = emitEvent, action) => { if (target === emitEvent && action === HookType.Remove) { void unsubscribe(); } }; this.hooks.push(spy); return sequence[Symbol.asyncIterator](); } /** * Transforms the event's values using a generator function, creating a new `Event` that emits the transformed values. * * @template PT - The type of values emitted by the transformed `Event`. * @template PR - The return type of the listeners of the transformed `Event`. * @param generator - A function that takes the original event's value and returns a generator (sync or async) that yields the transformed values. * @returns A new `Event` instance that emits the transformed values. * * ```typescript * const numbersEvent = new Event<number>(); * const evenNumbersEvent = numbersEvent.pipe(function*(value) { * if (value % 2 === 0) { * yield value; * } * }); * evenNumbersEvent.on((evenNumber) => console.log(evenNumber)); * await numbersEvent(1); * await numbersEvent(2); * await numbersEvent(3); * ``` */ pipe<PT, R>(generator: (event: T) => AsyncGenerator<PT, void, unknown> | Generator<PT, void, unknown>): Event<PT, R> { const emitEvent = async (value: T) => { try { for await (const generatedValue of generator(value)) { await result(generatedValue).catch(result.error); } } catch (e) { await result.error(e); } }; const unsubscribe = this.on(emitEvent).pre(() => { removeListener(this.hooks, hook); }); const hook: (typeof this.hooks)[number] = (target = emitEvent, action) => { if (target === emitEvent && action === HookType.Remove) { void unsubscribe(); } }; this.hooks.push(hook); const result = new Event<PT, R>(unsubscribe); return result; } /** * Creates an async generator that yields values as they are emitted by this event. * * @template PT - The type of values yielded by the async generator. * @param generator - An optional function that takes the original event's value and returns a generator (sync or async) * that yields values to include in the async generator. * @returns An async generator that yields values from this event as they occur. * * ```typescript * const numbersEvent = new Event<number>(); * const evenNumbersEvent = numbersEvent.pipe(function*(value) { * if (value % 2 === 0) { * yield value; * } * }); * evenNumbersEvent.on((evenNumber) => console.log(evenNumber)); * await numbersEvent(1); * await numbersEvent(2); * await numbersEvent(3); * ``` */ async *generator<PT>(generator: (event: T) => AsyncGenerator<PT, void, unknown> | Generator<PT, void, unknown>): AsyncGenerator<Awaited<PT>, void, unknown> { for await (const value of this.pipe(generator)) { yield value; } } /** * Filters events, creating a new event that only triggers when the provided filter function returns `true`. * This method can be used to selectively process events that meet certain criteria. * * @param {Filter<T, P>} predicate The filter function or predicate to apply to each event. * @returns {Event<P, R>} A new event that only triggers for filtered events. * * ```typescript * const keyPressedEvent = new Event<string>(); * const enterPressedEvent = keyPressedEvent.filter(key => key === 'Enter'); * enterPressedEvent.on(() => console.log('Enter key was pressed.')); * ``` */ filter<P extends T>(predicate: Predicate<T, P>): Event<P, R>; filter<P extends T>(filter: FilterFunction<T>): Event<P, R>; filter<P extends T>(filter: Filter<T, P>): Event<P, R> { return this.pipe<P, R>(async function* (value: T) { if (await filter(value)) { yield value as P; } }); } /** * Creates a new event that will only be triggered once when the provided filter function returns `true`. * This method is useful for handling one-time conditions in a stream of events. * * @param {Filter<T, P>} predicate - The filter function or predicate. * @returns {Event<P, R>} A new event that will be triggered only once when the filter condition is met. * * ```typescript * const sizeChangeEvent = new Event<number>(); * const sizeReachedEvent = sizeChangeEvent.first(size => size > 1024); * sizeReachedEvent.on(() => console.log('Size threshold exceeded.')); * ``` */ first<P extends T>(predicate: Predicate<T, P>): Event<P, R>; first<P extends T>(filter: FilterFunction<T>): Event<P, R>; first<P extends T>(filter: Filter<T, P>): Event<P, R> { const filteredEvent = this.pipe<P, R>(async function* (value: T) { if (await filter(value)) { yield value as P; await filteredEvent.dispose(); } }); return filteredEvent; } /** * Transforms the data emitted by this event using a mapping function. Each emitted event is processed by the `mapper` * function, which returns a new value that is then emitted by the returned `Event` instance. This is useful for data transformation * or adapting the event's data structure. * * @template M The type of data that the mapper function will produce. * @template MR The type of data emitted by the mapped event, usually related to or the same as `M`. * @param {Mapper<T, M>} mapper A function that takes the original event data and returns the transformed data. * @returns {Event<M, MR>} A new `Event` instance that emits the mapped values. * * ```typescript * // Assuming an event that emits numbers, create a new event that emits their squares. * const numberEvent = new Event<number>(); * const squaredEvent = numberEvent.map(num => num * num); * squaredEvent.on(squared => console.log('Squared number:', squared)); * await numberEvent(5); // Logs: "Squared number: 25" * ``` */ map<M, MR = R>(mapper: Mapper<T, M>): Event<Awaited<M>, MR> { return this.pipe(async function* (value) { yield await mapper(value); }); } /** * Accumulates the values emitted by this event using a reducer function, starting from an initial value. The reducer * function takes the accumulated value and the latest emitted event data, then returns a new accumulated value. This * new value is then emitted by the returned `Event` instance. This is particularly useful for accumulating state over time. * * @template A The type of the accumulator value. * @template AR The type of data emitted by the reduced event, usually the same as `A`. * @param {Reducer<T, A>} reducer A function that takes the current accumulated value and the new event data, returning the new accumulated value. * @param {A} init The initial value of the accumulator. * @returns {Event<A, AR>} A new `Event` instance that emits the reduced value. * * ```typescript * const sumEvent = numberEvent.reduce((a, b) => a + b, 0); * sumEvent.on((sum) => console.log(sum)); // 1, 3, 6 * await sumEvent(1); * await sumEvent(2); * await sumEvent(3); * ``` */ reduce<A, AR = R>(reducer: Reducer<T, A>, init?: A): Event<Awaited<A>, AR>; reduce<A, AR = R>(reducer: Reducer<T, A>, ...init: unknown[]): Event<Awaited<A>, AR> { let hasInit = init.length === 1; let result = init[0] as A | undefined; return this.pipe(async function* (value) { if (hasInit) { result = await reducer(result!, value); yield result; } else { result = value as unknown as A; hasInit = true; } }); } /** * Transforms each event's data into multiple events using an expander function. The expander function takes * the original event's data and returns an array of new data elements, each of which will be emitted individually * by the returned `Event` instance. This method is useful for scenarios where an event's data can naturally * be expanded into multiple, separate pieces of data which should each trigger further processing independently. * * @template ET - The type of data elements in the array returned by the expander function. * @template ER - The type of responses emitted by the expanded event, usually related to or the same as `ET`. * @param {Expander<T, ET[]>} expander - A function that takes the original event data and returns an array of new data elements. * @returns {Event<ET, ER>} - A new `Event` instance that emits each value from the array returned by the expander function. * * ```typescript * // Assuming an event that emits a sentence, create a new event that emits each word from the sentence separately. * const sentenceEvent = new Event<string>(); * const wordEvent = sentenceEvent.expand(sentence => sentence.split(' ')); * wordEvent.on(word => console.log('Word:', word)); * await sentenceEvent('Hello world'); // Logs: "Word: Hello", "Word: world" * ``` */ expand<ET, ER>(expander: Expander<T, ET[]>): Event<Awaited<ET>, ER> { return this.pipe(async function* (value) { const values = await expander(value); for (const value of values) { yield value; } }); } /** * Creates a new event that emits values based on a conductor event. The orchestrated event will emit the last value * captured from the original event each time the conductor event is triggered. * * @template T The type of data emitted by the original event. * @template R The type of data emitted by the orchestrated event, usually the same as `T`. * @param {Event<unknown, unknown>} conductor The event that triggers the emission of the last captured value. * @returns {Event<T, R>} A new event that emits values based on the conductor's triggers. * * ```typescript * const rightClickPositionEvent = mouseMoveEvent.orchestrate(mouseRightClickEvent); * ``` * * ```typescript * // An event that emits whenever a "tick" event occurs. * const tickEvent = new Event<void>(); * const dataEvent = new Event<string>(); * const synchronizedEvent = dataEvent.orchestrate(tickEvent); * synchronizedEvent.on(data => console.log('Data on tick:', data)); * await dataEvent('Hello'); * await dataEvent('World!'); * await tickEvent(); // Logs: "Data on tick: World!" * ``` */ orchestrate<CT, CR>(conductor: Event<CT, CR>): Event<T, R> { let initialized = false; let lastValue: T; const unsubscribe = this.on((event) => { initialized = true; lastValue = event; }); const unsubscribeConductor = conductor.on(async () => { if (initialized) { await orchestratedEvent(lastValue); initialized = false; } }); const orchestratedEvent = new Event<T, R>(unsubscribe.post(unsubscribeConductor)); return orchestratedEvent; } /** * Creates a debounced event that delays triggering until after a specified interval has elapsed * following the last time it was invoked. This method is particularly useful for limiting the rate * at which a function is executed. Common use cases include handling rapid user inputs, window resizing, * or scroll events. * * @param {number} interval - The amount of time to wait (in milliseconds) before firing the debounced event. * @returns {Event<T, R>} An event of debounced events. * * ```typescript * const debouncedEvent = textInputEvent.debounce(100); * debouncedEvent.on((str) => console.log(str)); // only 'text' is emitted * await event('t'); * await event('te'); * await event('tex'); * await event('text'); * ``` */ debounce(interval: number): Event<Awaited<T>, unknown> { let controller = new AbortController(); return this.pipe(async function* (value) { controller.abort(); controller = new AbortController(); const complete = await setTimeoutAsync(interval, controller.signal); if (complete) { yield value; } }); } /** * Creates a throttled event that emits values at most once per specified interval. * * This is useful for controlling the rate of event emissions, especially for high-frequency events. * The throttled event will immediately emit the first value, and then only emit subsequent values * if the specified interval has passed since the last emission. * * @param interval - The time interval (in milliseconds) between allowed emissions. * @returns A new Event that emits throttled values. * * ```typescript * const scrollEvent = new Event(); * const throttledScroll = scrollEvent.throttle(100); // Emit at most every 100ms * throttledScroll.on(() => console.log("Throttled scroll event")); * ``` */ throttle(interval: number): Event<Awaited<T>, unknown> { let timeout = 0; let pendingValue: T; let hasPendingValue = false; return this.pipe(async function* (value) { const now = Date.now(); if (timeout <= now) { timeout = now + interval; yield value; } else { pendingValue = value; if (!hasPendingValue) { hasPendingValue = true; await setTimeoutAsync(timeout - now); timeout = now + interval; hasPendingValue = false; yield pendingValue; } } }); } /** * Aggregates multiple event emissions into batches and emits the batched events either at specified * time intervals or when the batch reaches a predefined size. This method is useful for grouping * a high volume of events into manageable chunks, such as logging or processing data in bulk. * * @param {number} interval - The time in milliseconds between batch emissions. * @param {number} [size] - Optional. The maximum size of each batch. If specified, triggers a batch emission * once the batch reaches this size, regardless of the interval. * @returns {Event<T[], R>} An event of the batched results. * * ```typescript * // Batch messages for bulk processing every 1 second or when 10 messages are collected * const messageEvent = createEvent<string, void>(); * const batchedMessageEvent = messageEvent.batch(1000, 10); * batchedMessageEvent.on((messages) => console.log('Batched Messages:', messages)); * ``` */ batch(interval: number, size?: number): Event<T[], R> { let controller = new AbortController(); const batch: T[] = []; return this.pipe(async function* (value) { batch.push(value); if (size !== undefined && batch.length >= size) { controller.abort(); yield batch.splice(0); } if (batch.length === 1) { controller = new AbortController(); const complete = await setTimeoutAsync(interval, controller.signal); if (complete) { yield batch.splice(0); } } }); } /** * Creates a queue from the event, where each emitted value is sequentially processed. The returned object allows popping elements * from the queue, ensuring that elements are handled one at a time. This method is ideal for scenarios where order and sequential processing are critical. * * @returns {Queue<T>} An object representing the queue. The 'pop' method retrieves the next element from the queue, while 'stop' halts further processing. * * ```typescript * // Queueing tasks for sequential execution * const taskEvent = new Event<string>(); * const taskQueue = taskEvent.queue(); * (async () => { * console.log('Processing:', await taskQueue.pop()); // Processing: Task 1 * // Queue also can be used as a Promise * console.log('Processing:', await taskQueue); // Processing: Task 2 * })(); * await taskEvent('Task 1'); * await taskEvent('Task 2'); *``` * *```typescript * // Additionally, the queue can be used as an async iterator * const taskEvent = new Event<string>(); * const taskQueue = taskEvent.queue(); * (async () => { * for await (const task of taskQueue) { * console.log('Processing:', task); * } * })(); * await taskEvent('Task 1'); * await taskEvent('Task 2'); * ``` * */ queue(): Queue<T> { const ctrl = new AbortController(); const sequence = new Sequence<T>(ctrl.signal); const onEvent = (value: T) => { sequence(value); }; const unsubscribe = this.on(onEvent).pre(() => { ctrl.abort('done'); }); const pop = async () => await sequence; return { pop, stop: async () => { await unsubscribe(); }, get stopped() { return ctrl.signal.aborted; }, then<TResult1 = T, TResult2 = never>( onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null, ): Promise<TResult1 | TResult2> { return pop().then(onfulfilled, onrejected); }, [Symbol.asyncIterator]() { return { next: async () => { try { const value = await pop(); return { value, done: false }; } catch { return { value: undefined, done: true }; } }, }; }, }; } } export interface Queue<T> extends AsyncIterable<T>, PromiseLike<T> { pop(): Promise<T>; stop(): Promise<void>; stopped: boolean; } export type EventParameters<T> = T extends Event<infer P, any> ? P : never; export type EventResult<T> = T extends Event<any, infer R> ? R : never; export type AllEventsParameters<T extends Event<any, any>[]> = { [K in keyof T]: EventParameters<T[K]> }[number]; export type AllEventsResults<T extends Event<any, any>[]> = { [K in keyof T]: EventResult<T[K]> }[number]; /** * Merges multiple events into a single event. This function takes any number of `Event` instances * and returns a new `Event` that triggers whenever any of the input events trigger. The parameters * and results of the merged event are derived from the input events, providing a flexible way to * handle multiple sources of events in a unified manner. * * @template Events - An array of `Event` instances. * @param {...Events} events - A rest parameter that takes multiple events to be merged. * @returns {Event<AllEventsParameters<Events>, AllEventsResults<Events>>} - Returns a new `Event` instance * that triggers with the parameters and results of any of the merged input events. * * ```typescript * // Merging mouse and keyboard events into a single event * const mouseEvent = createEvent<MouseEvent>(); * const keyboardEvent = createEvent<KeyboardEvent>(); * const inputEvent = merge(mouseEvent, keyboardEvent); * inputEvent.on(event => console.log('Input event:', event)); * ``` */ export const merge = <Events extends Event<any, any>[]>(...events: Events): Event<AllEventsParameters<Events>, AllEventsResults<Events>> => { const mergedEvent = new Event<AllEventsParameters<Events>, AllEventsResults<Events>>(); events.forEach((event) => event.on(mergedEvent)); return mergedEvent; }; /** * Creates a periodic event that triggers at a specified interval. The event will automatically emit * an incrementing counter value each time it triggers, starting from zero. This function is useful * for creating time-based triggers within an application, such as updating UI elements, polling, * or any other timed operation. * * @template R - The return type of the event handler function, defaulting to `void`. * @param {number} interval - The interval in milliseconds at which the event should trigger. * @returns {Event<number, R>} - An `Event` instance that triggers at the specified interval, * emitting an incrementing counter value. * * ```typescript * // Creating an interval event that logs a message every second * const tickEvent = createInterval(1000); * tickEvent.on(tickNumber => console.log('Tick:', tickNumber)); * ``` */ export const createInterval = <R = unknown>(interval: number): Event<number, R> => { let counter = 0; const intervalEvent = new Event<number, R>(() => clearInterval(timerId)); const timerId: ReturnType<typeof setInterval> = setInterval(() => { void intervalEvent(counter++); }, interval); return intervalEvent; }; /** * Creates a new instance of the `Event` class, which allows for the registration of event handlers that get called when the event is emitted. * This factory function simplifies the creation of events by encapsulating the instantiation logic, providing a clean and simple API for event creation. * * @typeParam T - The tuple of argument types that the event will accept. * @typeParam R - The return type of the event handler function, which is emitted after processing the event data. * @returns {Event<T, R>} - A new instance of the `Event` class, ready to have listeners added to it. * * ```typescript * // Create a new event that accepts a string and returns the string length * const myEvent = createEvent<string, number>(); * myEvent.on((str: string) => str.length); * myEvent('hello').then(results => console.log(results)); // Logs: [5] * ``` */ export const createEvent = <T = unknown, R = unknown>(): Event<T, R> => new Event<T, R>(); export default createEvent; /** * A type helper that extracts the event listener type * * @typeParam E - The event type. */ export type EventHandler<E> = E extends Event<infer T, infer R> ? Listener<T, R> : never; /** * A type helper that extracts the event filter type * * @typeParam E The event type to filter. */ export type EventFilter<E> = FilterFunction<EventParameters<E>>; /** * A type helper that extracts the event predicate type * * @typeParam E The event type to predicate. */ export type EventPredicate<E, P extends EventParameters<E>> = Predicate<EventParameters<E>, P>; /** * A type helper that extracts the event mapper type * * @typeParam E The event type to map. * @typeParam M The new type to map `E` to. */ export type EventMapper<E, M> = Mapper<EventParameters<E>, M>; /** * A type helper that extracts the event mapper type * * @typeParam E The type of event to reduce. * @typeParam M The type of reduced event. */ export type EventReducer<E, R> = Reducer<EventParameters<E>, R>;