From 5e673ac993618697e352bf093d987574868699ca Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 7 Dec 2025 16:17:52 +0100 Subject: [PATCH] Add type-safe EventBus with generic event mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(); // 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 --- EVENTBUS_EXAMPLE.md | 120 ++++++++++++++++++++++++++++++++++++++++++++ src/event-bus.ts | 104 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 10 ++++ src/types.ts | 24 +++++++++ 4 files changed, 258 insertions(+) create mode 100644 EVENTBUS_EXAMPLE.md create mode 100644 src/event-bus.ts create mode 100644 src/types.ts diff --git a/EVENTBUS_EXAMPLE.md b/EVENTBUS_EXAMPLE.md new file mode 100644 index 0000000..612e1ea --- /dev/null +++ b/EVENTBUS_EXAMPLE.md @@ -0,0 +1,120 @@ +# EventBus Usage Examples + +## Type-Safe Event Bus + +The `EventBus` class provides fully type-safe event handling with TypeScript type inference. + +### Basic Usage + +```typescript +import { EventBus } from '@xtr-dev/rondevu-client'; + +// Define your event mapping +interface AppEvents { + 'user:connected': { userId: string; timestamp: number }; + 'user:disconnected': { userId: string }; + 'message:received': string; + 'connection:error': Error; +} + +// Create the event bus +const events = new EventBus(); + +// Subscribe to events - TypeScript knows the exact data type! +events.on('user:connected', (data) => { + // data is { userId: string; timestamp: number } + console.log(`User ${data.userId} connected at ${data.timestamp}`); +}); + +events.on('message:received', (data) => { + // data is string + console.log(data.toUpperCase()); +}); + +// Emit events - TypeScript validates the data type +events.emit('user:connected', { + userId: '123', + timestamp: Date.now() +}); + +events.emit('message:received', 'Hello World'); + +// Type errors caught at compile time: +// events.emit('user:connected', 'wrong type'); // ❌ Error! +// events.emit('message:received', { wrong: 'type' }); // ❌ Error! +``` + +### One-Time Listeners + +```typescript +// Subscribe once - handler auto-unsubscribes after first call +events.once('connection:error', (error) => { + console.error('Connection failed:', error.message); +}); +``` + +### Unsubscribing + +```typescript +const handler = (data: string) => { + console.log('Message:', data); +}; + +events.on('message:received', handler); + +// Later, unsubscribe +events.off('message:received', handler); +``` + +### Utility Methods + +```typescript +// Clear all handlers for a specific event +events.clear('message:received'); + +// Clear all handlers for all events +events.clear(); + +// Get listener count +const count = events.listenerCount('user:connected'); + +// Get all event names with handlers +const eventNames = events.eventNames(); +``` + +## Connection Events Example + +```typescript +interface ConnectionEvents { + 'connection:state': { state: 'connected' | 'disconnected' | 'connecting' }; + 'connection:message': { from: string; data: string | ArrayBuffer }; + 'connection:error': { code: string; message: string }; +} + +class ConnectionManager { + private events = new EventBus(); + + on( + event: K, + handler: (data: ConnectionEvents[K]) => void + ) { + this.events.on(event, handler); + } + + private handleStateChange(state: 'connected' | 'disconnected' | 'connecting') { + this.events.emit('connection:state', { state }); + } + + private handleMessage(from: string, data: string | ArrayBuffer) { + this.events.emit('connection:message', { from, data }); + } +} +``` + +## Benefits + +- ✅ **Full type safety** - TypeScript validates event names and data types +- ✅ **IntelliSense support** - Auto-completion for event names and data properties +- ✅ **Compile-time errors** - Catch type mismatches before runtime +- ✅ **Self-documenting** - Event interface serves as documentation +- ✅ **Refactoring-friendly** - Rename events or change types with confidence diff --git a/src/event-bus.ts b/src/event-bus.ts new file mode 100644 index 0000000..8d3d2b9 --- /dev/null +++ b/src/event-bus.ts @@ -0,0 +1,104 @@ +/** + * Type-safe EventBus with event name to payload type mapping + */ + +type EventHandler = (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(); + * + * // 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> { + private handlers: Map>; + + constructor() { + this.handlers = new Map(); + } + + /** + * Subscribe to an event + */ + on(event: K, handler: EventHandler): 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(event: K, handler: EventHandler): void { + const wrappedHandler = (data: TEvents[K]) => { + handler(data); + this.off(event, wrappedHandler); + }; + this.on(event, wrappedHandler); + } + + /** + * Unsubscribe from an event + */ + off(event: K, handler: EventHandler): 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(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(event?: K): void { + if (event !== undefined) { + this.handlers.delete(event); + } else { + this.handlers.clear(); + } + } + + /** + * Get count of handlers for an event + */ + listenerCount(event: K): number { + return this.handlers.get(event)?.size ?? 0; + } + + /** + * Get all event names that have handlers + */ + eventNames(): Array { + return Array.from(this.handlers.keys()); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7e7c368..c209972 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d8e06c7 --- /dev/null +++ b/src/types.ts @@ -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; \ No newline at end of file