tests — events
tests — events
This document describes the core components of the src/events module, as revealed and validated by the tests/events/event-bus.test.ts test suite. This module provides a robust, type-safe eventing system designed for internal application communication, offering features like event history, statistics, listener prioritization, and filtering.
Overview
The src/events module provides a flexible and type-safe way to manage events within the application. It centers around the TypedEventEmitter class, which offers enhanced capabilities over standard Node.js EventEmitters, including strong typing for event names and payloads, event history, and performance statistics. A global EventBus is provided for application-wide communication, and utility type guards help with runtime event type narrowing.
Core Concepts
BaseEvent: The fundamental interface for all events, requiring at least atypestring and atimestamp.- Event Types: Events are categorized by their
typestring (e.g.,tool:started,agent:completed). The module defines various event interfaces (e.g.,ToolEvent,AgentEvent,SessionEvent) which extendBaseEventand add specific properties. AllEvents: A union type representing all possible events in the system, used by the globalEventBus.
Key Components
1. TypedEventEmitter
The TypedEventEmitter is the foundational class for event management. It's a generic class where TEventMap is an interface mapping event names (strings) to their corresponding event data types.
Features:
- Type-Safe Event Handling: Listeners are strongly typed based on the
TEventMapprovided.
type ToolEvents = {
39;tool:started39;: { toolName: string; args: Record<string, any> };
39;tool:completed39;: { toolName: string; duration: number };
};
const emitter = new TypedEventEmitter<ToolEvents>();
emitter.on(39;tool:started39;, (event) => {
class="hl-cmt">// event is correctly typed as { toolName: string; args: Record<string, any> }
console.log(`Tool ${event.toolName} started.`);
});
emitter.emit(39;tool:completed39;, { toolName: 39;search39;, duration: 100 });
on(type, listener, options?): Registers a listener for a specific event type.options.priority: Listeners with higher priority values are executed first.options.filter: A predicate function(event) => booleanthat must returntruefor the listener to be invoked.once(type, listener): Registers a listener that will be invoked only once for the specified event type.off(listenerId): Removes a specific listener using the ID returned byonoronce.offAll(type?): Removes all listeners for a given eventtype, or all listeners across all types if notypeis provided.emit(type, event): Emits an event. Returnstrueif there were listeners for the event,falseotherwise.onAny(listener): Registers a wildcard listener that receives all emitted events, regardless of type.- Event History:
getHistory(): Returns an array of recently emitted events, including their timestamp and the event object.clearHistory(): Clears the stored event history.maxHistorySize: Configurable option during instantiation to limit the history size.- Statistics:
getStats(): Returns an object containingtotalEmitted,totalListeners, andeventCounts(a map of event types to their emission counts).resetStats(): Resets the emission counts, but preserves listener counts.- Control:
setEnabled(enabled: boolean): Enables or disables event emission. When disabled,emitcalls are ignored.isEnabled(): Checks the current enabled state.eventNames(): Returns an array of event types for which listeners are currently registered.listenerCount(type?): Returns the number of listeners for a specific eventtype, or the total number of listeners if notypeis provided.waitFor(type, options?): Returns aPromisethat resolves with the next event of the specifiedtype.options.timeout: An optional timeout in milliseconds, after which the promise will reject if the event is not received.pipe(type, targetEmitter): Forwards all events of a specifictypefrom this emitter to anotherTypedEventEmitterinstance.dispose(): Cleans up internal resources, removing all listeners and clearing history/stats. Essential for preventing memory leaks, especially in long-running processes or when emitters are short-lived.
2. FilteredEventEmitter
The FilteredEventEmitter provides a mechanism to create a specialized emitter that only processes events from a source TypedEventEmitter that match a specific type and an additional filter function.
- Creation: Instances are created via
TypedEventEmitter.filter(eventType, filterFn). - Usage: It exposes
on,once,off, andoffAllmethods similar toTypedEventEmitter, but these methods only apply to the events that pass the initial type and thefilterFnprovided during its creation.
const emitter = new TypedEventEmitter<ToolEvents>();
const bashToolEvents = emitter.filter(39;tool:started39;, (event) => event.toolName === 39;bash39;);
bashToolEvents.on((event) => {
class="hl-cmt">// This listener only receives 39;tool:started39; events where toolName is 39;bash39;
console.log(`Bash tool started: ${event.args.command}`);
});
emitter.emit(39;tool:started39;, { toolName: 39;bash39;, args: { command: 39;ls39; } }); class="hl-cmt">// Received by bashToolEvents listener
emitter.emit(39;tool:started39;, { toolName: 39;search39;, args: { query: 39;foo39; } }); class="hl-cmt">// Not received
3. EventBus
The EventBus is a singleton instance of TypedEventEmitter, serving as the central hub for application-wide events.
EventBus.getInstance(): Retrieves the singleton instance of theEventBus.EventBus.resetInstance(): Resets the singleton instance, creating a new one on the nextgetInstance()call. This is primarily used for testing to ensure isolation between test runs.getGlobalEventBus()/getEventBus(): Convenience functions that simply returnEventBus.getInstance().resetEventBus(): Convenience function that callsEventBus.resetInstance().
4. TypedEventEmitterAdapter
This class acts as an adapter, wrapping a TypedEventEmitter instance and exposing both its type-safe API (onTyped, emitTyped, etc.) and the standard Node.js EventEmitter API (on, emit). This allows for interoperability with code that expects a native EventEmitter while still leveraging the type safety and advanced features of TypedEventEmitter internally.
onTyped(type, listener)/onceTyped(...)/offTyped(...)/emitTyped(...): The type-safe methods, delegating to the internalTypedEventEmitter.on(event, listener)/emit(event, ...args): The nativeEventEmittermethods.getTypedEmitter(): Returns the underlyingTypedEventEmitterinstance.getEventStats()/getEventHistory(): Proxies to the underlyingTypedEventEmitter's methods.dispose(): Cleans up the internalTypedEventEmitter.
5. Type Guards
The module provides several type guard functions to safely narrow down the type of a BaseEvent at runtime. These are crucial when working with AllEvents or BaseEvent unions, allowing you to access specific properties of an event without type assertions.
isEventType(event, type): Checks if an event'stypeproperty matches a given string literal.isAgentEvent(event): NarrowseventtoAgentEventif its type starts with'agent:'.isToolEvent(event): NarrowseventtoToolEventif its type starts with'tool:'.isSessionEvent(event): NarrowseventtoSessionEventif its type starts with'session:'.isFileEvent(event): NarrowseventtoFileEventif its type starts with'file:'.isCacheEvent(event): NarrowseventtoCacheEventif its type starts with'cache:'.isSyncEvent(event): NarrowseventtoSyncEventif its type starts with'sync:'.
import { getGlobalEventBus, isToolEvent, AllEvents } from 39;./events39;;
const bus = getGlobalEventBus();
bus.onAny((event: AllEvents) => {
if (isToolEvent(event)) {
class="hl-cmt">// event is now safely typed as ToolEvent
console.log(`Tool event: ${event.toolName} - Type: ${event.type}`);
} else {
console.log(`Other event: ${event.type}`);
}
});
Component Relationships
The following diagram illustrates the relationships between the main eventing components:
classDiagram
class EventEmitter {
+on(event, listener)
+emit(event, ...args)
}
class TypedEventEmitter<T> {
+on(type, listener, options)
+emit(type, event)
+getHistory()
+getStats()
+filter(type, filterFn) FilteredEventEmitter
+waitFor(type, options) Promise<Event>
+pipe(type, target)
+dispose()
}
class FilteredEventEmitter<TEvent> {
+on(listener)
+once(listener)
+off(id)
+offAll()
}
class EventBus {
+getInstance() EventBus
+resetInstance()
}
class TypedEventEmitterAdapter<T> {
+onTyped(type, listener)
+emitTyped(type, event)
+getTypedEmitter() TypedEventEmitter<T>
}
TypedEventEmitter <|-- EventBus : extends
TypedEventEmitterAdapter o-- TypedEventEmitter : aggregates
EventEmitter <|-- TypedEventEmitterAdapter : extends (conceptual)
TypedEventEmitter ..> FilteredEventEmitter : creates
EventBusextendsTypedEventEmitter, inheriting all its capabilities but specifically typed forAllEvents.TypedEventEmitterAdapteraggregates aTypedEventEmitterinternally and conceptually extendsEventEmitterto provide both typed and untyped interfaces.TypedEventEmitteris responsible for creatingFilteredEventEmitterinstances.
Usage Patterns and Best Practices
- Use
TypedEventEmitterfor module-specific events: When events are confined to a particular module or component, instantiate aTypedEventEmitterwith the relevantTEventMap. - Use
EventBusfor global events: For events that need to be broadcast across different parts of the application, use thegetGlobalEventBus()singleton. - Always
dispose(): If you createTypedEventEmitterorTypedEventEmitterAdapterinstances that are not long-lived (e.g., within a function scope or a component that gets destroyed), ensure you calldispose()to clean up listeners and prevent memory leaks. - Leverage Type Guards: When consuming events from the
EventBus(which emitsAllEvents), use the provided type guards to safely narrow down event types and access specific properties. - Prioritize Listeners: Use the
priorityoption inon()for listeners that need to execute in a specific order (e.g., logging before processing). - Filter at the Listener Level: For fine-grained control over which events a listener receives, use the
filteroption inon()or create aFilteredEventEmitter. - Asynchronous Event Handling:
waitFor()is useful for scenarios where you need to pause execution until a specific event occurs, with built-in timeout handling.