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
JavaScript
"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,