- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
286 lines
8.8 KiB
JavaScript
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();
|
|
} |