/** * State Serializer for LiveComponents * * Provides type-safe state serialization/deserialization with versioning * and validation. */ /** * Serialize component state * * @param {LiveComponentState} state - Component state * @returns {string} Serialized state JSON */ export function serializeState(state) { // Validate state structure if (!state || typeof state !== 'object') { throw new Error('State must be an object'); } if (!state.id || typeof state.id !== 'string') { throw new Error('State must have a valid id'); } if (!state.component || typeof state.component !== 'string') { throw new Error('State must have a valid component name'); } if (typeof state.version !== 'number' || state.version < 1) { throw new Error('State must have a valid version number'); } // Ensure data is an object const data = state.data || {}; if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('State data must be an object'); } // Create serializable state const serializableState = { id: state.id, component: state.component, data: data, version: state.version }; // Serialize to JSON try { return JSON.stringify(serializableState); } catch (error) { throw new Error(`Failed to serialize state: ${error.message}`); } } /** * Deserialize component state * * @param {string} json - Serialized state JSON * @returns {LiveComponentState} Deserialized state */ export function deserializeState(json) { if (typeof json !== 'string') { throw new Error('State JSON must be a string'); } if (json.trim() === '') { // Return empty state return { id: '', component: '', data: {}, version: 1 }; } try { const parsed = JSON.parse(json); // Validate structure if (!parsed || typeof parsed !== 'object') { throw new Error('Invalid state structure'); } // Extract state data const id = parsed.id || ''; const component = parsed.component || ''; const data = parsed.data || {}; const version = typeof parsed.version === 'number' ? parsed.version : 1; // Validate data is an object if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('State data must be an object'); } return { id, component, data, version }; } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Invalid JSON format: ${error.message}`); } throw error; } } /** * Create state diff (changes between two states) * * @param {LiveComponentState} oldState - Previous state * @param {LiveComponentState} newState - New state * @returns {Object} State diff */ export function createStateDiff(oldState, newState) { const diff = { changed: false, added: {}, removed: {}, modified: {}, versionChange: newState.version - oldState.version }; const oldData = oldState.data || {}; const newData = newState.data || {}; // Find added keys for (const key in newData) { if (!(key in oldData)) { diff.added[key] = newData[key]; diff.changed = true; } } // Find removed keys for (const key in oldData) { if (!(key in newData)) { diff.removed[key] = oldData[key]; diff.changed = true; } } // Find modified keys for (const key in newData) { if (key in oldData) { const oldValue = oldData[key]; const newValue = newData[key]; // Deep comparison for objects if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { diff.modified[key] = { old: oldValue, new: newValue }; diff.changed = true; } } } return diff; } /** * Merge state changes * * @param {LiveComponentState} baseState - Base state * @param {Object} changes - State changes to apply * @returns {LiveComponentState} Merged state */ export function mergeStateChanges(baseState, changes) { const baseData = baseState.data || {}; const mergedData = { ...baseData, ...changes }; return { ...baseState, data: mergedData, version: baseState.version + 1 }; } /** * Validate state structure * * @param {LiveComponentState} state - State to validate * @returns {boolean} True if valid */ export function validateState(state) { if (!state || typeof state !== 'object') { return false; } if (!state.id || typeof state.id !== 'string') { return false; } if (!state.component || typeof state.component !== 'string') { return false; } if (typeof state.version !== 'number' || state.version < 1) { return false; } if (state.data && (typeof state.data !== 'object' || Array.isArray(state.data))) { return false; } return true; } /** * Get state from DOM element * * @param {HTMLElement} element - Component element * @returns {LiveComponentState|null} State or null if not found */ export function getStateFromElement(element) { const stateJson = element.dataset.state; if (!stateJson) { return null; } try { return deserializeState(stateJson); } catch (error) { console.error('[StateSerializer] Failed to deserialize state from element:', error); return null; } } /** * Set state on DOM element * * @param {HTMLElement} element - Component element * @param {LiveComponentState} state - State to set */ export function setStateOnElement(element, state) { if (!validateState(state)) { throw new Error('Invalid state structure'); } const stateJson = serializeState(state); element.dataset.state = stateJson; }