Spaces:
Running
Running
| /** | |
| * StateManager - Singleton pattern for global application state management | |
| * Manages AppState, provides getter/setter methods, and subscription system | |
| * Requirements: 2.6, 11.4 | |
| */ | |
| const StateManager = (function () { | |
| // βββ Private State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** @type {AppState} */ | |
| const _state = { | |
| // Models | |
| currentModel: null, | |
| modelList: [], | |
| // UI State | |
| selectedNodeId: null, | |
| zoomLevel: 1, | |
| viewMode: 'single', // 'single' | 'comparison' | |
| comparisonModel: null, | |
| // Search/Filter | |
| searchQuery: '', | |
| filteredModelList: [], | |
| // Error State | |
| error: null, | |
| // Loading State | |
| isLoading: false, | |
| loadingProgress: 0, | |
| }; | |
| /** @type {Map<string, Set<Function>>} */ | |
| const _subscribers = new Map(); | |
| // βββ Private Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Notify all subscribers for a given key. | |
| * @param {string} key | |
| * @param {*} newValue | |
| * @param {*} oldValue | |
| */ | |
| function _notify(key, newValue, oldValue) { | |
| if (_subscribers.has(key)) { | |
| _subscribers.get(key).forEach((cb) => { | |
| try { | |
| cb(newValue, oldValue, key); | |
| } catch (err) { | |
| console.error(`[StateManager] Subscriber error for key "${key}":`, err); | |
| } | |
| }); | |
| } | |
| // Also notify wildcard '*' subscribers | |
| if (_subscribers.has('*')) { | |
| _subscribers.get('*').forEach((cb) => { | |
| try { | |
| cb(newValue, oldValue, key); | |
| } catch (err) { | |
| console.error('[StateManager] Wildcard subscriber error:', err); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Generic setter that updates state and notifies subscribers. | |
| * @param {string} key | |
| * @param {*} value | |
| */ | |
| function _set(key, value) { | |
| if (!(key in _state)) { | |
| console.warn(`[StateManager] Unknown state key: "${key}"`); | |
| } | |
| const old = _state[key]; | |
| _state[key] = value; | |
| _notify(key, value, old); | |
| } | |
| // βββ Persistence Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function _persistSelectedModel(modelId) { | |
| try { | |
| if (modelId !== null && modelId !== undefined) { | |
| localStorage.setItem(CONFIG.STORAGE.SELECTED_MODEL, modelId); | |
| } else { | |
| localStorage.removeItem(CONFIG.STORAGE.SELECTED_MODEL); | |
| } | |
| } catch (err) { | |
| console.warn('[StateManager] localStorage write failed:', err); | |
| } | |
| } | |
| function _loadPersistedSelectedModel() { | |
| try { | |
| return localStorage.getItem(CONFIG.STORAGE.SELECTED_MODEL) || null; | |
| } catch (err) { | |
| console.warn('[StateManager] localStorage read failed:', err); | |
| return null; | |
| } | |
| } | |
| // βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| return { | |
| // ββ Getters ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** @returns {AppState} Shallow copy of the current state */ | |
| getState() { | |
| return Object.assign({}, _state); | |
| }, | |
| /** @returns {ParsedModel|null} */ | |
| getCurrentModel() { | |
| return _state.currentModel; | |
| }, | |
| /** @returns {Array} */ | |
| getModelList() { | |
| return _state.modelList; | |
| }, | |
| /** @returns {string|null} */ | |
| getSelectedNodeId() { | |
| return _state.selectedNodeId; | |
| }, | |
| /** @returns {number} */ | |
| getZoomLevel() { | |
| return _state.zoomLevel; | |
| }, | |
| /** @returns {'single'|'comparison'} */ | |
| getViewMode() { | |
| return _state.viewMode; | |
| }, | |
| /** @returns {ParsedModel|null} */ | |
| getComparisonModel() { | |
| return _state.comparisonModel; | |
| }, | |
| /** @returns {string} */ | |
| getSearchQuery() { | |
| return _state.searchQuery; | |
| }, | |
| /** @returns {Array} */ | |
| getFilteredModelList() { | |
| return _state.filteredModelList; | |
| }, | |
| /** @returns {{message:string, type:string, timestamp:number}|null} */ | |
| getError() { | |
| return _state.error; | |
| }, | |
| /** @returns {boolean} */ | |
| isLoading() { | |
| return _state.isLoading; | |
| }, | |
| /** @returns {number} */ | |
| getLoadingProgress() { | |
| return _state.loadingProgress; | |
| }, | |
| // ββ Setters ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Set the current model and persist its id to localStorage. | |
| * @param {ParsedModel|null} model | |
| */ | |
| setCurrentModel(model) { | |
| _set('currentModel', model); | |
| const modelId = model && model.metadata ? model.metadata.fileName : null; | |
| _persistSelectedModel(modelId); | |
| }, | |
| /** @param {Array} list */ | |
| setModelList(list) { | |
| _set('modelList', list); | |
| }, | |
| /** @param {string|null} nodeId */ | |
| setSelectedNodeId(nodeId) { | |
| _set('selectedNodeId', nodeId); | |
| }, | |
| /** @param {number} level */ | |
| setZoomLevel(level) { | |
| _set('zoomLevel', level); | |
| }, | |
| /** @param {'single'|'comparison'} mode */ | |
| setViewMode(mode) { | |
| _set('viewMode', mode); | |
| }, | |
| /** @param {ParsedModel|null} model */ | |
| setComparisonModel(model) { | |
| _set('comparisonModel', model); | |
| }, | |
| /** @param {string} query */ | |
| setSearchQuery(query) { | |
| _set('searchQuery', query); | |
| }, | |
| /** @param {Array} list */ | |
| setFilteredModelList(list) { | |
| _set('filteredModelList', list); | |
| }, | |
| /** | |
| * Set an error in state. | |
| * @param {string} message | |
| * @param {'error'|'warning'|'info'} [type='error'] | |
| */ | |
| setError(message, type = 'error') { | |
| _set('error', { message, type, timestamp: Date.now() }); | |
| }, | |
| /** Clear the current error */ | |
| clearError() { | |
| _set('error', null); | |
| }, | |
| /** @param {boolean} loading */ | |
| setLoading(loading) { | |
| _set('isLoading', loading); | |
| }, | |
| /** @param {number} progress 0-100 */ | |
| setLoadingProgress(progress) { | |
| _set('loadingProgress', progress); | |
| }, | |
| // ββ Subscription System βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Subscribe to changes on a specific state key (or '*' for all changes). | |
| * @param {string} key - State key to watch, or '*' for all changes | |
| * @param {Function} callback - Called with (newValue, oldValue, key) | |
| * @returns {Function} Unsubscribe function | |
| */ | |
| subscribe(key, callback) { | |
| if (typeof callback !== 'function') { | |
| throw new TypeError('[StateManager] subscribe() requires a function callback'); | |
| } | |
| if (!_subscribers.has(key)) { | |
| _subscribers.set(key, new Set()); | |
| } | |
| _subscribers.get(key).add(callback); | |
| // Return unsubscribe function | |
| return function unsubscribe() { | |
| const set = _subscribers.get(key); | |
| if (set) { | |
| set.delete(callback); | |
| if (set.size === 0) { | |
| _subscribers.delete(key); | |
| } | |
| } | |
| }; | |
| }, | |
| /** | |
| * Unsubscribe a specific callback from a key. | |
| * @param {string} key | |
| * @param {Function} callback | |
| */ | |
| unsubscribe(key, callback) { | |
| const set = _subscribers.get(key); | |
| if (set) { | |
| set.delete(callback); | |
| if (set.size === 0) { | |
| _subscribers.delete(key); | |
| } | |
| } | |
| }, | |
| // ββ Persistence βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Restore persisted selected model id from localStorage. | |
| * @returns {string|null} The persisted model id, or null | |
| */ | |
| getPersistedSelectedModelId() { | |
| return _loadPersistedSelectedModel(); | |
| }, | |
| // ββ Reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Reset all state to initial values (useful for testing). | |
| */ | |
| reset() { | |
| const keys = Object.keys(_state); | |
| const initial = { | |
| currentModel: null, | |
| modelList: [], | |
| selectedNodeId: null, | |
| zoomLevel: 1, | |
| viewMode: 'single', | |
| comparisonModel: null, | |
| searchQuery: '', | |
| filteredModelList: [], | |
| error: null, | |
| isLoading: false, | |
| loadingProgress: 0, | |
| }; | |
| keys.forEach((key) => _set(key, initial[key])); | |
| }, | |
| }; | |
| })(); | |
| // Export for global access in vanilla JS context | |
| window.StateManager = StateManager; | |