mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-08 00:33:24 +00:00
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:
120
EVENTBUS_EXAMPLE.md
Normal file
120
EVENTBUS_EXAMPLE.md
Normal 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
104
src/event-bus.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
10
src/index.ts
10
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';
|
||||
|
||||
24
src/types.ts
Normal file
24
src/types.ts
Normal 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;
|
||||
Reference in New Issue
Block a user