lever-ui-eventbus
Version:
A minimal TypeScript event bus: subscribe(Class, handler), async delivery, dead events, and polymorphic dispatch.
264 lines (257 loc) • 8.29 kB
TypeScript
/**
* A constructor type that can create instances of T.
* @template T The type that this constructor creates
*/
type Constructor<T = any> = new (...args: any[]) => T;
/**
* Context information provided when a subscriber throws an exception.
* @template T The event type
*/
interface SubscriberExceptionContext<T = unknown> {
/** The event that was being processed */
event: T;
/** The constructor/class of the event type */
eventType: Constructor<any>;
/** The handler function that threw the exception */
handler: (e: T) => void;
/** The EventBus instance that was processing the event */
eventBus: any;
}
/**
* Handler function for exceptions thrown by event subscribers.
*
* @param error The exception that was thrown
* @param ctx Context information about the failed event delivery
*
* @example
* ```ts
* const errorHandler: SubscriberExceptionHandler = (error, ctx) => {
* logger.error(`Event ${ctx.eventType.name} failed:`, error);
* };
* const bus = new EventBus(errorHandler);
* ```
*/
type SubscriberExceptionHandler = (error: unknown, ctx: SubscriberExceptionContext) => void;
/**
* Represents an active subscription that can be cancelled.
*/
interface Subscription {
/**
* Unsubscribe this specific handler from the event bus.
* After calling this, the handler will no longer receive events.
*/
unsubscribe(): void;
}
/**
* Internal handler record structure.
* @internal
*/
interface HandlerRec<T = unknown> {
fn: (e: T) => void;
}
/**
* Event emitted when no handler is found for a posted event.
* This helps detect unhandled events and avoid silent failures.
*
* @example
* ```ts
* bus.subscribe(DeadEvent, (e) => {
* console.warn('Unhandled event:', e.event);
* });
* ```
*/
declare class DeadEvent {
source: any;
event: unknown;
/**
* @param source The EventBus that could not deliver the event
* @param event The original event that was unhandled
*/
constructor(source: any, event: unknown);
}
/**
* Resolves the prototype chain for an event to determine all applicable types.
* Uses WeakMap caching for performance optimization.
* @internal
*/
declare class TypeResolver {
private typeCache;
/**
* Get all constructor types in the prototype chain for an event.
* Results are cached for performance.
*/
getTypesFor(event: unknown, hasObjectHandler: boolean): Constructor[];
/**
* Clear the type cache (useful when clearing the event bus).
*/
clearCache(): void;
}
/**
* A synchronous event bus that delivers events to registered handlers.
*
* Features:
* - Type-safe event subscription
* - Polymorphic dispatch (handlers for base classes receive subclass events)
* - Dead event handling for undelivered events
* - Exception handling for subscriber errors
*
* @example
* ```ts
* class OrderCreated { constructor(public id: string) {} }
*
* const bus = new EventBus();
* const subscription = bus.subscribe(OrderCreated, (event) => {
* console.log('Order:', event.id);
* });
*
* bus.post(new OrderCreated('123')); // logs: "Order: 123"
* subscription.unsubscribe();
* ```
*/
declare class EventBus {
protected exceptionHandler: SubscriberExceptionHandler;
protected registry: Map<Constructor, Set<HandlerRec<unknown>>>;
protected typeResolver: TypeResolver;
/**
* Create a new EventBus.
*
* @param exceptionHandler Handler for exceptions thrown by subscribers.
* Defaults to logging to console.error.
*/
constructor(exceptionHandler?: SubscriberExceptionHandler);
/**
* Subscribe to events of a specific type.
*
* @template T The event type to subscribe to
* @param type The constructor/class of events to listen for
* @param handler Function to call when events of this type are posted
* @returns Subscription object that can be used to unsubscribe
*
* @example
* ```ts
* class UserLoggedIn { constructor(public userId: string) {} }
*
* const sub = bus.subscribe(UserLoggedIn, (event) => {
* console.log('User logged in:', event.userId);
* });
* ```
*/
subscribe<T>(type: Constructor<T>, handler: (e: T) => void): Subscription;
/**
* Remove all handlers for a specific event type.
*
* @param type The constructor/class to remove all handlers for
* @returns The number of handlers that were removed
*
* @example
* ```ts
* const removed = bus.unsubscribeAll(UserLoggedIn);
* console.log(`Removed ${removed} handlers`);
* ```
*/
unsubscribeAll(type: Constructor): number;
/**
* Get the number of active subscriptions for a specific event type.
*
* @param type The constructor/class to count handlers for
* @returns The number of active handlers for this type
*
* @example
* ```ts
* const count = bus.getSubscriptionCount(UserLoggedIn);
* console.log(`${count} handlers for UserLoggedIn`);
* ```
*/
getSubscriptionCount(type: Constructor): number;
/**
* Get all event types that have active subscriptions.
*
* @returns Array of constructor functions that have handlers
*
* @example
* ```ts
* const activeTypes = bus.getActiveEventTypes();
* console.log('Subscribed types:', activeTypes.map(t => t.name));
* ```
*/
getActiveEventTypes(): Constructor[];
/**
* Remove all subscriptions from the event bus.
*
* @returns The total number of handlers that were removed
*
* @example
* ```ts
* const totalRemoved = bus.clear();
* console.log(`Cleared ${totalRemoved} handlers`);
* ```
*/
clear(): number;
/**
* Post an event to all registered handlers.
*
* Handlers are called for the exact type and all parent types in the prototype chain.
* If no handlers are found, a DeadEvent is posted instead.
*
* @template T The type of event being posted
* @param event The event instance to deliver
* @returns The number of handlers that received the event
*
* @example
* ```ts
* const delivered = bus.post(new UserLoggedIn('user123'));
* console.log(`Event delivered to ${delivered} handlers`);
* ```
*/
post<T>(event: T): number;
/**
* Deliver an event to a specific handler with exception handling.
* @internal
*/
protected deliver(fn: (e: unknown) => void, type: Constructor, event: unknown): void;
}
/**
* An asynchronous event bus that delivers events via a pluggable executor.
*
* All event delivery is deferred through the executor function, allowing for
* custom scheduling strategies like setTimeout, process.nextTick, or custom queues.
*
* @example
* ```ts
* // Using setTimeout for delivery
* const asyncBus = new AsyncEventBus((task) => setTimeout(task, 0));
*
* // Using custom executor
* const customBus = new AsyncEventBus((task) => {
* myCustomScheduler.schedule(task);
* });
*
* asyncBus.subscribe(OrderCreated, (event) => {
* console.log('Async order:', event.id); // called asynchronously
* });
*
* asyncBus.post(new OrderCreated('123'));
* console.log('Posted'); // this logs first
* ```
*/
declare class AsyncEventBus extends EventBus {
private executor;
/**
* Create a new AsyncEventBus.
*
* @param executor Function that schedules task execution. Defaults to queueMicrotask.
* @param exceptionHandler Handler for exceptions thrown by subscribers.
*/
constructor(executor?: (task: () => void) => void, exceptionHandler?: SubscriberExceptionHandler);
/**
* Deliver an event asynchronously using the configured executor.
* @internal
*/
protected deliver(fn: (e: unknown) => void, type: Constructor, event: unknown): void;
}
/**
* Default exception handler that logs errors to console.
* @internal
*/
declare const defaultExceptionHandler: SubscriberExceptionHandler;
export { AsyncEventBus, type Constructor, DeadEvent, EventBus, type HandlerRec, type SubscriberExceptionContext, type SubscriberExceptionHandler, type Subscription, TypeResolver, defaultExceptionHandler };