datastores/memory/MemoryDataStore.js

/**
 *  MemoryDataStore - stores properties temporarily in memory
 * 
 *  This code is licensed under the MIT License (MIT).
 *  
 *  Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the “Software”), to deal
 *  in the Software without restriction, including without limitation the rights 
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 *  copies of the Software, and to permit persons to whom the Software is 
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 *  THE SOFTWARE.
 *  
 */

/**
 * A general purpose datastore that stores properties temporarily in memory. The
 * memory is lost on reload.
 * 
 * This datastore registers as the type "memory".
 *
 * ### Options
 * * "storageName" - The memory bucket name of the storage (default: "memory"). Multiple independant buckets can be maintained
 *
 * @memberOf Datastores
 * @example
 * {
 *   "dataStores": { 
 *      "myDataStore": {
 *          "type": "memory", 
 *          "options": {
 *              "storageName": "myMemory"
 *          }
 *      },
 *      ...
 *  },
 *  ...
 *
 */
class MemoryDataStore extends DirectDatastore {
    constructor(name, options = {}) {
        super(name, options);

        this.deleteCallbacks = [];
    }

    isShared() {
        return false;
    }

    destroy() {
        this.deleteCallbacks.forEach((deleteCallback)=>{
            deleteCallback.delete();
        })
    }

    async init() {
        const self = this;
        this.typeVariable = "__memoryDataStore_internalType";

        this.storageName = "memory";
        if (this.options.storageName) this.storageName = this.options.storageName;

        this.deleteCallbacks.push(VarvEngine.registerEventCallback("disappeared", async (context)=> {
            if(MemoryDataStore.DEBUG) {
                console.log("Saw disappeared UUID (MemoryDataStore):", context.target);
            }

            context.concept.properties.forEach((property) => {
                if (self.isPropertyMapped(context.concept, property)) {
                    let data = self.internalPropertyTrackingData(context.concept, property);
                    delete data[context.target];
                }
            });
            self.getStorage().delete(context.target);
        }));
        this.deleteCallbacks.push(VarvEngine.registerEventCallback("appeared", async (context)=> {
            if(MemoryDataStore.DEBUG) {
                console.log("Saw appeared UUID (MemoryDataStore):", context.target);
            }
            let mark = VarvPerformance.start();
            if (self.isConceptMapped(context.concept) && !self.getStorage().has(context.target)){
                self.getStorage().set(context.target, {
                    [self.typeVariable]: context.concept.name
                });
            }
            VarvPerformance.stop("MemoryDataStore.registerEventCallback.appeared", mark);
        }));
    }
    
    getStorage(){
        if (!MemoryDataStore.storages[this.storageName]){
            MemoryDataStore.storages[this.storageName] = new Map();            
        }
        return MemoryDataStore.storages[this.storageName];
    }

    createBackingStore(concept, property) {
        const self = this;
        
        if (this.isPropertyMapped(concept,property)) return;
                
        let setter = (uuid, value) => {
            let mark = VarvPerformance.start();
            if (!self.getStorage().has(uuid)){
                throw new Error("Tried to set concept in memory that never appeared: "+concept.name+"."+property.name);
            }
            
            let data = self.getStorage().get(uuid);
            data[property.name] = value;
            VarvPerformance.stop("MemoryDataStore.setter", mark);
        };
        let getter = (uuid) => {
            let mark = VarvPerformance.start();
            let data = self.getStorage().get(uuid);
            if (!data) throw new Error("Tried to get concept from memory that was never set: "+concept.name+"."+property.name);
            if (!data.hasOwnProperty(property.name)) throw new Error("Tried to get property from memory that was never set: "+concept.name+"."+property.name);
            let result = data[property.name];
            VarvPerformance.stop("MemoryDataStore.getter", mark);
            return result;
        };
        property.addSetCallback(setter);
        property.addGetCallback(getter);

        // Check if concept already is mapped, if not, register it
        this.internalAddPropertyMapping(concept, property, {setter: setter, getter: getter});
    }    
    
    removeBackingStore(concept, property) {
        if (!this.isPropertyMapped(concept, property)){
            throw new Error('Cannot unmap property from memory because the property was not mapped: ' + concept + "." + property);
        }

        let trackingData = this.internalPropertyTrackingData(concept, property);
        property.removeSetCallback(trackingData.setter);
        property.removeGetCallback(trackingData.getter);
        
        this.internalRemovePropertyMapping(concept, property);
    }
    
    async loadBackingStore() {
        // For each of our stored and mapped concepts
        for(let [uuid,storedConcept] of this.getStorage().entries()) {
            if (MemoryDataStore.DEBUG) console.log("Loading from memory",uuid,storedConcept);
            
            let type = storedConcept[this.typeVariable];
            if ((!type) || !this.isConceptTypeMapped(type)){
                if (MemoryDataStore.DEBUG) console.log("Ignoring concept from memory since it is not mapped", storedConcept);                    
                continue;
            }
            
            // Check if already registered and only generate an appear event if not
            let conceptByUUID = await VarvEngine.getConceptFromUUID(uuid);
            let concept = VarvEngine.getConceptFromType(type);                        
            
            this.registerConceptFromUUID(uuid, concept);

            // Stil set the properties that we know about
            for (const [propertyName,value] of Object.entries(storedConcept)){
                if (propertyName !== this.typeVariable){
                    try {
                        let property = concept.getProperty(propertyName);                    
                        if (MemoryDataStore.DEBUG) console.log("Loading property", property, value);
                        if (this.isPropertyMapped(concept, property)){
                            await property.setValue(uuid, value);
                        }
                    } catch (ex){
                        console.error("Failed to push concept property from memory to concept", ex);
                    }
                }
            }
            
            if (!conceptByUUID) {
                await concept.appeared(uuid);                        
            }            
        }
    }
}
MemoryDataStore.DEBUG = false;
window.MemoryDataStore = MemoryDataStore;
MemoryDataStore.storages = new Map();

// Register default dom datastore
Datastore.registerDatastoreType("memory", MemoryDataStore);