UNPKG

lever-ui-eventbus

Version:

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

394 lines (385 loc) 11.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/integrations/react/index.ts var react_exports = {}; __export(react_exports, { AsyncEventBus: () => AsyncEventBus, DeadEvent: () => DeadEvent, EventBus: () => EventBus, EventBusProvider: () => EventBusProvider, useEventBus: () => useEventBus, useEventBusManager: () => useEventBusManager, useEventCollection: () => useEventCollection, useEventPost: () => useEventPost, useEventState: () => useEventState, useEventSubscription: () => useEventSubscription, useLatestEvent: () => useLatestEvent }); module.exports = __toCommonJS(react_exports); // src/integrations/react/context.tsx var import_react = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var EventBusContext = (0, import_react.createContext)(null); function EventBusProvider({ bus, children }) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EventBusContext.Provider, { value: bus, children }); } function useEventBus() { const bus = (0, import_react.useContext)(EventBusContext); if (!bus) { throw new Error("useEventBus must be used within an EventBusProvider"); } return bus; } // src/integrations/react/hooks.ts var import_react2 = require("react"); function useEventSubscription(type, handler, deps) { const bus = useEventBus(); const subscriptionRef = (0, import_react2.useRef)(null); const memoizedHandler = (0, import_react2.useMemo)(() => handler, deps || [handler]); (0, import_react2.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]); (0, import_react2.useEffect)(() => { return () => { if (subscriptionRef.current) { subscriptionRef.current.unsubscribe(); } }; }, []); } function useEventPost() { const bus = useEventBus(); return (0, import_react2.useCallback)((event) => { return bus.post(event); }, [bus]); } function useEventState(type, initialValue, reducer) { const [state, setState] = (0, import_react2.useState)(initialValue); useEventSubscription(type, (event) => { setState((currentState) => reducer(currentState, event)); }, [reducer]); return state; } function useLatestEvent(type, initialValue = null) { const [latestEvent, setLatestEvent] = (0, import_react2.useState)(initialValue); useEventSubscription(type, setLatestEvent); return latestEvent; } function useEventCollection(type, maxSize = 100) { const [events, setEvents] = (0, import_react2.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] = (0, import_react2.useState)({}); const refresh = (0, import_react2.useCallback)(() => { forceUpdate({}); }, []); const activeTypes = (0, import_react2.useMemo)(() => bus.getActiveEventTypes(), [bus, forceUpdate]); const subscriptionCount = (0, import_react2.useMemo)(() => { return activeTypes.reduce((total, type) => { return total + bus.getSubscriptionCount(type); }, 0); }, [bus, activeTypes]); const clear = (0, import_react2.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)); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AsyncEventBus, DeadEvent, EventBus, EventBusProvider, useEventBus, useEventBusManager, useEventCollection, useEventPost, useEventState, useEventSubscription, useLatestEvent });