Class_generator/src/core/EventBus.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

215 lines
6.0 KiB
JavaScript

/**
* EventBus - Strict event-driven communication system
* Enforces type safety and prevents direct module coupling
*/
class EventBus {
constructor() {
// Private event storage
this._listeners = new Map();
this._moduleRegistry = new Map();
this._eventHistory = [];
this._maxHistorySize = 1000;
// Seal to prevent external modification
Object.seal(this);
}
/**
* Register a module with the event bus
* @param {Module} module - Module instance
*/
registerModule(module) {
if (!module || typeof module.name !== 'string') {
throw new Error('Invalid module: must have a name property');
}
if (this._moduleRegistry.has(module.name)) {
throw new Error(`Module ${module.name} is already registered`);
}
this._moduleRegistry.set(module.name, module);
}
/**
* Unregister a module and clean up its listeners
* @param {string} moduleName - Name of module to unregister
*/
unregisterModule(moduleName) {
if (!this._moduleRegistry.has(moduleName)) {
throw new Error(`Module ${moduleName} is not registered`);
}
// Remove all listeners for this module
for (const [eventType, listeners] of this._listeners) {
const filteredListeners = listeners.filter(listener => listener.module !== moduleName);
if (filteredListeners.length === 0) {
this._listeners.delete(eventType);
} else {
this._listeners.set(eventType, filteredListeners);
}
}
this._moduleRegistry.delete(moduleName);
}
/**
* Subscribe to an event type
* @param {string} eventType - Type of event to listen for
* @param {Function} callback - Function to call when event occurs
* @param {string} moduleName - Name of the subscribing module
*/
on(eventType, callback, moduleName) {
this._validateEventType(eventType);
this._validateCallback(callback);
this._validateModule(moduleName);
if (!this._listeners.has(eventType)) {
this._listeners.set(eventType, []);
}
const listener = {
callback,
module: moduleName,
id: this._generateId()
};
this._listeners.get(eventType).push(listener);
return listener.id; // Return ID for unsubscribing
}
/**
* Unsubscribe from an event
* @param {string} eventType - Event type to unsubscribe from
* @param {string} listenerId - ID returned from on() method
*/
off(eventType, listenerId) {
this._validateEventType(eventType);
if (!this._listeners.has(eventType)) {
return false;
}
const listeners = this._listeners.get(eventType);
const index = listeners.findIndex(listener => listener.id === listenerId);
if (index === -1) {
return false;
}
listeners.splice(index, 1);
if (listeners.length === 0) {
this._listeners.delete(eventType);
}
return true;
}
/**
* Emit an event to all subscribers
* @param {string} eventType - Type of event
* @param {*} data - Event data
* @param {string} sourceModule - Module emitting the event
*/
emit(eventType, data = null, sourceModule) {
this._validateEventType(eventType);
this._validateModule(sourceModule);
const event = {
type: eventType,
data,
source: sourceModule,
timestamp: Date.now(),
id: this._generateId()
};
// Add to history
this._addToHistory(event);
// Get listeners for this event type
const listeners = this._listeners.get(eventType) || [];
// Call all listeners (async to prevent blocking)
listeners.forEach(listener => {
try {
// Prevent modules from listening to their own events (optional)
if (listener.module !== sourceModule) {
setTimeout(() => listener.callback(event), 0);
}
} catch (error) {
console.error(`Error in event listener for ${eventType}:`, error);
}
});
return event.id;
}
/**
* Get event history (for debugging)
* @param {number} limit - Maximum number of events to return
*/
getEventHistory(limit = 50) {
return this._eventHistory.slice(-limit);
}
/**
* Get registered modules (for debugging)
*/
getRegisteredModules() {
return Array.from(this._moduleRegistry.keys());
}
/**
* Get active listeners (for debugging)
*/
getActiveListeners() {
const result = {};
for (const [eventType, listeners] of this._listeners) {
result[eventType] = listeners.map(l => l.module);
}
return result;
}
// Private validation methods
_validateEventType(eventType) {
if (!eventType || typeof eventType !== 'string') {
throw new Error('Event type must be a non-empty string');
}
}
_validateCallback(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
}
_validateModule(moduleName) {
if (!moduleName || typeof moduleName !== 'string') {
throw new Error('Module name must be a non-empty string');
}
if (!this._moduleRegistry.has(moduleName)) {
throw new Error(`Module ${moduleName} is not registered with EventBus`);
}
}
_generateId() {
return Math.random().toString(36).substr(2, 9);
}
_addToHistory(event) {
this._eventHistory.push(event);
// Limit history size
if (this._eventHistory.length > this._maxHistorySize) {
this._eventHistory.shift();
}
}
}
// Freeze to prevent modification
Object.freeze(EventBus);
Object.freeze(EventBus.prototype);
export default EventBus;