UNPKG

superfly-timeline

Version:

Resolver for defining objects with temporal boolean logic relationships on a timeline

298 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hashTimelineObject = exports.CacheHandler = void 0; const lib_1 = require("./lib/lib"); const performance_1 = require("./lib/performance"); const reference_1 = require("./lib/reference"); const timeline_1 = require("./lib/timeline"); class CacheHandler { constructor(cache, resolvedTimeline) { this.resolvedTimeline = resolvedTimeline; if (!cache.objHashes) cache.objHashes = {}; if (!cache.objects) cache.objects = {}; if (!cache.canBeUsed) { // Reset the cache: CacheHandler.resetCache(cache); this.canUseIncomingCache = false; } else { this.canUseIncomingCache = true; } if (this.resolvedTimeline.traceResolving) { this.resolvedTimeline.addResolveTrace(`cache: init`); this.resolvedTimeline.addResolveTrace(`cache: canUseIncomingCache: ${this.canUseIncomingCache}`); this.resolvedTimeline.addResolveTrace(`cache: cached objects: ${JSON.stringify(Object.keys(cache.objects))}`); } // cache.canBeUsed will be set in this.persistData() cache.canBeUsed = false; this.cache = cache; } determineChangedObjects() { const toc = (0, performance_1.tic)(' cache.determineChangedObjects'); // Go through all new objects, and determine whether they have changed: const allNewObjects = {}; const changedTracker = new ChangedTracker(); for (const obj of this.resolvedTimeline.objectsMap.values()) { const oldHash = this.cache.objHashes[obj.id]; const newHash = hashTimelineObject(obj); allNewObjects[obj.id] = true; if (!oldHash) { if (this.resolvedTimeline.traceResolving) { this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is new`); } } else if (oldHash !== newHash) { if (this.resolvedTimeline.traceResolving) { this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" has changed`); } } if ( // Object is new: !oldHash || // Object has changed: oldHash !== newHash) { this.cache.objHashes[obj.id] = newHash; changedTracker.addChangedObject(obj); const oldObj = this.cache.objects[obj.id]; if (oldObj) changedTracker.addChangedObject(oldObj); } else { // No timing-affecting changes detected /* istanbul ignore if */ if (!oldHash) { if (this.resolvedTimeline.traceResolving) { this.resolvedTimeline.addResolveTrace(`cache: object "${obj.id}" is similar`); } } // Even though the timeline-properties hasn't changed, // the content (and other properties) might have: const oldObj = this.cache.objects[obj.id]; /* istanbul ignore if */ if (!oldObj) { console.error(`oldHash: "${oldHash}"`); console.error(`ids: ${JSON.stringify(Object.keys(this.cache.objects))}`); throw new Error(`Internal Error: obj "${obj.id}" not found in cache, even though hashes match!`); } this.cache.objects[obj.id] = { ...obj, resolved: oldObj.resolved, }; } } if (this.canUseIncomingCache) { // Go through all old hashes, removing the ones that doesn't exist anymore for (const objId in this.cache.objects) { if (!allNewObjects[objId]) { const obj = this.cache.objects[objId]; delete this.cache.objHashes[objId]; changedTracker.addChangedObject(obj); } } // At this point, all directly changed objects have been marked as changed. // Next step is to invalidate any indirectly affected objects, by gradually removing the invalidated ones from validObjects // Prepare the invalidator, ie populate it with the objects that are still valid: const invalidator = new Invalidator(); for (const obj of this.resolvedTimeline.objectsMap.values()) { invalidator.addValidObject(obj); } for (const obj of this.resolvedTimeline.objectsMap.values()) { // Add everything that this object affects: const cachedObj = this.cache.objects[obj.id]; let affectedReferences = getAllReferencesThisObjectAffects(obj); if (cachedObj) { affectedReferences = (0, reference_1.joinReferences)(affectedReferences, getAllReferencesThisObjectAffects(cachedObj)); } for (let i = 0; i < affectedReferences.length; i++) { const ref = affectedReferences[i]; const objRef = `#${obj.id}`; if (ref !== objRef) { invalidator.addAffectedReference(objRef, ref); } } // Add everything that this object is affected by: if (changedTracker.isChanged(`#${obj.id}`)) { // The object is directly said to have changed. } else { // The object is not directly said to have changed. // But if might have been affected by other objects that have changed. // Note: we only have to check for the OLD object, since if the old and the new object differs, // that would mean it'll be directly invalidated anyway. if (cachedObj) { // Fetch all references for the object from the last time it was resolved. // Note: This can be done, since _if_ the object was changed in any way since last resolve // it'll be invalidated anyway const dependOnReferences = cachedObj.resolved.directReferences; // Build up objectLayerMap: if ((0, timeline_1.objHasLayer)(cachedObj)) { invalidator.addObjectOnLayer(`${cachedObj.layer}`, obj); } for (let i = 0; i < dependOnReferences.length; i++) { const ref = dependOnReferences[i]; invalidator.addAffectedReference(ref, `#${obj.id}`); } } } } // Invalidate all changed objects, and recursively invalidate all objects that reference those objects: for (const reference of changedTracker.listChanged()) { invalidator.invalidateObjectsWithReference(reference); } if (this.resolvedTimeline.traceResolving) { this.resolvedTimeline.addResolveTrace(`cache: changed references: ${JSON.stringify(Array.from(changedTracker.listChanged()))}`); this.resolvedTimeline.addResolveTrace(`cache: invalidated objects: ${JSON.stringify(Array.from(invalidator.getInValidObjectIds()))}`); this.resolvedTimeline.addResolveTrace(`cache: unchanged objects: ${JSON.stringify(invalidator.getValidObjects().map((o) => o.id))}`); } // At this point, the objects that are left in validObjects are still valid (ie has not changed or is affected by any others). // We can reuse the old resolving for those: for (const obj of invalidator.getValidObjects()) { if (!this.cache.objects[obj.id]) { /* istanbul ignore next */ throw new Error(`Internal Error: Something went wrong: "${obj.id}" does not exist in cache.resolvedTimeline.objects`); } this.resolvedTimeline.objectsMap.set(obj.id, this.cache.objects[obj.id]); } } toc(); } persistData() { const toc = (0, performance_1.tic)(' cache.persistData'); if (this.resolvedTimeline.resolveError) { // If there was a resolve error, clear the cache: this.cache.objHashes = {}; this.cache.objects = {}; this.cache.canBeUsed = false; } else { this.cache.objects = (0, lib_1.mapToObject)(this.resolvedTimeline.objectsMap); this.cache.canBeUsed = true; } toc(); } /** Resets / Clears the cache */ static resetCache(cache) { delete cache.canBeUsed; cache.objHashes = {}; cache.objects = {}; } } exports.CacheHandler = CacheHandler; /** Return a "hash-string" which changes whenever anything that affects timing of a timeline-object has changed. */ function hashTimelineObject(obj) { /* Note: The following properties are ignored, as they don't affect timing or resolving: * id * children * keyframes * isGroup * content */ return `${JSON.stringify(obj.enable)},${+!!obj.disabled},${obj.priority}',${obj.resolved.parentId},${+obj.resolved .isKeyframe},${obj.classes ? obj.classes.join('.') : ''},${obj.layer},${+!!obj.seamless}`; } exports.hashTimelineObject = hashTimelineObject; function getAllReferencesThisObjectAffects(newObj) { const references = [`#${newObj.id}`]; if (newObj.classes) { for (const className of newObj.classes) { references.push(`.${className}`); } } if ((0, timeline_1.objHasLayer)(newObj)) references.push(`$${newObj.layer}`); if (newObj.children) { for (const child of newObj.children) { references.push(`#${child.id}`); } } return references; } /** * Keeps track of which timeline object have been changed */ class ChangedTracker { constructor() { this.changedReferences = new Set(); } /** * Mark an object as "has changed". * Will store all references that are affected by this object. */ addChangedObject(obj) { for (const ref of getAllReferencesThisObjectAffects(obj)) { this.changedReferences.add(ref); } } /** Returns true if a reference has changed */ isChanged(ref) { return this.changedReferences.has(ref); } /** Returns a list of all changed references */ listChanged() { return this.changedReferences.keys(); } } /** The Invalidator */ class Invalidator { constructor() { this.handledReferences = {}; /** All references that depend on another reference (ie objects, class or layers): */ this.affectReferenceMap = {}; this.validObjects = {}; this.inValidObjectIds = []; /** Map of which objects can be affected by any other object, per layer */ this.objectLayerMap = {}; } addValidObject(obj) { this.validObjects[obj.id] = obj; } getValidObjects() { return Object.values(this.validObjects); } getInValidObjectIds() { return this.inValidObjectIds; } addObjectOnLayer(layer, obj) { if (!this.objectLayerMap[layer]) this.objectLayerMap[layer] = []; this.objectLayerMap[layer].push(obj.id); } addAffectedReference(objRef, ref) { if (!this.affectReferenceMap[objRef]) this.affectReferenceMap[objRef] = []; this.affectReferenceMap[objRef].push(ref); } /** Invalidate all changed objects, and recursively invalidate all objects that reference those objects */ invalidateObjectsWithReference(reference) { if (this.handledReferences[reference]) return; // to avoid infinite loops this.handledReferences[reference] = true; if ((0, reference_1.isObjectReference)(reference)) { const objId = (0, reference_1.getRefObjectId)(reference); if (this.validObjects[objId]) { delete this.validObjects[objId]; this.inValidObjectIds.push(objId); } } if ((0, reference_1.isLayerReference)(reference)) { const layer = (0, reference_1.getRefLayer)(reference); if (this.objectLayerMap[layer]) { for (const affectedObjId of this.objectLayerMap[layer]) { this.invalidateObjectsWithReference(`#${affectedObjId}`); } } } // Invalidate all objects that depend on any of the references that this reference affects: const affectedReferences = this.affectReferenceMap[reference]; if (affectedReferences) { for (let i = 0; i < affectedReferences.length; i++) { const referencingReference = affectedReferences[i]; this.invalidateObjectsWithReference(referencingReference); } } } } //# sourceMappingURL=CacheHandler.js.map