UNPKG

lever-ui-eventbus

Version:

A minimal TypeScript event bus: subscribe(Class, handler), async delivery, dead events, and polymorphic dispatch.

252 lines (247 loc) 6.93 kB
// src/dead-event.ts var DeadEvent = class { /** * @param source The EventBus that could not deliver the event * @param event The original event that was unhandled */ constructor(source, event) { this.source = source; this.event = event; } }; // src/utils/exception-handler.ts var defaultExceptionHandler = (error, ctx) => { console.error(`[EventBus] Error delivering ${ctx.eventType.name}:`, error); }; // src/utils/type-resolver.ts var TypeResolver = class { constructor() { this.typeCache = /* @__PURE__ */ new WeakMap(); } /** * Get all constructor types in the prototype chain for an event. * Results are cached for performance. */ getTypesFor(event, hasObjectHandler) { if (!event || typeof event !== "object") return []; const cached = this.typeCache.get(event); if (cached) { return hasObjectHandler && !cached.includes(Object) ? [...cached, Object] : cached; } const out = []; let ctor = event?.constructor ?? null; while (ctor && ctor !== Object) { out.push(ctor); const proto = Object.getPrototypeOf(ctor.prototype); ctor = proto?.constructor ?? null; } if (hasObjectHandler) { out.push(Object); } this.typeCache.set(event, out); return out; } /** * Clear the type cache (useful when clearing the event bus). */ clearCache() { this.typeCache = /* @__PURE__ */ new WeakMap(); } }; // src/event-bus.ts var EventBus = class { /** * Create a new EventBus. * * @param exceptionHandler Handler for exceptions thrown by subscribers. * Defaults to logging to console.error. */ constructor(exceptionHandler = defaultExceptionHandler) { this.exceptionHandler = exceptionHandler; this.registry = /* @__PURE__ */ new Map(); this.typeResolver = new TypeResolver(); } /** * 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(type, handler) { const set = this.registry.get(type) ?? /* @__PURE__ */ new Set(); const rec = { fn: handler }; set.add(rec); this.registry.set(type, set); return { unsubscribe: () => { const current = this.registry.get(type); if (!current) return; current.delete(rec); if (current.size === 0) this.registry.delete(type); } }; } /** * 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) { const set = this.registry.get(type); const count = set?.size ?? 0; this.registry.delete(type); return count; } /** * 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) { return this.registry.get(type)?.size ?? 0; } /** * 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() { return Array.from(this.registry.keys()).filter( (type) => this.registry.get(type).size > 0 ); } /** * 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() { let total = 0; for (const set of this.registry.values()) { total += set.size; } this.registry.clear(); this.typeResolver.clearCache(); return total; } /** * 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(event) { if (event == null) return 0; let delivered = 0; const types = this.typeResolver.getTypesFor( event, this.registry.has(Object) ); const handlersToCall = []; for (const type of types) { const set = this.registry.get(type); if (!set || set.size === 0) continue; for (const rec of set) { handlersToCall.push({ fn: rec.fn, type }); delivered++; } } for (const handler of handlersToCall) { this.deliver(handler.fn, handler.type, event); } if (delivered === 0 && !(event instanceof DeadEvent)) { this.post(new DeadEvent(this, event)); } return delivered; } /** * Deliver an event to a specific handler with exception handling. * @internal */ deliver(fn, type, event) { try { fn(event); } catch (err) { this.exceptionHandler(err, { event, eventType: type, handler: fn, eventBus: this }); } } }; // src/async-event-bus.ts var AsyncEventBus = class extends EventBus { /** * 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) => queueMicrotask(task), exceptionHandler) { super(exceptionHandler ?? defaultExceptionHandler); this.executor = executor; } /** * Deliver an event asynchronously using the configured executor. * @internal */ deliver(fn, type, event) { this.executor(() => super.deliver(fn, type, event)); } }; export { AsyncEventBus, DeadEvent, EventBus, TypeResolver, defaultExceptionHandler };