/**
* LocalStorageDataStore - serializes into the localStorage database
*
* 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 storage that serializes into the localStorage database
*
* ### Options
* * storageName - The name of the prefix to store that data below (Default: "varv-data")
*
* @memberOf Datastores
*/
class LocalStorageDataStore extends DirectDatastore {
constructor(name, options = {}) {
super(name, options);
if (typeof webstrate !== "undefined"){
this.storagePrefix = webstrate.webstrateId+"-datastore";
} else if (location.protocol !== "file:"){
this.storagePrefix = location.pathname+"-datastore";
} else {
this.storagePrefix = "varv-data";
}
this.entities = {};
if (this.options.storageName) this.storagePrefix = this.options.storageName;
this.deleteCallbacks = [];
}
isShared() {
return false;
}
async init(){
const self = this;
if (!localStorage) throw new Error("Cannot use localStorage as the feature is not available on this js runtime platform");
if (!localStorage.getItem(this.storagePrefix)) self.saveEntities();
this.deleteCallbacks.push(VarvEngine.registerEventCallback("disappeared", async (context)=> {
if(LocalStorageDataStore.DEBUG) {
console.log("Saw disappeared UUID (LocalStorageDataStore):", context.target);
}
if (!self.entities[context.target]) return; // avoid loops when we caused the disappear event ourselves
context.concept.properties.forEach((property) => {
if (self.isPropertyMapped(context.concept, property)) {
localStorage.removeItem(self.storagePrefix + "-" + context.target + "-" + property.name);
}
});
delete self.entities[context.target];
self.saveEntities();
}));
this.deleteCallbacks.push(VarvEngine.registerEventCallback("appeared", async (context)=> {
if(LocalStorageDataStore.DEBUG) {
console.log("Saw appeared UUID (LocalStorageDataStore):", context.target);
}
let mark = VarvPerformance.start();
if (self.entities[context.target]) return; // avoid loops when we caused the appear event ourselves
if (self.isConceptMapped(context.concept)) {
self.entities[context.target] = context.concept.name;
self.saveEntities();
}
VarvPerformance.stop("LocalStorageDataStore.registerEventCallback.appeared", mark);
}));
this.storageEventListener = async function localStorageChangeUpdate(event){
if (event.key===self.storagePrefix){
// This is a change in the active entities, pull any new ones
let storedEntities = JSON.parse(event.newValue);
for (const [uuid,type] of Object.entries(storedEntities)){
if (!self.entities[uuid]){
// TODO: check mapped type?
self.entities[uuid] = type;
await self.pullConcept(uuid);
}
}
// and kill any that are gone
for (const [uuid,type] of Object.entries(self.entities)){
if (!storedEntities[uuid]){
delete self.entities[uuid]; // delete our tracker BEFORE sending disappear event to avoid loops
await VarvEngine.getConceptFromType(type).disappeared(uuid);
}
};
} else if (event.key.startsWith(self.storagePrefix+"-")){
// This could be a property change event if the key matches a property
let matches = new RegExp("^([^-]+)\-(.+?)$").exec(event.key.substring(self.storagePrefix.length+1));
if (matches.length===3){
if (event.newValue===null) return; // TODO: The property was removed, is there any way to handle this properly?
let uuid = matches[1];
let propertyName = matches[2];
let concept = self.getConceptFromUUID(uuid);
if (!concept){
console.log("Localstorage got property update from concept with UUID that does not exist locally", uuid);
return;
}
let property = concept.getProperty(propertyName);
if (!property){
console.log("Localstorage got property update from concept property that does not exist locally", concept, propertyName);
return;
}
await property.setValue(uuid, JSON.parse(event.newValue));
}
}
};
window.addEventListener("storage", this.storageEventListener);
}
destroy() {
this.deleteCallbacks.forEach((deleteCallback)=>{
deleteCallback.delete();
});
window.removeEventListener("storage", this.storageEventListener);
}
saveEntities(){
localStorage.setItem(this.storagePrefix, JSON.stringify(this.entities));
}
createBackingStore(concept, property) {
const self = this;
if (this.isPropertyMapped(concept,property)) {
console.log("FIXME: Trying to create localStorage backing store for already mapped property, ignored", concept, property);
return;
}
let setter = (uuid, value) => {
let mark = VarvPerformance.start();
if (!self.entities[uuid]){
throw new Error("Tried to set concept property in localStorage for concept instance that never appeared: "+concept.name+"."+property.name);
}
localStorage.setItem(self.storagePrefix+"-"+uuid+"-"+property.name, JSON.stringify(value));
VarvPerformance.stop("LocalStorageDataStore.setter", mark);
};
let getter = (uuid) => {
let mark = VarvPerformance.start();
if (!self.entities[uuid]){
throw new Error("Tried to get concept property from localStorage for concept instance that was never seen: "+concept.name+"."+property.name);
}
let data = localStorage.getItem(self.storagePrefix+"-"+uuid+"-"+property.name);
if (data===null){
throw new Error("Tried to get concept property from localStorage that was never set: "+concept.name+"."+property.name);
}
let result = JSON.parse(data);
VarvPerformance.stop("LocalStorageDataStore.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 localStorage 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);
}
/**
* Loads all concept instances currently registered as backed from serialized state
*
* @returns {undefined}
*/
async loadBackingStore() {
let self = this;
this.entities = JSON.parse(localStorage.getItem(this.storagePrefix));
for (const [uuid,type] of Object.entries(this.entities)){
await self.pullConcept(uuid);
}
}
async pullConcept(uuid){
let self = this;
let conceptType = this.entities[uuid];
if (!conceptType){
throw new Error("Tried to pull a concept from localStorage that wasn't a known entity in there");
}
const concept = VarvEngine.getConceptFromType(conceptType);
if (!concept) {
if (LocalStorageDataStore.DEBUG) console.log("LocalStorage: Ignoring unknown concept with type", type);
return;
}
if (!self.isConceptMapped(concept)){
if (LocalStorageDataStore.DEBUG) console.log("LocalStorage: Ignoring concept with type that isn't mapped to localStorage", type);
return;
}
let conceptByUUID = await VarvEngine.getConceptFromUUID(uuid);
this.registerConceptFromUUID(uuid, concept);
// Pull all properties
for (const property of concept.properties){
if (self.isPropertyMapped(concept, property)){
try {
let value = localStorage.getItem(self.storagePrefix+"-"+uuid+"-"+property.name);
if (value!==null){
await property.setValue(uuid, JSON.parse(value));
}
} catch (ex){
// Ignore
}
}
};
if (!conceptByUUID) {
await concept.appeared(uuid);
}
}
}
LocalStorageDataStore.DEBUG = false;
window.LocalStorageDataStore = LocalStorageDataStore;
//Register default dom datastore
Datastore.registerDatastoreType("localStorage", LocalStorageDataStore);