qnce-engine
Version:
Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization
328 lines • 11.1 kB
JavaScript
;
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