/** * StateHistory - Manages state versioning and metadata * * @description Provides automatic versioning, rollback, replay, and diffing capabilities * for state management in the scripting harness. Each state change creates a new version * with metadata including timestamp and hash for change detection. * * Features: * - Automatic version tracking * - Configurable version limits with cleanup * - State diffing between versions * - Rollback and replay capabilities * - Memory-efficient storage with automatic cleanup */ class StateHistory { constructor(maxVersions = 100) { this.versions = new Map(); this.maxVersions = maxVersions; } /** * Add a new version to the history * * @param {number} version - Version number * @param {Object} inputState - Input state with metadata wrapper * @param {Object} outputModel - Output model (pure table data) */ addVersion(version, inputState, outputModel) { // Store version data this.versions.set(version, { version, timestamp: Date.now(), inputState, outputModel, hash: this.calculateHash(outputModel) }); // Clean up old versions if needed this.cleanupOldVersions(); } /** * Get state at specific version * * @param {number} version - Version number to retrieve * @returns {Object|null} State data or null if version not found */ getVersion(version) { const versionData = this.versions.get(version); return versionData ? versionData.outputModel : null; } /** * Get all versions with metadata * * @returns {Array} Array of version metadata objects */ getAllVersions() { return Array.from(this.versions.values()).map(v => ({ version: v.version, timestamp: v.timestamp, hash: v.hash })); } /** * Get diff between two versions * * @param {number} fromVersion - Starting version * @param {number} toVersion - Ending version * @returns {Object|null} Diff object or null if versions not found */ getDiff(fromVersion, toVersion) { const fromState = this.getVersion(fromVersion); const toState = this.getVersion(toVersion); if (!fromState || !toState) { return null; } return { added: this.findAddedProperties(fromState, toState), removed: this.findRemovedProperties(fromState, toState), changed: this.findChangedProperties(fromState, toState) }; } /** * Clean up old versions to prevent memory leaks */ cleanupOldVersions() { if (this.versions.size > this.maxVersions) { const sortedVersions = Array.from(this.versions.keys()).sort(); const toDelete = sortedVersions.slice(0, this.versions.size - this.maxVersions); for (const version of toDelete) { this.versions.delete(version); } } } /** * Calculate simple hash for change detection * * @param {Object} state - State object to hash * @returns {number} Hash value */ calculateHash(state) { // Simple hash for change detection if (state === undefined || state === null) { return 0; } return JSON.stringify(state).length; } /** * Find properties added in the new state * * @param {Object} fromState - Original state * @param {Object} toState - New state * @returns {Object} Object containing added properties */ findAddedProperties(fromState, toState) { const added = {}; for (const key in toState) { if (!(key in fromState)) { added[key] = toState[key]; } } return added; } /** * Find properties removed in the new state * * @param {Object} fromState - Original state * @param {Object} toState - New state * @returns {Object} Object containing removed properties */ findRemovedProperties(fromState, toState) { const removed = {}; for (const key in fromState) { if (!(key in toState)) { removed[key] = fromState[key]; } } return removed; } /** * Find properties changed in the new state * * @param {Object} fromState - Original state * @param {Object} toState - New state * @returns {Object} Object containing changed properties with from/to values */ findChangedProperties(fromState, toState) { const changed = {}; for (const key in toState) { if (key in fromState && fromState[key] !== toState[key]) { changed[key] = { from: fromState[key], to: toState[key] }; } } return changed; } } export { StateHistory };