UNPKG

qnce-engine

Version:

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

328 lines 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StoryDeltaPatcher = exports.StoryDeltaComparator = void 0; exports.createDeltaTools = createDeltaTools; /** * Delta Comparison Engine for Hot-Reload Story Updates * Identifies minimal changes needed to update narrative content */ class StoryDeltaComparator { /** * Compare two story configurations and generate delta */ compareStories(oldStory, newStory) { const timestamp = performance.now(); return { nodeChanges: this.compareNodes(oldStory.nodes || [], newStory.nodes || []), assetChanges: this.compareAssets(oldStory.assets || [], newStory.assets || []), timestamp }; } /** * Deep comparison of narrative nodes */ compareNodes(oldNodes, newNodes) { const deltas = []; const oldNodeMap = new Map(oldNodes.map(n => [n.id, n])); const newNodeMap = new Map(newNodes.map(n => [n.id, n])); // Check for removed nodes for (const [nodeId, oldNode] of oldNodeMap) { if (!newNodeMap.has(nodeId)) { deltas.push({ nodeId, changeType: 'removed', oldNode, affectedFields: ['*'] }); } } // Check for added and modified nodes for (const [nodeId, newNode] of newNodeMap) { const oldNode = oldNodeMap.get(nodeId); if (!oldNode) { // New node deltas.push({ nodeId, changeType: 'added', newNode, affectedFields: ['*'] }); } else { // Check for modifications const affectedFields = this.findChangedFields(oldNode, newNode); if (affectedFields.length > 0) { deltas.push({ nodeId, changeType: 'modified', oldNode, newNode, affectedFields }); } } } return deltas; } /** * Compare assets (future: images, audio, etc.) */ compareAssets(oldAssets, newAssets) { const deltas = []; const oldAssetMap = new Map(oldAssets.map(a => [a.id, a])); const newAssetMap = new Map(newAssets.map(a => [a.id, a])); // Check for removed assets for (const [assetId, oldAsset] of oldAssetMap) { if (!newAssetMap.has(assetId)) { deltas.push({ assetId, changeType: 'removed', oldAsset, sizeChange: -(oldAsset.size || 0) }); } } // Check for added and modified assets for (const [assetId, newAsset] of newAssetMap) { const oldAsset = oldAssetMap.get(assetId); if (!oldAsset) { // New asset deltas.push({ assetId, changeType: 'added', newAsset, sizeChange: newAsset.size || 0 }); } else if (this.assetsAreDifferent(oldAsset, newAsset)) { // Modified asset deltas.push({ assetId, changeType: 'modified', oldAsset, newAsset, sizeChange: (newAsset.size || 0) - (oldAsset.size || 0) }); } } return deltas; } /** * Fine-grained field comparison for nodes */ findChangedFields(oldNode, newNode) { const changedFields = []; const allFields = new Set([...Object.keys(oldNode), ...Object.keys(newNode)]); for (const field of allFields) { if (!this.deepEqual(oldNode[field], newNode[field])) { changedFields.push(field); } } return changedFields; } /** * Asset comparison logic */ assetsAreDifferent(oldAsset, newAsset) { // Simple comparison - in practice, would use checksums/hashes return JSON.stringify(oldAsset) !== JSON.stringify(newAsset); } /** * Deep equality check for objects */ deepEqual(a, b) { if (a === b) return true; if (a === null || b === null) return false; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key)) return false; if (!this.deepEqual(a[key], b[key])) return false; } return true; } return false; } } exports.StoryDeltaComparator = StoryDeltaComparator; /** * Hot-Reload Delta Patcher * Applies story deltas with minimal engine disruption */ class StoryDeltaPatcher { engine; constructor(engine) { this.engine = engine; } /** * Apply delta patch to running engine * Target: <2ms frame stall for hot-reload */ async applyDelta(delta) { const startTime = performance.now(); const patchId = `patch-${Date.now()}`; try { // Phase 1: Validate delta can be applied safely (sync, fast) const validation = this.validateDelta(delta); if (!validation.safe) { return { success: false, patchId, duration: performance.now() - startTime, error: validation.error }; } // Phase 2: Apply changes synchronously for speed // For small deltas (<10 changes), apply immediately // For larger deltas, use optimized batch processing if (delta.nodeChanges.length <= 10) { this.applyNodeChangesFast(delta.nodeChanges); } else { await this.applyNodeChanges(delta.nodeChanges); } // Phase 3: Skip asset changes for now (async operation) // Assets can be updated in background without frame stall // Phase 4: Minimal state refresh this.refreshEngineState(); const duration = performance.now() - startTime; return { success: true, patchId, duration, nodesChanged: delta.nodeChanges.length, assetsChanged: delta.assetChanges.length }; } catch (error) { return { success: false, patchId, duration: performance.now() - startTime, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Validate delta is safe to apply */ validateDelta(delta) { // Check if current node would be affected const currentNodeId = this.engine.getState().currentNodeId; const currentNodeChange = delta.nodeChanges.find(c => c.nodeId === currentNodeId); if (currentNodeChange && currentNodeChange.changeType === 'removed') { return { safe: false, error: 'Cannot remove currently active node' }; } // Additional safety checks could go here return { safe: true }; } /** * Apply node changes to story data */ async applyNodeChanges(nodeChanges) { // Batch node changes to minimize engine updates const batches = this.batchNodeChanges(nodeChanges); for (const batch of batches) { await this.processBatch(batch); } } /** * Fast synchronous node changes for small deltas (no async overhead) */ applyNodeChangesFast(nodeChanges) { for (const change of nodeChanges) { this.applyNodeChange(change); } } /** * Apply asset changes (placeholder for future implementation) */ async applyAssetChanges(assetChanges) { // Future: Implement asset hot-reload console.log(`Would apply ${assetChanges.length} asset changes`); } /** * Batch node changes by type for efficient processing */ batchNodeChanges(nodeChanges) { const batches = []; const batchSize = 25; // Larger batch size for efficiency for (let i = 0; i < nodeChanges.length; i += batchSize) { batches.push(nodeChanges.slice(i, i + batchSize)); } return batches; } /** * Process a batch of node changes */ async processBatch(batch) { // Use requestAnimationFrame or setTimeout to yield control return new Promise(resolve => { setTimeout(() => { for (const change of batch) { this.applyNodeChange(change); } resolve(); }, 0); }); } /** * Apply individual node change */ applyNodeChange(change) { // Access engine's internal story data const storyData = this.engine.storyData; switch (change.changeType) { case 'added': { if (change.newNode) { storyData.nodes.push(change.newNode); } break; } case 'removed': { const removeIndex = storyData.nodes.findIndex((n) => n.id === change.nodeId); if (removeIndex >= 0) { storyData.nodes.splice(removeIndex, 1); } break; } case 'modified': const modifyIndex = storyData.nodes.findIndex((n) => n.id === change.nodeId); if (modifyIndex >= 0 && change.newNode) { // For simplicity, always replace the entire node storyData.nodes[modifyIndex] = change.newNode; } break; } } /** * Refresh engine state after patch */ refreshEngineState() { // Minimal state refresh - avoid full reinitialization // Future: Could trigger cache invalidation, UI updates, etc. } } exports.StoryDeltaPatcher = StoryDeltaPatcher; // Factory function for creating delta tools function createDeltaTools(engine) { return { comparator: new StoryDeltaComparator(), patcher: new StoryDeltaPatcher(engine) }; } //# sourceMappingURL=HotReloadDelta.js.map