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

120
EVENTBUS_EXAMPLE.md Normal file
View File

@@ -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<AppEvents>();
// 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<ConnectionEvents>();
on<K extends keyof ConnectionEvents>(
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

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;