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
JavaScript
// 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
};