UNPKG

qnce-engine

Version:

Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization

1,300 lines (1,299 loc) 52.7 kB
"use strict"; // QNCE Core Engine - Framework Agnostic // Quantum Narrative Convergence Engine Object.defineProperty(exports, "__esModule", { value: true }); exports.QNCEEngine = void 0; exports.createQNCEEngine = createQNCEEngine; exports.loadStoryData = loadStoryData; const ObjectPool_1 = require("../performance/ObjectPool"); const ThreadPool_1 = require("../performance/ThreadPool"); const PerfReporter_1 = require("../performance/PerfReporter"); // Branching system imports const branching_1 = require("../narrative/branching"); // State persistence imports - Sprint 3.3 const types_1 = require("./types"); // Conditional choice evaluator - Sprint 3.4 const condition_1 = require("./condition"); // Demo narrative data moved to demo-story.ts function findNode(nodes, id) { const node = nodes.find(n => n.id === id); if (!node) throw new Error(`Node not found: ${id}`); return node; } /** * QNCE Engine - Core narrative state management * Framework agnostic implementation with object pooling integration */ class QNCEEngine { state; storyData; activeFlowEvents = []; performanceMode = false; enableProfiling = false; branchingEngine; // Sprint 3.3: State persistence and checkpoints checkpoints = new Map(); maxCheckpoints = 50; autoCheckpointEnabled = false; // Sprint 3.3: State persistence properties checkpointManager; // Sprint 3.5: Autosave and Undo/Redo properties undoStack = []; redoStack = []; autosaveConfig = { enabled: false, triggers: ['choice', 'flag-change'], maxEntries: 10, throttleMs: 1000, includeMetadata: true }; undoRedoConfig = { enabled: true, maxUndoEntries: 50, maxRedoEntries: 20, trackFlagChanges: true, trackChoiceText: true, trackActions: ['choice', 'flag-change', 'state-load'] }; lastAutosaveTime = 0; isUndoRedoOperation = false; constructor(storyData, initialState, performanceMode = false, threadPoolConfig) { this.storyData = storyData; this.performanceMode = performanceMode; this.enableProfiling = performanceMode; // Enable profiling with performance mode // Initialize ThreadPool if in performance mode if (this.performanceMode && threadPoolConfig) { (0, ThreadPool_1.getThreadPool)(threadPoolConfig); } this.state = { currentNodeId: initialState?.currentNodeId || storyData.initialNodeId, flags: initialState?.flags || {}, history: initialState?.history || [storyData.initialNodeId], }; } getCurrentNode() { const cacheKey = `node-${this.state.currentNodeId}`; // Profiling: Record cache operation if (this.enableProfiling) { const found = findNode(this.storyData.nodes, this.state.currentNodeId); PerfReporter_1.perf.cacheHit(cacheKey, { nodeId: this.state.currentNodeId }); return found; } if (this.performanceMode) { // Use pooled node for enhanced node data const pooledNode = ObjectPool_1.poolManager.borrowNode(); const coreNode = findNode(this.storyData.nodes, this.state.currentNodeId); pooledNode.initialize(coreNode.id, coreNode.text, coreNode.choices); // Return the pooled node (caller should return it when done) return { id: pooledNode.id, text: pooledNode.text, choices: pooledNode.choices }; } return findNode(this.storyData.nodes, this.state.currentNodeId); } getState() { return { ...this.state }; } getFlags() { return { ...this.state.flags }; } setFlag(key, value) { // Sprint 3.5: Save state for undo before making changes const preChangeState = this.deepCopy(this.state); this.state.flags[key] = value; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('flag-change')) { this.pushToUndoStack(preChangeState, 'flag-change', { flagsChanged: [key], flagValue: value }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('flag-change')) { this.triggerAutosave('flag-change', { flagKey: key, flagValue: value }).catch((error) => { console.warn('[QNCE] Autosave failed:', error.message); }); } } getHistory() { return [...this.state.history]; } selectChoice(choice) { // Sprint 3.5: Save state for undo before making changes const preChangeState = this.deepCopy(this.state); // S2-T4: Add state transition profiling const transitionSpanId = this.enableProfiling ? (0, PerfReporter_1.getPerfReporter)().startSpan('state-transition', { fromNodeId: this.state.currentNodeId, toNodeId: choice.nextNodeId, hasEffects: !!choice.flagEffects }) : null; const fromNodeId = this.state.currentNodeId; const toNodeId = choice.nextNodeId; // Create flow event for tracking narrative progression if (this.performanceMode) { const flowSpanId = this.enableProfiling ? PerfReporter_1.perf.flowStart(fromNodeId, { toNodeId }) : null; const flowEvent = this.createFlowEvent(fromNodeId, toNodeId, choice.flagEffects); this.recordFlowEvent(flowEvent); // Return the flow immediately after recording (we don't need to keep it) // This ensures proper pool recycling ObjectPool_1.poolManager.returnFlow(flowEvent); if (flowSpanId) { PerfReporter_1.perf.flowComplete(flowSpanId, toNodeId, { transitionType: 'choice' }); } } this.state.currentNodeId = choice.nextNodeId; this.state.history.push(choice.nextNodeId); if (choice.flagEffects) { this.state.flags = { ...this.state.flags, ...choice.flagEffects }; // S2-T4: Track flag updates if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { flagCount: Object.keys(choice.flagEffects).length, nodeId: toNodeId, eventType: 'flag-update' }); } } // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('choice')) { this.pushToUndoStack(preChangeState, 'choice', { nodeId: fromNodeId, choiceText: this.undoRedoConfig.trackChoiceText ? choice.text : undefined, flagsChanged: choice.flagEffects ? Object.keys(choice.flagEffects) : undefined }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('choice')) { this.triggerAutosave('choice', { fromNodeId, toNodeId, choiceText: choice.text }).catch(error => { console.warn('[QNCE] Autosave failed:', error.message); }); } // Complete state transition span if (transitionSpanId) { (0, PerfReporter_1.getPerfReporter)().endSpan(transitionSpanId, { historyLength: this.state.history.length, flagCount: Object.keys(this.state.flags).length }); } // S2-T2: Background operations after state change if (this.performanceMode) { // Preload next possible nodes in background this.preloadNextNodes().catch(error => { console.warn('[QNCE] Background preload failed:', error.message); }); // Write telemetry data in background this.backgroundTelemetryWrite({ action: 'choice-selected', fromNodeId, toNodeId, choiceText: choice.text.slice(0, 50) // First 50 chars for privacy }).catch(error => { console.warn('[QNCE] Background telemetry failed:', error.message); }); } } resetNarrative() { // Sprint 3.5: Save state for undo before reset const preChangeState = this.deepCopy(this.state); // Clean up pooled objects before reset if (this.performanceMode) { this.cleanupPools(); } this.state.currentNodeId = this.storyData.initialNodeId; this.state.flags = {}; this.state.history = [this.storyData.initialNodeId]; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('reset')) { this.pushToUndoStack(preChangeState, 'reset', { nodeId: this.storyData.initialNodeId }); } } /** * Load simple state (legacy method for backward compatibility) * @param state - QNCEState to load */ loadSimpleState(state) { // Sprint 3.5: Save state for undo before loading const preChangeState = this.deepCopy(this.state); this.state = { ...state }; // Sprint 3.5: Track state change for undo/redo if (this.undoRedoConfig.enabled && !this.isUndoRedoOperation && this.undoRedoConfig.trackActions.includes('state-load')) { this.pushToUndoStack(preChangeState, 'state-load', { nodeId: state.currentNodeId }); } // Sprint 3.5: Trigger autosave if enabled if (this.autosaveConfig.enabled && this.autosaveConfig.triggers.includes('state-load')) { this.triggerAutosave('state-load', { nodeId: state.currentNodeId }).catch((error) => { console.warn('[QNCE] Autosave failed:', error.message); }); } } // Sprint 3.3: State Persistence & Checkpoints Implementation /** * Save current engine state to a serialized format * @param options - Serialization options * @returns Promise resolving to serialized state */ async saveState(options = {}) { const startTime = performance.now(); if (!this.state.currentNodeId) { throw new Error('Invalid state: currentNodeId is missing.'); } try { // Create serialization metadata const metadata = { engineVersion: types_1.PERSISTENCE_VERSION, timestamp: new Date().toISOString(), storyId: this.generateStoryHash(), customMetadata: options.customMetadata || {}, compression: options.compression || 'none' }; // Build serialized state const serializedState = { state: this.deepCopy(this.state), flowEvents: options.includeFlowEvents !== false ? this.deepCopy(this.activeFlowEvents) : [], metadata }; // Add optional data based on options if (options.includePerformanceData && this.performanceMode) { serializedState.performanceState = { performanceMode: this.performanceMode, enableProfiling: this.enableProfiling, backgroundTasks: [], // Would be populated with actual background task IDs telemetryData: [] // Would be populated with telemetry history }; serializedState.poolStats = this.getPoolStats() || {}; } if (options.includeBranchingContext && this.branchingEngine) { serializedState.branchingContext = { activeBranches: [], // Would be populated from branching engine branchStates: {}, convergencePoints: [] }; } if (options.includeValidationState) { serializedState.validationState = { disabledRules: [], customRules: {}, validationErrors: [] }; } // Generate checksum if requested if (options.generateChecksum) { const stateToHash = { ...serializedState }; delete stateToHash.metadata.checksum; // Exclude checksum from hash const stateString = JSON.stringify(stateToHash); metadata.checksum = await this.generateChecksum(stateString); serializedState.metadata = metadata; } // Performance tracking if (this.enableProfiling) { const duration = performance.now() - startTime; PerfReporter_1.perf.record('custom', { eventType: 'state-serialized', duration, stateSize: JSON.stringify(serializedState).length, includePerformance: !!options.includePerformanceData, includeFlowEvents: options.includeFlowEvents !== false }); } return serializedState; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown serialization error'; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-serialization-failed', error: errorMessage, duration: performance.now() - startTime }); } throw new Error(`Failed to save state: ${errorMessage}`); } } /** * Load engine state from serialized format * @param serializedState - The serialized state to load * @param options - Load options * @returns Promise resolving to persistence result */ async loadState(serializedState, options = {}) { const startTime = performance.now(); try { // Validate serialized state structure const validationResult = this.validateSerializedState(serializedState); if (!validationResult.isValid) { return { success: false, error: `Invalid serialized state: ${validationResult.errors.join(', ')}`, warnings: validationResult.warnings }; } // Check compatibility if (!options.skipCompatibilityCheck) { const compatibilityCheck = this.checkCompatibility(serializedState.metadata); if (!compatibilityCheck.compatible) { return { success: false, error: `Incompatible state version: ${compatibilityCheck.reason}`, warnings: compatibilityCheck.suggestions }; } } // Verify checksum if available and requested if (options.verifyChecksum && serializedState.metadata.checksum) { const isValid = await this.verifyChecksum(serializedState); if (!isValid) { return { success: false, error: 'Checksum verification failed - state may be corrupted' }; } } // Story hash validation if (this.generateStoryHash() !== serializedState.metadata.storyId) { return { success: false, error: 'Story hash mismatch. The state belongs to a different narrative.' }; } // Apply migration if needed let stateToLoad = serializedState; if (options.migrationFunction) { stateToLoad = options.migrationFunction(serializedState); } // Load core state this.state = this.deepCopy(stateToLoad.state); // Restore optional data based on options if (options.restoreFlowEvents && stateToLoad.flowEvents) { this.activeFlowEvents = this.deepCopy(stateToLoad.flowEvents); } if (options.restorePerformanceState && stateToLoad.performanceState) { this.performanceMode = stateToLoad.performanceState.performanceMode; this.enableProfiling = stateToLoad.performanceState.enableProfiling; // Background tasks would be restored here } if (options.restoreBranchingContext && stateToLoad.branchingContext && this.branchingEngine) { // Restore branching context - would be implemented with actual branching engine } // Performance tracking const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-loaded', duration, stateSize: JSON.stringify(stateToLoad).length, restoredFlowEvents: !!options.restoreFlowEvents, restoredPerformance: !!options.restorePerformanceState }); } return { success: true, data: { size: JSON.stringify(stateToLoad).length, duration, checksum: stateToLoad.metadata.checksum } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown loading error'; const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'state-loading-failed', error: errorMessage, duration }); } return { success: false, error: `Failed to load state: ${errorMessage}`, data: { duration } }; } } /** * Create a lightweight checkpoint of current state * @param name - Optional checkpoint name * @param options - Checkpoint options * @returns Promise resolving to created checkpoint */ async createCheckpoint(name, options = {}) { const checkpointId = this.generateCheckpointId(); const timestamp = new Date().toISOString(); const checkpoint = { id: checkpointId, name: name || `Checkpoint ${checkpointId.slice(-8)}`, state: this.deepCopy(this.state), timestamp, description: options.includeMetadata ? `Auto-checkpoint at node ${this.state.currentNodeId}` : undefined, tags: options.autoTags || [], metadata: options.includeMetadata ? { nodeTitle: this.getCurrentNode().text.slice(0, 50), choiceCount: this.getCurrentNode().choices.length, flagCount: Object.keys(this.state.flags).length, historyLength: this.state.history.length } : undefined }; // Store checkpoint this.checkpoints.set(checkpointId, checkpoint); // Cleanup old checkpoints if needed const maxCheckpoints = options.maxCheckpoints || this.maxCheckpoints; if (this.checkpoints.size > maxCheckpoints) { await this.cleanupCheckpoints(options.cleanupStrategy || 'lru', maxCheckpoints); } // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-created', checkpointId, checkpointCount: this.checkpoints.size, stateSize: JSON.stringify(checkpoint.state).length }); } return checkpoint; } /** * Restore engine state from a checkpoint * @param checkpointId - ID of checkpoint to restore * @returns Promise resolving to persistence result */ async restoreFromCheckpoint(checkpointId) { const startTime = performance.now(); try { const checkpoint = this.checkpoints.get(checkpointId); if (!checkpoint) { return { success: false, error: `Checkpoint not found: ${checkpointId}` }; } // Restore state this.state = this.deepCopy(checkpoint.state); const duration = performance.now() - startTime; // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-restored', checkpointId, duration, stateSize: JSON.stringify(checkpoint.state).length }); } return { success: true, data: { size: JSON.stringify(checkpoint.state).length, duration, checksum: checkpointId } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown restore error'; const duration = performance.now() - startTime; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoint-restore-failed', checkpointId, error: errorMessage, duration }); } return { success: false, error: `Failed to restore checkpoint: ${errorMessage}`, data: { duration } }; } } // Utility method for checking flag conditions checkFlag(flagName, expectedValue) { if (expectedValue === undefined) { return this.state.flags[flagName] !== undefined; } return this.state.flags[flagName] === expectedValue; } // Get available choices (with conditional filtering) getAvailableChoices() { const currentNode = this.getCurrentNode(); const context = { state: this.state, timestamp: Date.now(), customData: {} }; return currentNode.choices.filter((choice) => { // If no condition is specified, choice is always available if (!choice.condition) { return true; } try { // Evaluate the condition using the condition evaluator return condition_1.conditionEvaluator.evaluate(choice.condition, context); } catch (error) { // Log condition evaluation errors but don't block other choices if (error instanceof condition_1.ConditionEvaluationError) { console.warn(`[QNCE] Choice condition evaluation failed: ${error.message}`, { choiceText: choice.text, condition: choice.condition, nodeId: this.state.currentNodeId }); } else { console.warn(`[QNCE] Unexpected error evaluating choice condition:`, error); } // Return false for invalid conditions (choice won't be shown) return false; } }); } // Performance and object pooling methods /** * Create a flow event using pooled objects for performance tracking */ createFlowEvent(fromNodeId, toNodeId, metadata) { const flow = ObjectPool_1.poolManager.borrowFlow(); flow.initialize(fromNodeId, metadata); flow.addTransition(fromNodeId, toNodeId); return flow; } /** * Record and manage flow events */ recordFlowEvent(flow) { const flowEvent = { id: `${flow.nodeId}-${Date.now()}`, fromNodeId: flow.nodeId, toNodeId: flow.transitions[flow.transitions.length - 1]?.split('->')[1] || '', timestamp: flow.timestamp, metadata: flow.metadata }; this.activeFlowEvents.push(flowEvent); // Clean up old flow events (basic LRU-style cleanup) if (this.activeFlowEvents.length > 10) { this.activeFlowEvents.shift(); // Remove oldest } } /** * Get current flow events (for debugging/monitoring) */ getActiveFlows() { return [...this.activeFlowEvents]; } /** * Get object pool statistics for performance monitoring */ getPoolStats() { return this.performanceMode ? ObjectPool_1.poolManager.getAllStats() : null; } /** * Return all pooled objects (cleanup method) */ cleanupPools() { // Clear flow events (no pooled objects to return since we return them immediately) this.activeFlowEvents.length = 0; } // S2-T2: Background ThreadPool Operations /** * Preload next possible nodes in background using ThreadPool */ async preloadNextNodes(choice) { if (!this.performanceMode) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); const currentNode = this.getCurrentNode(); const choicesToPreload = choice ? [choice] : currentNode.choices; // Submit background jobs for each node to preload for (const ch of choicesToPreload) { threadPool.submitJob('cache-load', { nodeId: ch.nextNodeId }, 'normal').catch(error => { if (this.enableProfiling) { PerfReporter_1.perf.record('cache-miss', { nodeId: ch.nextNodeId, error: error.message, jobType: 'preload' }); } }); } } /** * Write telemetry data in background using ThreadPool */ async backgroundTelemetryWrite(eventData) { if (!this.performanceMode || !this.enableProfiling) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); const telemetryData = { timestamp: performance.now(), sessionId: this.state.history[0], // Use first node as session ID eventData, stateSnapshot: { nodeId: this.state.currentNodeId, flagCount: Object.keys(this.state.flags).length, historyLength: this.state.history.length } }; threadPool.submitJob('telemetry-write', telemetryData, 'low').catch(error => { console.warn('[QNCE] Telemetry write failed:', error.message); }); } // ================================ // Sprint #3: Advanced Branching System Integration // ================================ /** * Enable advanced branching capabilities for this story * Integrates the QNCE Branching API with the core engine */ enableBranching(story) { if (this.branchingEngine) { console.warn('[QNCE] Branching already enabled for this engine instance'); return this.branchingEngine; } // Create branching engine with current state this.branchingEngine = (0, branching_1.createBranchingEngine)(story, this.state); if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-enabled', storyId: story.id, chapterCount: story.chapters.length, performanceMode: this.performanceMode }); } return this.branchingEngine; } /** * Get the branching engine if enabled */ getBranchingEngine() { return this.branchingEngine; } /** * Check if branching is enabled */ isBranchingEnabled() { return !!this.branchingEngine; } /** * Sync core engine state with branching engine * Call this when core state changes to keep branching engine updated */ syncBranchingState() { if (this.branchingEngine) { // The branching engine maintains its own state copy // This method could be extended to sync state changes if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-state-synced', currentNodeId: this.state.currentNodeId }); } } } /** * Disable branching and cleanup resources */ disableBranching() { if (this.branchingEngine) { this.branchingEngine = undefined; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'branching-disabled' }); } } } /** * Background cache warming for story data */ async warmCache() { if (!this.performanceMode) return; const threadPool = (0, ThreadPool_1.getThreadPool)(); // Cache all nodes in background const cacheWarmData = { nodeIds: this.storyData.nodes.map(n => n.id), storyId: this.storyData.initialNodeId }; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'cache-warm-start', nodeCount: this.storyData.nodes.length }); } threadPool.submitJob('cache-load', cacheWarmData, 'low').catch(error => { if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'cache-warm-failed', error: error.message }); } }); } // Sprint 3.3: State persistence utility methods /** * Deep copy utility for state objects * @param obj - Object to deep copy * @returns Deep copied object */ deepCopy(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map(item => this.deepCopy(item)); } if (typeof obj === 'object') { const copy = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { copy[key] = this.deepCopy(obj[key]); } } return copy; } return obj; } /** * Generate a hash for the current story data * @returns Story hash string */ generateStoryHash() { const storyString = JSON.stringify({ initialNodeId: this.storyData.initialNodeId, nodeCount: this.storyData.nodes.length, nodeIds: this.storyData.nodes.map(n => n.id).sort() }); // Simple hash function (in production, use crypto.subtle.digest) let hash = 0; for (let i = 0; i < storyString.length; i++) { const char = storyString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } /** * Generate a unique checkpoint ID * @returns Unique checkpoint ID */ generateCheckpointId() { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `checkpoint_${timestamp}_${random}`; } /** * Generate checksum for data integrity * @param data - Data to generate checksum for * @returns Promise resolving to checksum string */ async generateChecksum(data) { // Simple checksum implementation (in production, use crypto.subtle.digest) let checksum = 0; for (let i = 0; i < data.length; i++) { checksum = ((checksum << 5) - checksum) + data.charCodeAt(i); checksum = checksum & checksum; } return checksum.toString(16); } /** * Verify checksum integrity * @param serializedState - State with checksum to verify * @returns Promise resolving to verification result */ async verifyChecksum(serializedState) { const receivedChecksum = serializedState.metadata.checksum; if (!receivedChecksum) return false; const stateToHash = { ...serializedState }; delete stateToHash.metadata.checksum; const stateString = JSON.stringify(stateToHash); const expectedChecksum = await this.generateChecksum(stateString); return receivedChecksum === expectedChecksum; } /** * Validate the structure of the serialized state object * @param serializedState - State to validate * @returns Validation result */ validateSerializedState(serializedState) { const errors = []; const warnings = []; // Check required fields if (!serializedState.state) { errors.push('Missing state field'); } else { if (!serializedState.state.currentNodeId) { errors.push('Missing currentNodeId in state'); } if (!serializedState.state.flags) { warnings.push('Missing flags in state'); } if (!serializedState.state.history) { warnings.push('Missing history in state'); } } if (!serializedState.metadata) { errors.push('Missing metadata field'); } else { if (!serializedState.metadata.engineVersion) { warnings.push('Missing engine version in metadata'); } if (!serializedState.metadata.timestamp) { warnings.push('Missing timestamp in metadata'); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Check version compatibility * @param metadata - Serialization metadata * @returns Compatibility check result */ checkCompatibility(metadata) { const currentVersion = types_1.PERSISTENCE_VERSION; const stateVersion = metadata.engineVersion; if (!stateVersion) { return { compatible: false, reason: 'Unknown state version', suggestions: ['State may be from an older engine version'] }; } if (stateVersion === currentVersion) { return { compatible: true }; } // Simple version comparison (in production, use semver) const [currentMajor, currentMinor] = currentVersion.split('.').map(Number); const [stateMajor, stateMinor] = stateVersion.split('.').map(Number); if (stateMajor > currentMajor || (stateMajor === currentMajor && stateMinor > currentMinor)) { return { compatible: false, reason: `State from newer engine version (${stateVersion} > ${currentVersion})`, suggestions: ['Update the engine to a newer version'] }; } if (stateMajor < currentMajor) { return { compatible: false, reason: `State from incompatible major version (${stateVersion} vs ${currentVersion})`, suggestions: ['Use migration function to upgrade state format'] }; } // Minor version differences are usually compatible return { compatible: true, suggestions: [`State from older minor version (${stateVersion})`] }; } /** * Cleanup old checkpoints based on strategy * @param strategy - Cleanup strategy * @param maxCheckpoints - Maximum number of checkpoints to keep * @returns Promise resolving to number of cleaned checkpoints */ async cleanupCheckpoints(strategy, maxCheckpoints) { const checkpoints = Array.from(this.checkpoints.entries()); if (checkpoints.length <= maxCheckpoints) { return 0; } const toRemove = checkpoints.length - maxCheckpoints; let checkpointsToDelete = []; switch (strategy) { case 'fifo': // Remove oldest by creation order (assuming IDs contain timestamp) checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; case 'timestamp': // Remove oldest by timestamp checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; case 'lru': default: // For now, same as FIFO (would need access tracking in production) checkpointsToDelete = checkpoints .sort(([, a], [, b]) => a.timestamp.localeCompare(b.timestamp)) .slice(0, toRemove) .map(([id]) => id); break; } // Remove checkpoints for (const id of checkpointsToDelete) { this.checkpoints.delete(id); } if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'checkpoints-cleaned', strategy, removedCount: checkpointsToDelete.length, remainingCount: this.checkpoints.size }); } return checkpointsToDelete.length; } /** * Get all checkpoints * @returns Array of all checkpoints */ getCheckpoints() { return Array.from(this.checkpoints.values()); } /** * Get specific checkpoint by ID * @param id - Checkpoint ID * @returns Checkpoint or undefined */ getCheckpoint(id) { return this.checkpoints.get(id); } /** * Delete a checkpoint * @param id - Checkpoint ID to delete * @returns Whether checkpoint was deleted */ deleteCheckpoint(id) { return this.checkpoints.delete(id); } /** * Enable/disable automatic checkpointing * @param enabled - Whether to enable auto-checkpointing */ setAutoCheckpoint(enabled) { this.autoCheckpointEnabled = enabled; } // Sprint 3.5: Autosave and Undo/Redo Implementation /** * Configure autosave settings * @param config - Autosave configuration */ configureAutosave(config) { this.autosaveConfig = { ...this.autosaveConfig, ...config }; } /** * Configure undo/redo settings * @param config - Undo/redo configuration */ configureUndoRedo(config) { this.undoRedoConfig = { ...this.undoRedoConfig, ...config }; } /** * Push a state to the undo stack * @param state - State to save * @param action - Action that caused this state change * @param metadata - Optional metadata about the change */ pushToUndoStack(state, action, metadata) { const startTime = performance.now(); const entry = { id: this.generateHistoryId(), state: this.deepCopy(state), timestamp: new Date().toISOString(), action, metadata }; this.undoStack.push(entry); // Clear redo stack when new change is made this.redoStack = []; // Enforce max undo entries if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) { this.undoStack.shift(); // Remove oldest entry } // Performance tracking if (this.enableProfiling) { const duration = performance.now() - startTime; PerfReporter_1.perf.record('custom', { eventType: 'undo-stack-push', duration, undoCount: this.undoStack.length, action }); } } /** * Undo the last operation * @returns Result of undo operation */ undo() { const startTime = performance.now(); if (this.undoStack.length === 0) { return { success: false, error: 'No operations to undo', stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } try { // Save current state to redo stack const currentEntry = { id: this.generateHistoryId(), state: this.deepCopy(this.state), timestamp: new Date().toISOString(), action: 'redo-point' }; this.redoStack.push(currentEntry); // Enforce max redo entries if (this.redoStack.length > this.undoRedoConfig.maxRedoEntries) { this.redoStack.shift(); // Remove oldest entry } // Restore previous state const entryToRestore = this.undoStack.pop(); // Set flag to prevent triggering undo/redo tracking during restore this.isUndoRedoOperation = true; this.state = this.deepCopy(entryToRestore.state); this.isUndoRedoOperation = false; const duration = performance.now() - startTime; // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'undo-operation', duration, undoCount: this.undoStack.length, redoCount: this.redoStack.length }); } return { success: true, restoredState: this.deepCopy(this.state), entry: { id: entryToRestore.id, timestamp: entryToRestore.timestamp, action: entryToRestore.action, nodeId: entryToRestore.metadata?.nodeId }, stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } catch (error) { this.isUndoRedoOperation = false; // Ensure flag is reset on error const errorMessage = error instanceof Error ? error.message : 'Unknown undo error'; return { success: false, error: `Undo failed: ${errorMessage}`, stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } } /** * Redo the last undone operation * @returns Result of redo operation */ redo() { const startTime = performance.now(); if (this.redoStack.length === 0) { return { success: false, error: 'No operations to redo', stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } try { // Save current state to undo stack const currentEntry = { id: this.generateHistoryId(), state: this.deepCopy(this.state), timestamp: new Date().toISOString(), action: 'undo-point' }; this.undoStack.push(currentEntry); // Enforce max undo entries if (this.undoStack.length > this.undoRedoConfig.maxUndoEntries) { this.undoStack.shift(); // Remove oldest entry } // Restore redo state const entryToRestore = this.redoStack.pop(); // Set flag to prevent triggering undo/redo tracking during restore this.isUndoRedoOperation = true; this.state = this.deepCopy(entryToRestore.state); this.isUndoRedoOperation = false; const duration = performance.now() - startTime; // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'redo-operation', duration, undoCount: this.undoStack.length, redoCount: this.redoStack.length }); } return { success: true, restoredState: this.deepCopy(this.state), entry: { id: entryToRestore.id, timestamp: entryToRestore.timestamp, action: entryToRestore.action, nodeId: entryToRestore.metadata?.nodeId }, stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } catch (error) { this.isUndoRedoOperation = false; // Ensure flag is reset on error const errorMessage = error instanceof Error ? error.message : 'Unknown redo error'; return { success: false, error: `Redo failed: ${errorMessage}`, stackSizes: { undoCount: this.undoStack.length, redoCount: this.redoStack.length } }; } } /** * Check if undo is available * @returns True if undo is possible */ canUndo() { return this.undoRedoConfig.enabled && this.undoStack.length > 0; } /** * Check if redo is available * @returns True if redo is possible */ canRedo() { return this.undoRedoConfig.enabled && this.redoStack.length > 0; } /** * Get the number of available undo operations * @returns Number of undo entries */ getUndoCount() { return this.undoStack.length; } /** * Get the number of available redo operations * @returns Number of redo entries */ getRedoCount() { return this.redoStack.length; } /** * Clear all undo/redo history */ clearHistory() { this.undoStack = []; this.redoStack = []; if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'history-cleared' }); } } /** * Get history summary for debugging * @returns Summary of undo/redo history */ getHistorySummary() { return { undoEntries: this.undoStack.map(entry => ({ id: entry.id, timestamp: entry.timestamp, action: entry.action })), redoEntries: this.redoStack.map(entry => ({ id: entry.id, timestamp: entry.timestamp, action: entry.action })) }; } /** * Trigger an autosave operation * @param trigger - What triggered the autosave * @param metadata - Optional metadata about the trigger * @returns Promise resolving to autosave result */ async triggerAutosave(trigger, metadata) { const startTime = performance.now(); // Check if autosave is enabled if (!this.autosaveConfig.enabled) { return { success: false, error: 'Autosave is disabled' }; } // Check throttling const now = performance.now(); if (now - this.lastAutosaveTime < this.autosaveConfig.throttleMs) { return { success: false, error: 'Autosave throttled', trigger }; } try { // Create autosave checkpoint const checkpointName = `autosave-${trigger}-${Date.now()}`; const checkpoint = await this.createCheckpoint(checkpointName, { includeMetadata: this.autosaveConfig.includeMetadata, autoTags: ['autosave', trigger], maxCheckpoints: this.autosaveConfig.maxEntries }); this.lastAutosaveTime = now; const duration = performance.now() - startTime; // Performance tracking if (this.enableProfiling) { PerfReporter_1.perf.record('custom', { eventType: 'autosave-completed', duration, trigger, checkpointId: checkpoint.id }); } return { success: true, checkpointId: checkpoint.id, trigger,