Add type-safe EventBus with generic event mapping

Implemented EventBus class with full TypeScript type inference:
- Generic type parameter TEvents for event name to payload mapping
- Type-safe on/once/off/emit methods with inferred data types
- Utility methods: clear, listenerCount, eventNames
- Complete JSDoc documentation with usage examples

Added core connection types:
- ConnectionIdentity, ConnectionState, ConnectionInterface
- QueueMessageOptions for message queuing
- Connection composite type

All types and classes exported from main index.

Example usage:
```typescript
interface MyEvents {
  'user:connected': { userId: string; timestamp: number };
  'message:received': string;
}

const bus = new EventBus<MyEvents>();

// TypeScript knows data is { userId: string; timestamp: number }
bus.on('user:connected', (data) => {
  console.log(data.userId, data.timestamp);
});
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-07 16:17:52 +01:00
parent 511bac8033
commit 5e673ac993
4 changed files with 258 additions and 0 deletions

104
src/event-bus.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* Type-safe EventBus with event name to payload type mapping
*/
type EventHandler<T = any> = (data: T) => void;
/**
* EventBus - Type-safe event emitter with inferred event data types
*
* @example
* interface MyEvents {
* 'user:connected': { userId: string; timestamp: number };
* 'user:disconnected': { userId: string };
* 'message:received': string;
* }
*
* const bus = new EventBus<MyEvents>();
*
* // TypeScript knows data is { userId: string; timestamp: number }
* bus.on('user:connected', (data) => {
* console.log(data.userId, data.timestamp);
* });
*
* // TypeScript knows data is string
* bus.on('message:received', (data) => {
* console.log(data.toUpperCase());
* });
*/
export class EventBus<TEvents extends Record<string, any>> {
private handlers: Map<keyof TEvents, Set<EventHandler>>;
constructor() {
this.handlers = new Map();
}
/**
* Subscribe to an event
*/
on<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
/**
* Subscribe to an event once (auto-unsubscribe after first call)
*/
once<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {
const wrappedHandler = (data: TEvents[K]) => {
handler(data);
this.off(event, wrappedHandler);
};
this.on(event, wrappedHandler);
}
/**
* Unsubscribe from an event
*/
off<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.delete(handler);
if (eventHandlers.size === 0) {
this.handlers.delete(event);
}
}
}
/**
* Emit an event with data
*/
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.forEach(handler => handler(data));
}
}
/**
* Remove all handlers for a specific event, or all handlers if no event specified
*/
clear<K extends keyof TEvents>(event?: K): void {
if (event !== undefined) {
this.handlers.delete(event);
} else {
this.handlers.clear();
}
}
/**
* Get count of handlers for an event
*/
listenerCount<K extends keyof TEvents>(event: K): number {
return this.handlers.get(event)?.size ?? 0;
}
/**
* Get all event names that have handlers
*/
eventNames(): Array<keyof TEvents> {
return Array.from(this.handlers.keys());
}
}

View File

@@ -4,3 +4,13 @@
*/
export { ConnectionManager } from './connection-manager.js';
export { EventBus } from './event-bus.js';
// Export types
export type {
ConnectionIdentity,
ConnectionState,
ConnectionInterface,
Connection,
QueueMessageOptions
} from './types.js';

24
src/types.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* Core connection types
*/
export interface ConnectionIdentity {
id: string;
hostUsername: string;
}
export interface ConnectionState {
state: 'connected' | 'disconnected' | 'connecting';
lastActive: number;
}
export interface QueueMessageOptions {
expiresAt?: number;
}
export interface ConnectionInterface {
queueMessage(message: string | ArrayBuffer, options?: QueueMessageOptions): void;
sendMessage(message: string | ArrayBuffer): void;
}
export type Connection = ConnectionIdentity & ConnectionState & ConnectionInterface;