model-explorer / js /state /stateManager.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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;