src — events

Module: src-events Cohesion: 0.80 Members: 0

src — events

The src/events module provides a robust, type-safe, and centralized event system for the application. It aims to replace or augment the scattered use of Node.js's native EventEmitter with a unified, developer-friendly solution that offers strong TypeScript support, advanced listening capabilities, and a clear structure for application-wide communication.

Purpose and Key Features

The primary goal of this module is to establish a consistent and predictable way for different parts of the application to communicate without direct coupling. By leveraging TypeScript, it ensures that event names and their associated data payloads are strictly typed, preventing common runtime errors and improving developer experience through auto-completion and compile-time checks.

Key features include:

Core Concepts

At the heart of the event system are a few fundamental concepts:

  1. BaseEvent: All events must extend this interface, which defines common properties like type (a string identifier) and timestamp.
  2. Event Type Maps: Interfaces like ApplicationEvents and AllEvents map event names (e.g., 'agent:started') to their specific event data interfaces (e.g., AgentEvent). These maps are crucial for providing type safety across the system.
  3. TypedEventEmitter: The generic class that implements the core event emission and listening logic, enforcing type safety based on the provided event map.
  4. EventBus: A singleton instance of TypedEventEmitter configured with AllEvents, serving as the global communication hub.

Module Architecture

The src/events module is structured into several interconnected files, each with a specific responsibility:

classDiagram
    direction LR
    class TypedEventEmitter {
        +emit()
        +on()
        +filter()
        +waitFor()
    }
    class FilteredEventEmitter {
        +on()
        +waitFor()
    }
    class EventBus {
        +static getInstance()
    }
    class TypedEventEmitterAdapter {
        +emitTyped()
        +onTyped()
    }
    class EventEmitter {
        <<Node.js>>
    }

    TypedEventEmitter "1" -- "0..N" FilteredEventEmitter : creates
    EventBus "1" *-- "1" TypedEventEmitter : wraps (singleton)
    TypedEventEmitterAdapter "1" *-- "1" TypedEventEmitter : wraps
    TypedEventEmitterAdapter --|> EventEmitter : extends Node.js EventEmitter
    TypedEventEmitter "1" *-- "1" EventEmitter : wraps for error handling

src/events/types.ts: Event Definitions

This file is the backbone of the type-safe event system. It defines:

Contribution Note: When adding new event types, define a new interface extending BaseEvent and add it to the AllEvents (and potentially other category-specific) interface.

src/events/typed-emitter.ts: The Type-Safe Emitter

This file contains the core TypedEventEmitter class and the TypedEventEmitterAdapter for migration.

TypedEventEmitter

This is the primary class for creating type-safe event emitters. It manages listeners, handles event emission, and provides advanced features:

TypedEventEmitterAdapter

This class is designed to facilitate a gradual migration from existing codebases that use Node.js's native EventEmitter.

This adapter allows developers to incrementally update their event handling logic without breaking existing functionality.

src/events/filtered-emitter.ts: Scoped Event Views

The FilteredEventEmitter class provides a specialized "view" of events from a TypedEventEmitter. It's not meant to be instantiated directly but is returned by TypedEventEmitter.filter().

This component simplifies listening to specific subsets of events without repeatedly defining the same filter logic.

src/events/event-bus.ts: The Global Event Bus

This file provides the singleton EventBus for application-wide event communication.

The global EventBus is the central nervous system for decoupled communication across the application.

src/events/type-guards.ts: Runtime Type Narrowing

This file provides utility functions for runtime type checking of events.

Usage Patterns

Using TypedEventEmitter Directly

For module-specific or component-specific eventing, you can instantiate TypedEventEmitter directly:

import { TypedEventEmitter, ToolEvents } from &#39;./events/index.js&#39;;

class="hl-cmt">// Create an emitter for ToolEvents
const toolEmitter = new TypedEventEmitter<ToolEvents>();

class="hl-cmt">// Listen for a specific tool event
toolEmitter.on(&#39;tool:started&#39;, (event) => {
  console.log(`Tool &#39;${event.toolName}&#39; started with args:`, event.args);
});

class="hl-cmt">// Listen for any tool event
toolEmitter.onAny((event) => {
  if (event.type.startsWith(&#39;tool:&#39;)) {
    console.log(`A tool event occurred: ${event.type}`);
  }
});

class="hl-cmt">// Emit a tool event (type-safe)
toolEmitter.emit(&#39;tool:started&#39;, { toolName: &#39;git&#39;, args: { command: &#39;clone&#39; } });
toolEmitter.emit(&#39;tool:completed&#39;, { toolName: &#39;git&#39;, result: { success: true, output: &#39;Cloned repo&#39; } });

Using the Global EventBus

For application-wide events, use the singleton EventBus:

import { getGlobalEventBus, AllEvents } from &#39;./events/index.js&#39;;

const bus = getGlobalEventBus();

class="hl-cmt">// Listen for an agent event
bus.on(&#39;agent:started&#39;, (event) => {
  console.log(`Agent &#39;${event.agentId}&#39; has started.`);
});

class="hl-cmt">// Listen for a file event with a filter
const listenerId = bus.on(&#39;file:modified&#39;, (event) => {
  console.log(`File modified: ${event.filePath}`);
}, {
  filter: (event) => event.filePath.endsWith(&#39;.ts&#39;)
});

class="hl-cmt">// Emit events
bus.emit(&#39;agent:started&#39;, { agentId: &#39;my-agent-123&#39; });
bus.emit(&#39;file:modified&#39;, { filePath: &#39;/src/main.ts&#39;, operation: &#39;edit&#39; });
bus.emit(&#39;file:modified&#39;, { filePath: &#39;/README.md&#39;, operation: &#39;edit&#39; }); class="hl-cmt">// This won&#39;t trigger the listener above

class="hl-cmt">// Later, remove the listener
bus.off(listenerId);

Filtering Events with FilteredEventEmitter

You can create a filtered view for a specific event type:

import { getGlobalEventBus, AllEvents } from &#39;./events/index.js&#39;;

const bus = getGlobalEventBus();

class="hl-cmt">// Create a filtered emitter for &#39;file:modified&#39; events on &#39;.js&#39; files
const jsFileModifiedEmitter = bus.filter(&#39;file:modified&#39;, (event) => event.filePath.endsWith(&#39;.js&#39;));

jsFileModifiedEmitter.on((event) => {
  console.log(`JavaScript file modified: ${event.filePath}`);
});

bus.emit(&#39;file:modified&#39;, { filePath: &#39;index.js&#39;, operation: &#39;save&#39; }); class="hl-cmt">// Triggers listener
bus.emit(&#39;file:modified&#39;, { filePath: &#39;style.css&#39;, operation: &#39;save&#39; }); class="hl-cmt">// Does not trigger

Waiting for Events

The waitFor method is useful for scenarios where you need to react to the next occurrence of an event:

import { getGlobalEventBus, AllEvents } from &#39;./events/index.js&#39;;

const bus = getGlobalEventBus();

async function waitForAgentCompletion() {
  console.log(&#39;Waiting for agent to complete...&#39;);
  try {
    const completionEvent = await bus.waitFor(&#39;agent:stopped&#39;, { timeout: 5000 });
    console.log(`Agent &#39;${completionEvent.agentId}&#39; stopped successfully.`);
  } catch (error) {
    console.error(&#39;Agent did not stop in time:&#39;, error);
  }
}

waitForAgentCompletion();

class="hl-cmt">// Simulate agent stopping after a delay
setTimeout(() => {
  bus.emit(&#39;agent:stopped&#39;, { agentId: &#39;my-agent-456&#39; });
}, 2000);

Gradual Migration with TypedEventEmitterAdapter

If you have existing classes that extend Node.js EventEmitter, you can switch them to TypedEventEmitterAdapter to gradually introduce type safety:

import { TypedEventEmitterAdapter, SessionEvent } from &#39;./events/index.js&#39;;
import { EventEmitter } from &#39;events&#39;; class="hl-cmt">// Native EventEmitter

interface MySessionEvents extends Record<string, SessionEvent> {
  &#39;session:started&#39;: SessionEvent;
  &#39;session:ended&#39;: SessionEvent;
  &#39;legacy-event&#39;: { data: string }; class="hl-cmt">// Keep old untyped events if needed
}

class="hl-cmt">// Old class (extends native EventEmitter)
class OldSessionManager extends EventEmitter {
  startSession(sessionId: string) {
    this.emit(&#39;session:started&#39;, { sessionId, timestamp: Date.now() }); class="hl-cmt">// Untyped emit
    this.emit(&#39;legacy-event&#39;, { data: &#39;old data&#39; });
  }
}

class="hl-cmt">// New class (extends TypedEventEmitterAdapter)
class NewSessionManager extends TypedEventEmitterAdapter<MySessionEvents> {
  startSession(sessionId: string) {
    class="hl-cmt">// New type-safe API
    this.emitTyped(&#39;session:started&#39;, { sessionId });

    class="hl-cmt">// Old API still works for backward compatibility
    this.emit(&#39;legacy-event&#39;, { data: &#39;new data&#39; }); class="hl-cmt">// This will also trigger native EventEmitter listeners
  }
}

const newManager = new NewSessionManager();

class="hl-cmt">// Listen using the new type-safe API
newManager.onTyped(&#39;session:started&#39;, (event) => {
  console.log(`Typed session started: ${event.sessionId}`);
});

class="hl-cmt">// Listen using the old native EventEmitter API (still works)
newManager.on(&#39;legacy-event&#39;, (data: { data: string }) => {
  console.log(`Legacy event received: ${data.data}`);
});

newManager.startSession(&#39;session-abc&#39;);

Event History and Statistics

TypedEventEmitter instances (including the EventBus) track emitted events and listener counts:

import { getGlobalEventBus } from &#39;./events/index.js&#39;;

const bus = getGlobalEventBus();

bus.emit(&#39;agent:started&#39;, { agentId: &#39;test-agent&#39; });
bus.emit(&#39;tool:completed&#39;, { toolName: &#39;test-tool&#39;, result: { success: true } });

console.log(&#39;Event History:&#39;, bus.getHistory());
console.log(&#39;Event Stats:&#39;, bus.getStats());

class="hl-cmt">// Filter history
const agentEvents = bus.getFilteredHistory((entry) => entry.event.type.startsWith(&#39;agent:&#39;));
console.log(&#39;Agent Event History:&#39;, agentEvents);

Integration Points

The event system is designed to be a central communication layer. Based on the call graph, here's how other parts of the codebase interact with it:

This demonstrates that core application logic, such as tool execution and cloud synchronization, relies on the event system for reporting status and enabling decoupled reactions.

Contributing to Events

When adding new event types or categories:

  1. Define Event Interface: Create a new interface in src/events/types.ts that extends BaseEvent. Ensure it has a unique type string literal and defines all necessary payload properties.
    export interface MyNewFeatureEvent extends BaseEvent {
      type: &#39;myfeature:started&#39; | &#39;myfeature:completed&#39;;
      featureId: string;
      class="hl-cmt">// ... other properties
    }

  1. Add to AllEvents: Include your new event interface in the AllEvents map in src/events/types.ts to make it globally available. Consider adding it to a category-specific map if appropriate (e.g., PluginEvents).
    export interface AllEvents extends Record<string, BaseEvent> {
      class="hl-cmt">// ... existing events
      &#39;myfeature:started&#39;: MyNewFeatureEvent;
      &#39;myfeature:completed&#39;: MyNewFeatureEvent;
    }

  1. Implement Emission: Use getGlobalEventBus().emit('myfeature:started', { featureId: '...' }) or this.emitTyped('myfeature:started', { featureId: '...' }) if using TypedEventEmitterAdapter.
  2. Add Type Guards (Optional): If your new event category will be frequently checked at runtime (e.g., in onAny listeners), consider adding a type guard function to src/events/type-guards.ts.
    export function isMyNewFeatureEvent(event: BaseEvent): event is MyNewFeatureEvent {
      return event.type.startsWith(&#39;myfeature:&#39;);
    }

  1. Update index.ts: Ensure your new types are exported from src/events/index.ts if they are intended for public consumption.

By following these guidelines, the event system remains consistent, type-safe, and easy to extend.