UNPKG

sub-events

Version:

Lightweight, strongly-typed events, with monitored subscriptions.

407 lines (406 loc) 13 kB
import { Subscription } from './sub'; import { EventConsumer } from './consumer'; /** * Schedule for emitting / broadcasting data to subscribers, to be used by method {@link SubEvent.emit}. * It represents a concurrency strategy for delivering event to subscribers. */ export declare enum EmitSchedule { /** * Data is sent to all subscribers synchronously / immediately. * * This is the default schedule. */ sync = "sync", /** * Data broadcast is fully asynchronous: each subscriber will be receiving the event * within its own processor tick (under Node.js), or timer tick (in browsers). * * Subscribers are enumerated after the initial delay. */ async = "async", /** * Wait for the next processor tick (under Node.js), or timer tick (in browsers), * and then broadcast data to all subscribers synchronously. * * Subscribers are enumerated after the delay. */ next = "next" } /** * Options to be used with method {@link SubEvent.emit}. */ export interface IEmitOptions { /** * Event-emitting schedule. Default is `sync`. */ schedule?: EmitSchedule; /** * Callback for catching all unhandled errors from subscribers, * from both synchronous and asynchronous subscription functions. * * ```ts * (err: any, name?: string) => void; * ``` * * @param err * `err`: The error that was thrown or rejected. * * @param name * `name`: The subscription `name`, if set during {@link SubEvent.subscribe} call. */ onError?: (err: any, name?: string) => void; /** * Notification callback of when the last recipient has received the data. * Note that asynchronous subscribers may still be processing the data at this point. * * ```ts * (count: number) => void; * ``` * * @param count * `count`: Total number of subscribers that have received the data. */ onFinished?: (count: number) => void; } /** * Subscription Context Interface, as used with {@link IEventOptions.onSubscribe} and {@link IEventOptions.onCancel} * notification options that can be set during {@link SubEvent} construction. */ export interface ISubContext<T = unknown> { /** * Event object that provides the context. */ readonly event: SubEvent<T>; /** * Subscription name, if one was specified with method {@link SubEvent.subscribe}. */ readonly name?: string; /** * Unknown-type data to let event wrappers persist any * context they need within the event's lifecycle. */ data?: any; } /** * Constructor options for {@link SubEvent} class. */ export interface IEventOptions<T> { /** * Maximum number of subscribers that can receive events. * Default is 0, meaning `no limit applies`. * * Newer subscriptions outside the maximum quota will start * receiving events when the older subscriptions get cancelled. */ maxSubs?: number; /** * Notification of a new subscriber being registered. * * ```ts * (ctx: ISubContext<T>) => void; * ``` * * @param ctx * `ctx`: {@link ISubContext} - Subscription Context. */ onSubscribe?: (ctx: ISubContext<T>) => void; /** * Notification about a cancelled subscription. * * ```js * (ctx: ISubContext<T>) => void; * ``` * * @param ctx * `ctx`: {@link ISubContext} - Subscription Context. */ onCancel?: (ctx: ISubContext<T>) => void; } /** * Options that can be passed into method {@link SubEvent.subscribe}. */ export interface ISubOptions { /** * Unique subscription name. It helps with diagnosing subscription leaks, * via method {@link SubEvent.getStat}, and provides additional details during error handling. * The name should help identify place in the code where the subscription was created. * * @see {@link SubEvent.getStat} */ name?: string; /** * Calling / `this` context for the subscription callback function. * * Standard way of passing in context is this way: * ```ts * event.subscribe(func.bind(this)) * ``` * * With this option you can also do it this way: * ```ts * event.subscribe(func, {thisArg: this}) * ``` */ thisArg?: any; /** * Subscription-cancel callback, to be notified on subscription explicit * {@link Subscription.cancel} call, or when cancelled implicitly via {@link SubEvent.cancelAll}. * * This is mostly for internal usage, and has no protection against * errors, should the handler throw any. */ onCancel?: () => void; } /** * Subscriptions statistics, as returned by method {@link SubEvent.getStat}. */ export interface ISubStat { /** * Map of subscription names to their usage counters. It consists of only * subscriptions for which option `name` was set when calling {@link SubEvent.subscribe}. */ named: { [name: string]: number; }; /** * Total number of unnamed subscriptions. */ unnamed: number; } /** * Subscription callback function type. */ export type SubFunction<T> = (data: T) => any; /** * Internal structure for each subscriber. * @hidden */ export interface ISubscriber<T> extends ISubContext<T> { /** * Event notification callback function. */ cb?: SubFunction<T>; /** * Cancels the subscription. */ cancel: (() => void); } /** * Core class, implementing event subscription + emitting the event. * * @see {@link subscribe}, {@link emit} */ export declare class SubEvent<T = unknown> { /** * Last emitted event, if there was any, or `undefined` otherwise. * * It is set after all subscribers have received the event, but just before * optional {@link IEmitOptions.onFinished} callback is invoked. */ get lastEvent(): T | undefined; /** * @hidden */ readonly options: IEventOptions<T>; /** * Internal list of subscribers. * @hidden */ protected _subs: ISubscriber<T>[]; /** * Last emitted event container. * @private */ private _lastEvent?; /** * Event constructor. * * @param options * Configuration Options. */ constructor(options?: IEventOptions<T>); /** * Returns a new {@link EventConsumer} for the event, which physically hides methods {@link SubEvent.emit} and {@link SubEvent.cancelAll}. * * This method simplifies creation of a receive-only event object representation. * * ```ts * const e = new SubEvent<number>(); // full-access, emit-receive event * * const c = e.toConsumer(); // the same "e" event, but with receive-only access * * // It is equivalent to the full syntax of: * // const c = new EventConsumer<number>(e); * ``` */ toConsumer<E extends SubEvent<T>>(): EventConsumer<T, E>; /** * Subscribes to the event. * * When subscription is no longer needed, method {@link Subscription.cancel} should be called on the * returned object, to avoid performance degradation caused by abandoned subscribers. * * Method {@link SubEvent.getStat} can help with diagnosing leaked subscriptions. * * @param cb * Event notification callback function. * * @param options * Subscription Options. * * @returns * Object for cancelling the subscription safely. * * @see {@link once} */ subscribe(cb: SubFunction<T>, options?: ISubOptions): Subscription; /** * Subscribes to receive just one event, and cancel the subscription immediately. * * You may still want to call {@link Subscription.cancel} on the returned object, * if you suddenly need to prevent the first event, or to avoid dead once-off * subscriptions that never received their event, and thus were not cancelled. * * @param cb * Event notification function, invoked after self-cancelling the subscription. * * @param options * Subscription Options. * * @returns * Object for cancelling the subscription safely. * * @see {@link toPromise} */ once(cb: SubFunction<T>, options?: ISubOptions): Subscription; /** * Broadcasts data to all subscribers, according to the `emit` schedule, * which is synchronous by default. * * @param data * Event data to be sent, according to the template type. * * @param options * Event-emitting options. * * @returns * The event object itself. */ emit(data: T, options?: IEmitOptions): this; /** * Current number of live subscriptions. */ get count(): number; /** * Maximum number of subscribers that can receive events. * Default is 0, meaning `no limit applies`. * * Newer subscriptions outside the maximum quota will start * receiving events when the older subscriptions get cancelled. * * It can only be set with the constructor. */ get maxSubs(): number; /** * Retrieves subscriptions statistics, to help with diagnosing subscription leaks. * * For this method to be useful, you need to set option `name` when calling {@link SubEvent.subscribe}. * * See also: {@link https://github.com/vitaly-t/sub-events/wiki/Diagnostics Diagnostics} * * @param options * Statistics Options: * * - `minUse: number` - Minimum subscription usage/count to be included into the list of named * subscriptions. If subscription is used fewer times, it will be excluded from the `named` list. * * @see {@link ISubStat} */ getStat(options?: { minUse?: number; }): ISubStat; /** * Cancels all existing subscriptions for the event. * * This is a convenience method for some special cases, when you want to cancel all subscriptions * for the event at once. Usually, subscribers just call {@link Subscription cancel} when they want to cancel their * own subscription. * * This method will always offer much better performance than cancelling each subscription individually, * which may become increasingly important when working with a large number of subscribers. * * @returns * Number of subscriptions cancelled. * * @see {@link Subscription.cancel} */ cancelAll(): number; /** * Creates a new subscription as a promise, to resolve with the next received event value, * and cancel the subscription. * * Examples of where it can be useful include: * - verify that a fast-pace subscription keeps receiving data; * - peek at fast-pace subscription data for throttled updates; * - for simpler receive-once / signal async processing logic. * * ```ts * try { * const nextValue = await myEvent.toPromise({timeout: 1000}); * } catch(e) { * // Either subscription didn't produce any event after 1 second, * // or myEvent.cancelAll() was called somewhere. * } * ``` * * The returned promise can reject in two cases: * - when the timeout has been reached (if set via option `timeout`), it rejects with `Event timed out` error; * - when {@link cancelAll} is called on the event object, it rejects with `Event cancelled` error. * * Note that if you use this method consecutively, you can miss events in between, * because the subscription is auto-cancelled after receiving the first event. * * @param options * Subscription Options: * * - `name` - for the internal subscription name. See `name` in {@link ISubOptions}. * In this context, it is also included within any rejection error. * * - `timeout` - sets timeout in ms (when `timeout` >= 0), to auto-reject with * `Event timed out` error. * * @see {@link once} */ toPromise(options?: { name?: string; timeout?: number; }): Promise<T>; /** * Gets all recipients that must receive data. * * It returns a copy of subscribers' array for safe iteration, while applying the * maximum limit when it is set with the {@link IEventOptions.maxSubs} option. * * @hidden */ protected _getRecipients(): ISubscriber<T>[]; /** * Creates unsubscribe callback function for the {@link Subscription} class. * @hidden * * @param sub * Subscriber details. * * @returns * Function that implements the `unsubscribe` request. */ protected _createCancel(sub: ISubscriber<T>): () => void; /** * Cancels an existing subscription. * @hidden * * @param sub * Subscriber to be removed, which must be on the list. */ protected _cancelSub(sub: ISubscriber<T>): void; }