UNPKG

lever-ui-eventbus

Version:

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

357 lines (350 loc) 10 kB
// src/integrations/react/context.tsx import { createContext, useContext } from "react"; import { jsx } from "react/jsx-runtime"; var EventBusContext = createContext(null); function EventBusProvider({ bus, children }) { return /* @__PURE__ */ jsx(EventBusContext.Provider, { value: bus, children }); } function useEventBus() { const bus = useContext(EventBusContext); if (!bus) { throw new Error("useEventBus must be used within an EventBusProvider"); } return bus; } // src/integrations/react/hooks.ts import { useEffect, useRef, useState, useCallback, useMemo } from "react"; function useEventSubscription(type, handler, deps) { const bus = useEventBus(); const subscriptionRef = useRef(null); const memoizedHandler = useMemo(() => handler, deps || [handler]); useEffect(() => { if (subscriptionRef.current) { subscriptionRef.current.unsubscribe(); } subscriptionRef.current = bus.subscribe(type, memoizedHandler); return () => { if (subscriptionRef.current) { subscriptionRef.current.unsubscribe(); subscriptionRef.current = null; } }; }, [bus, type, memoizedHandler]); useEffect(() => { return () => { if (subscriptionRef.current) { subscriptionRef.current.unsubscribe(); } }; }, []); } function useEventPost() { const bus = useEventBus(); return useCallback((event) => { return bus.post(event); }, [bus]); } function useEventState(type, initialValue, reducer) { const [state, setState] = useState(initialValue); useEventSubscription(type, (event) => { setState((currentState) => reducer(currentState, event)); }, [reducer]); return state; } function useLatestEvent(type, initialValue = null) { const [latestEvent, setLatestEvent] = useState(initialValue); useEventSubscription(type, setLatestEvent); return latestEvent; } function useEventCollection(type, maxSize = 100) { const [events, setEvents] = useState([]); useEventSubscription(type, (event) => { setEvents((currentEvents) => { const newEvents = [event, ...currentEvents]; return newEvents.slice(0, maxSize); }); }, [maxSize]); return events; } function useEventBusManager() { const bus = useEventBus(); const [, forceUpdate] = useState({}); const refresh = useCallback(() => { forceUpdate({}); }, []); const activeTypes = useMemo(() => bus.getActiveEventTypes(), [bus, forceUpdate]); const subscriptionCount = useMemo(() => { return activeTypes.reduce((total, type) => { return total + bus.getSubscriptionCount(type); }, 0); }, [bus, activeTypes]); const clear = useCallback(() => { bus.clear(); refresh(); }, [bus, refresh]); return { activeTypes, subscriptionCount, clear, refresh, getSubscriptionCount: (type) => bus.getSubscriptionCount(type), unsubscribeAll: (type) => { const count = bus.unsubscribeAll(type); refresh(); return count; } }; } // 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, EventBusProvider, useEventBus, useEventBusManager, useEventCollection, useEventPost, useEventState, useEventSubscription, useLatestEvent };