Files
michaelschiemer/resources/js/core/StateManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

286 lines
8.8 KiB
JavaScript

/**
* 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<string, any>} */
this.state = new Map();
/** @type {Map<string, StateSubscription[]>} */
this.subscribers = new Map();
/** @type {Map<string, string>} */
this.stateOwners = new Map();
/** @type {Map<string, any>} */
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();
}