/** * Reactive State Management System für Module-Kommunikation * Ermöglicht type-safe state sharing zwischen Modulen */ import { Logger } from './logger.js'; /** * @typedef {Object} StateChangeEvent * @property {string} key - State key that changed * @property {*} value - New value * @property {*} oldValue - Previous value * @property {string} source - Module that triggered the change */ /** * @typedef {Object} StateSubscription * @property {string} id - Unique subscription ID * @property {Function} callback - Callback function * @property {string} subscriber - Module name that subscribed */ export class StateManager { constructor() { /** @type {Map} */ this.state = new Map(); /** @type {Map} */ this.subscribers = new Map(); /** @type {Map} */ this.stateOwners = new Map(); /** @type {Map} */ this.defaultValues = new Map(); /** @type {string} */ this.currentModule = 'unknown'; this.subscriptionCounter = 0; } /** * Set the current module context for state operations * @param {string} moduleName - Name of the module */ setContext(moduleName) { this.currentModule = moduleName; } /** * Register a state key with default value and owner * @param {string} key - State key * @param {*} defaultValue - Default value * @param {string} [owner] - Module that owns this state */ register(key, defaultValue, owner = this.currentModule) { if (this.state.has(key)) { Logger.warn(`[StateManager] State key '${key}' already registered by ${this.stateOwners.get(key)}`); return; } this.state.set(key, defaultValue); this.defaultValues.set(key, defaultValue); this.stateOwners.set(key, owner); this.subscribers.set(key, []); Logger.info(`[StateManager] Registered '${key}' (owner: ${owner})`); } /** * Get current value of state key * @param {string} key - State key * @returns {*} Current value or undefined */ get(key) { if (!this.state.has(key)) { Logger.warn(`[StateManager] Unknown state key: '${key}'`); return undefined; } return this.state.get(key); } /** * Set value for state key (with ownership check) * @param {string} key - State key * @param {*} value - New value * @param {boolean} [force=false] - Force set even if not owner * @returns {boolean} Success status */ set(key, value, force = false) { if (!this.state.has(key)) { Logger.warn(`[StateManager] Cannot set unknown state key: '${key}'`); return false; } const owner = this.stateOwners.get(key); if (!force && owner !== this.currentModule) { Logger.warn(`[StateManager] Module '${this.currentModule}' cannot modify '${key}' (owned by ${owner})`); return false; } const oldValue = this.state.get(key); if (oldValue === value) { return true; // No change } this.state.set(key, value); this.notifySubscribers(key, value, oldValue); Logger.info(`[StateManager] Updated '${key}' by ${this.currentModule}`); return true; } /** * Subscribe to state changes * @param {string} key - State key to watch * @param {Function} callback - Callback function (value, oldValue, key) => void * @param {string} [subscriber] - Subscriber module name * @returns {string} Subscription ID for unsubscribing */ subscribe(key, callback, subscriber = this.currentModule) { if (!this.state.has(key)) { Logger.warn(`[StateManager] Cannot subscribe to unknown state key: '${key}'`); return null; } const subscriptionId = `${subscriber}_${++this.subscriptionCounter}`; const subscription = { id: subscriptionId, callback, subscriber }; if (!this.subscribers.has(key)) { this.subscribers.set(key, []); } this.subscribers.get(key).push(subscription); Logger.info(`[StateManager] Subscribed '${subscriber}' to '${key}'`); return subscriptionId; } /** * Unsubscribe from state changes * @param {string} subscriptionId - Subscription ID from subscribe() */ unsubscribe(subscriptionId) { for (const [key, subscriptions] of this.subscribers.entries()) { const index = subscriptions.findIndex(sub => sub.id === subscriptionId); if (index !== -1) { const subscription = subscriptions[index]; subscriptions.splice(index, 1); Logger.info(`[StateManager] Unsubscribed '${subscription.subscriber}' from '${key}'`); return; } } Logger.warn(`[StateManager] Subscription ID not found: ${subscriptionId}`); } /** * Notify all subscribers of a state change * @private * @param {string} key - State key * @param {*} value - New value * @param {*} oldValue - Previous value */ notifySubscribers(key, value, oldValue) { const subscriptions = this.subscribers.get(key) || []; subscriptions.forEach(subscription => { try { subscription.callback(value, oldValue, key); } catch (error) { Logger.error(`[StateManager] Error in subscriber '${subscription.subscriber}' for '${key}':`, error); } }); } /** * Reset state key to default value * @param {string} key - State key to reset * @returns {boolean} Success status */ reset(key) { if (!this.state.has(key)) { Logger.warn(`[StateManager] Cannot reset unknown state key: '${key}'`); return false; } const defaultValue = this.defaultValues.get(key); return this.set(key, defaultValue, true); } /** * Clear all subscriptions for a module (useful for cleanup) * @param {string} moduleName - Module name */ clearModuleSubscriptions(moduleName) { let cleared = 0; for (const [key, subscriptions] of this.subscribers.entries()) { const filtered = subscriptions.filter(sub => sub.subscriber !== moduleName); cleared += subscriptions.length - filtered.length; this.subscribers.set(key, filtered); } if (cleared > 0) { Logger.info(`[StateManager] Cleared ${cleared} subscriptions for module '${moduleName}'`); } } /** * Get current state snapshot (for debugging) * @returns {Object} Current state and metadata */ getSnapshot() { const snapshot = { state: Object.fromEntries(this.state), owners: Object.fromEntries(this.stateOwners), subscriptions: {} }; for (const [key, subs] of this.subscribers.entries()) { snapshot.subscriptions[key] = subs.map(sub => ({ id: sub.id, subscriber: sub.subscriber })); } return snapshot; } /** * Reset entire state manager (useful for testing) */ resetAll() { this.state.clear(); this.subscribers.clear(); this.stateOwners.clear(); this.defaultValues.clear(); this.subscriptionCounter = 0; Logger.info('[StateManager] Reset complete'); } /** * Create a scoped state manager for a specific module * @param {string} moduleName - Module name * @returns {Object} Scoped state interface */ createScope(moduleName) { return { register: (key, defaultValue) => { this.setContext(moduleName); return this.register(key, defaultValue, moduleName); }, get: (key) => this.get(key), set: (key, value) => { this.setContext(moduleName); return this.set(key, value); }, subscribe: (key, callback) => { this.setContext(moduleName); return this.subscribe(key, callback, moduleName); }, unsubscribe: (subscriptionId) => this.unsubscribe(subscriptionId), reset: (key) => this.reset(key), cleanup: () => this.clearModuleSubscriptions(moduleName) }; } } // Global instance export const stateManager = new StateManager(); // Debug access if (typeof window !== 'undefined') { window.stateManager = stateManager; window.stateSnapshot = () => stateManager.getSnapshot(); }