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>
215 lines
6.0 KiB
JavaScript
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; |