UNPKG

lincd

Version:

LINCD is a JavaScript library for building user interfaces with linked data (also known as 'structured data', or RDF)

871 lines 44.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LinkedStorage = void 0; const models_1 = require("../models"); const QuadSet_1 = require("../collections/QuadSet"); const CoreMap_1 = require("../collections/CoreMap"); const NodeSet_1 = require("../collections/NodeSet"); const SHACL_1 = require("../shapes/SHACL"); const EventBatcher_1 = require("../events/EventBatcher"); const next_tick_1 = __importDefault(require("next-tick")); const QuadArray_1 = require("../collections/QuadArray"); const CoreSet_1 = require("../collections/CoreSet"); const ShapeSet_1 = require("../collections/ShapeSet"); const ShapeClass_1 = require("./ShapeClass"); const rdf_1 = require("../ontologies/rdf"); class LinkedStorage { static init() { if (!this._initialized) { models_1.Quad.emitter.on(models_1.Quad.QUADS_ALTERED, this.onEvent.bind(this, models_1.Quad.QUADS_ALTERED)); models_1.NamedNode.emitter.on(models_1.NamedNode.STORE_NODES, this.onEvent.bind(this, models_1.NamedNode.STORE_NODES)); models_1.NamedNode.emitter.on(models_1.NamedNode.REMOVE_NODES, this.onEvent.bind(this, models_1.NamedNode.REMOVE_NODES)); models_1.NamedNode.emitter.on(models_1.NamedNode.CLEARED_PROPERTIES, this.onEvent.bind(this, models_1.NamedNode.CLEARED_PROPERTIES)); this._initialized = true; } } /** * Returns true if Storage is set up to use any specific store * returns false if storage is managed manually, and no call like Storage.setDefaultStore has been made */ static isInitialised() { return this.defaultStore && true; } static onEvent(eventType, ...args) { //so either a TRIPLES_ALTERED, CLEARED_PROPERTIES, STORE_RESOURCES or REMOVE_RESOURCES event comes in //if we have not stored any events yet if (!this.storedEvents) { //start storing this.storedEvents = {}; //and if we're not already in an active processing cycle //then let's start processing whatever we store in this event cycle on the next tick if (!this.processingPromise) { this.startProcessingOnNextTick(); } } //save the event to be processed later if (!this.storedEvents[eventType]) { this.storedEvents[eventType] = []; } this.storedEvents[eventType].push(args); } static startProcessingOnNextTick() { //create the processing promise, so that any request for promiseUpdate() will already get the promise that resolves after these events are handled let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); this.processingPromise = { promise, resolve, reject }; //start processing the stored events on the next tick (0, next_tick_1.default)(() => { this.processStoredEvents(); }); } static processStoredEvents() { return __awaiter(this, void 0, void 0, function* () { const storedEvents = this.storedEvents; this.storedEvents = null; //we store and then process events, so we can determine in which order we process them. //the order of this array determines the order. //each entry in the array that is looped is an event type + handler const processOrder = [ [models_1.NamedNode.REMOVE_NODES, this.onRemoveNodes], [models_1.NamedNode.CLEARED_PROPERTIES, this.onClearedProperties], [models_1.NamedNode.STORE_NODES, this.onStoreNodes], [models_1.Quad.QUADS_ALTERED, this.onQuadsAltered], ]; //combine multiple events that want to add/remove quads into 1 if (storedEvents[models_1.Quad.QUADS_ALTERED] && storedEvents[models_1.Quad.QUADS_ALTERED].length > 1) { const created = new QuadSet_1.QuadSet(); const removed = new QuadSet_1.QuadSet(); storedEvents[models_1.Quad.QUADS_ALTERED].forEach(([quadsCreated, quadsRemoved]) => { quadsCreated.forEach((q) => created.add(q)); quadsRemoved.forEach((q) => removed.add(q)); }); //remove things that have been created & removed during the previous execution cycle removed.forEach((q) => { if (created.has(q)) { removed.delete(q); created.delete(q); } }); //replace stored events with one stored event that has again 2 args, the combined sets storedEvents[models_1.Quad.QUADS_ALTERED] = [[created, removed]]; } //combine the Sets of multiple STORE_NODES/REMOVE_NODES/CLEARED_PROPERTIES events into one Set each // note that since they have slightly different values, this will convert NodeSets of STORE_NODES to a CoreSet (note a NodeSet) [models_1.NamedNode.STORE_NODES, models_1.NamedNode.REMOVE_NODES].forEach((eventType) => { if (storedEvents[eventType] && storedEvents[eventType].length > 1) { const mergedNodes = new CoreSet_1.CoreSet(); storedEvents[eventType].forEach(([nodesCreated]) => { nodesCreated.forEach((n) => mergedNodes.add(n)); }); //replace stored events with one stored event that has again 2 args, the combined sets storedEvents[eventType] = [[mergedNodes]]; } }); let success = true; for (const [eventType, handler] of processOrder) { if (storedEvents[eventType]) { success = success && (yield Promise.all(storedEvents[eventType].map((args) => { return handler.apply(this, args); })) .then(() => true) .catch((err) => { console.warn('Error whilst processing ' + eventType, err); return false; })); } } this.finalizeProcess(success); }); } static finalizeProcess(success) { if (success) { //if we changed graphs in the process, there may be more events waiting if (EventBatcher_1.eventBatcher.hasBatchedEvents()) { //let's make sure we process those as well before resolving EventBatcher_1.eventBatcher.dispatchBatchedEvents(); } //if we now have work to do if (this.storedEvents) { //do that and come back here later this.processStoredEvents(); } else { //no more storage work to do for sure! let's resolve this.processingPromise.resolve(); this.processingPromise = null; } } else { this.processingPromise.reject(); this.processingPromise = null; } } static getDefaultStore() { return this.defaultStore; } static setDefaultStore(store) { this.defaultStore = store; this.defaultStore.init(); const defaultGraph = store.getDefaultGraph(); if (defaultGraph) { this.setDefaultStorageGraph(defaultGraph); this.setStoreForGraph(store, defaultGraph); } else { // console.warn('Default store did not return a default graph.'); } this.init(); } static setDefaultStorageGraph(graph) { this.defaultStorageGraph = graph; } static setGraphForShapes(graph, ...shapeClasses) { shapeClasses.forEach((shapeClass) => { this.shapesToGraph.set(shapeClass, graph); if (shapeClass['shape']) { if (!this.graphToTargetClasses.has(graph)) { this.graphToTargetClasses.set(graph, new NodeSet_1.NodeSet()); } this.graphToTargetClasses.get(graph).add(shapeClass['shape'].targetClass); //we also add any shape class that extends this shape class //For example, if storage is configured for Thing, then we want to also list all the shapes that extend Thing //because the types of all those super shapes should be pointing towards the same graph (0, ShapeClass_1.getSuperShapesClasses)(shapeClass).forEach(subShape => { this.graphToTargetClasses.get(graph).add(subShape['shape'].targetClass); }); this.nodeShapesToGraph.set(shapeClass['shape'].namedNode, graph); } }); this.init(); } static setStoreForGraph(store, graph) { this.graphToStore.set(graph, store); } static getGraphForStore(store) { for (const [graph, targetStore] of this.graphToStore) { //shapes don't have to be the same instance, but they share the same node if (store['node'] === targetStore['node']) { return graph; } } } static getStores() { return new CoreSet_1.CoreSet([...this.graphToStore.values()]); } /** * Set the target store for instances of these shapes * @param store * @param shapes */ static setStoreForShapes(store, ...shapes) { const graph = store.getDefaultGraph(); this.setStoreForGraph(store, graph); this.setGraphForShapes(graph, ...shapes); } static assignQuadsToGraph(quads, removeFromSet = false) { const map = this.getTargetGraphMap(quads); const alteredNodes = new CoreMap_1.CoreMap(); const movedQuads = new QuadSet_1.QuadSet(); map.forEach((graphQuads, graph) => { graphQuads.forEach((quad) => { if (quad.graph !== graph) { //move the quad to the target graph (both old and new graph will be updated) //this will trigger a QUADS_ALTERED event --> onQuadsAltered quad.moveToGraph(graph); //we also remove the quad from the set it was in, if requested //this prevents moved quads from still being added to the store of the old graph if (removeFromSet) { quads instanceof QuadSet_1.QuadSet ? quads.delete(quad) : quads.splice(quads.indexOf(quad), 1); } movedQuads.add(quad); //also keep track of which nodes had a quad that moved to a different graph if (!alteredNodes.has(quad.subject)) { alteredNodes.set(quad.subject, graph); } } }); }); //now that all quads have been updated, we need to check one more thing //changes in quads MAY have changed which shapes the subject nodes are an instance of //thus the target graph of the whole node may have changed, so: return movedQuads.concat(this.moveAllQuadsOfNodeIfRequired(alteredNodes)); } static moveAllQuadsOfNodeIfRequired(alteredNodes) { const movedQuads = new QuadSet_1.QuadSet(); //for all subjects who have a quad that moved to a different graph alteredNodes.forEach((graph, subjectNode) => { //go over each quad of that node subjectNode.getAllQuads().forEach((quad) => { //and if that quad is not in the same graph as the target graph that we just determined for that node if (quad.graph !== graph) { //then update it quad.moveToGraph(graph); movedQuads.add(quad); } }); }); return movedQuads; } /** * * @returns a promise that resolves when all storage events have been processed. For example shapes that are saved() have been stored and received a permanent URI. */ static promiseUpdated() { return __awaiter(this, void 0, void 0, function* () { if (this.defaultStore) { yield this.defaultStore.init(); } //we wait till all events are dispatched // console.log('awaiting events'); return EventBatcher_1.eventBatcher.promiseDone().then(() => { //if that triggered a storage update if (this.processingPromise) { // console.log('awaiting storage process'); //we will wait for that return this.processingPromise.promise; } }); }); } static onQuadsAltered(quadsCreated, quadsRemoved, baseStoreOnSubject = false, alteration = false) { //quads may have been removed since they have been created and emitted filter that out here let addMap, removeMap; if (quadsCreated && (quadsCreated.size || quadsCreated['length'])) { quadsCreated = quadsCreated.filter((q) => !q.isRemoved); //first see if any new quads need to move to the right graphs (note that this will possibly add "mimicked" quads (with the previous graph as their graph) to quadsRemoved) //true, signals that we want to remove the quads from quadsCreated if they get moved //the new quads will be going through the event loop again and end up here again, but then they will not be removed from quadsCreated this.assignQuadsToGraph(quadsCreated, true); if (baseStoreOnSubject) { addMap = this.getStoreMapForNodes(quadsCreated.getSubjects()); } else { //default: get the right stores based on the graph of the quads addMap = this.getTargetStoreMap(quadsCreated); } } if (quadsRemoved && (quadsRemoved.size || quadsRemoved['length'])) { //TODO: we may not need this baseStoreOnSubject anymore, the second call with "true" param is more accurate? if (baseStoreOnSubject) { removeMap = this.getStoreMapForNodes(quadsRemoved.getSubjects()); } else { //default: get the right stores based on the graph of the quads removeMap = this.getTargetStoreMap(quadsRemoved, true); } } //combine the keys of both maps (which are stores) const stores = [...(addMap ? addMap.keys() : []), ...(removeMap ? removeMap.keys() : [])]; //go over each store that has added/removed quads return Promise.all(stores.map((store) => __awaiter(this, void 0, void 0, function* () { let storeAddQuads, storeRemoveQuads; if (baseStoreOnSubject) { //in case we looked up target stores based on the subject of the quads, // we need to still filter the quads to get only those that are relevant for this store const storeAddSubjects = addMap === null || addMap === void 0 ? void 0 : addMap.get(store); const storeRemoveSubjects = removeMap === null || removeMap === void 0 ? void 0 : removeMap.get(store); storeAddQuads = storeAddSubjects ? quadsCreated.filter((q) => storeAddSubjects.includes(q.subject)) : null; storeRemoveQuads = storeRemoveSubjects ? quadsRemoved.filter((q) => storeRemoveSubjects.includes(q.subject)) : null; } else { storeAddQuads = (addMap === null || addMap === void 0 ? void 0 : addMap.get(store)) || null; storeRemoveQuads = (removeMap === null || removeMap === void 0 ? void 0 : removeMap.get(store)) || null; } // console.log(`updating store ${store?.toString()}. Adding ${storeAddQuads?.size || storeAddQuads?.length}. Removing ${storeRemoveQuads?.size || storeRemoveQuads?.length}`); return store.update(storeAddQuads, storeRemoveQuads); }))) .then((res) => { return res; }) .catch((err) => { console.warn('Error during storage update: ', err); }); } static getGraphForNode(subject, checkShapes = true) { // if (checkShapes && (!subject.isTemporaryNode || (subject.isTemporaryNode && subject.isStoring)) && this.nodeShapesToGraph.size > 0) { // const subjectShapes = NodeShape.getShapesOf(subject,true); // // //see if any of these shapes has a specific target graph // for (const shape of subjectShapes) { // if (this.nodeShapesToGraph.has(shape.namedNode)) { // //currently, the target graph of the very first shape that has a target graph is returned // return this.nodeShapesToGraph.get(shape.namedNode); // } // } // } if (!subject.isTemporaryNode) { for (const [graph, targetClasses] of this.graphToTargetClasses) { if (subject.getAll(rdf_1.rdf.type).some(type => targetClasses.has(type))) { return graph; } } } //if it's not a temporary node, and we have a default graph for permanent storage, then use that if ((!subject.isTemporaryNode || (subject.isTemporaryNode && subject.isStoring)) && this.defaultStorageGraph) { return this.defaultStorageGraph; } //if no shape defined a target graph OR if the node is a temporary node //then use the default graph (which is usually not connected to any store, and just lives in local memory) //this prevents temporary local nodes from being automatically stored return models_1.defaultGraph; } static getDefaultStorageGraph() { return this.defaultStorageGraph || models_1.defaultGraph; } static onClearedProperties(clearProperties) { return __awaiter(this, void 0, void 0, function* () { const subjects = new NodeSet_1.NodeSet(clearProperties.keys()); //get a map of where each of these nodes are stored const storeMap = this.getStoreMapForNodes(subjects); //call on each store to remove the appropriate nodes yield Promise.all([...storeMap.entries()].map(([store, subjects]) => { const storeClearMap = new CoreMap_1.CoreMap(); subjects.forEach((subject) => { const subjectClearMap = new NodeSet_1.NodeSet(); clearProperties.get(subject).forEach(([clearedProperty, quads]) => { //TODO: if we ever need access to the LOCALLY cleared quads in the remote stores, grab & send them from here // However, if we don't, we can reshape the NamedNode model so that quads don't get sent in these events anymore subjectClearMap.add(clearedProperty); }); storeClearMap.set(subject, subjectClearMap); }); return store.clearProperties(storeClearMap); })) .then((res) => { return res; }) .catch((err) => { console.warn('Could not clear properties: ' + err); }); }); } static onRemoveNodes(nodesAndQuads) { return __awaiter(this, void 0, void 0, function* () { //turn all the removed quads back on (as if they were still in the graph) //this allows us to read the properties of the node as they were just before the node was removed. const nodes = new NodeSet_1.NodeSet(); const nodeToQuadsMap = new CoreMap_1.CoreMap(); nodesAndQuads.forEach(([node, quads]) => { quads.turnOn(); nodes.add(node); nodeToQuadsMap.set(node, quads); }); //get a map of where each of these nodes are stored const storeMap = this.getStoreMapForNodes(nodes); //turn the quads back off (they should be removed after all) nodesAndQuads.forEach(([node, quads]) => { quads.turnOff(); }); //call on each store to remove the appropriate nodes yield Promise.all([...storeMap.entries()].map(([store, nodesToRemove]) => { // console.log('removing '+nodesToRemove.length+' nodes from store', store.toString()); let quadsToRemove = new QuadSet_1.QuadSet(); nodesToRemove.forEach((node) => { quadsToRemove = quadsToRemove.concat(nodeToQuadsMap.get(node)); }); return store.removeNodes(nodesToRemove, quadsToRemove); })) .then((res) => { return res; }) .catch((err) => { console.warn('Could not remove nodes from storage: ' + err); }); }); } static onStoreNodes(nodes) { return __awaiter(this, void 0, void 0, function* () { //TODO: no need to convert to QuadSet once we phase out QuadArray const nodesWithTempURIs = nodes.filter((node) => node.uri.indexOf(models_1.NamedNode.TEMP_URI_BASE) === 0); const storeMap = this.getStoreMapForNodes(nodesWithTempURIs); yield Promise.all([...storeMap.entries()].map(([store, temporaryNodes]) => { const nodeUriMap = new CoreMap_1.CoreMap(); temporaryNodes.forEach((node) => { nodeUriMap.set(node, node.uri); }); //let the store determine the URI's for these nodes return store.setURIs(nodeUriMap).then((uriUpdates) => { //and THEN update them (yes this currently needs to be separate because the frontend requests new uri's before sending data,so this URI request should not change any URI's on the backend) uriUpdates.forEach(([oldUri, newUri]) => { const currentNode = models_1.NamedNode.getNamedNode(oldUri); const alreadyExistingNode = models_1.NamedNode.getNamedNode(newUri); if (alreadyExistingNode) { console.warn(`Node with URI ${newUri} already exists in the store. This is an error in the store ${store.toString()}`, currentNode.print(), alreadyExistingNode.print()); return; } //currently, when a node is saved and removed in the same event cycle, it will not be in the store anymore if (currentNode) { currentNode.uri = newUri; } }); }).catch(err => { console.warn(`Error during URI update for store ${store.toString()}: `, err); }); })); //now that their URI's are updated we can indicate that they are no longer temporary nodes //NOTE though that the code below will move the nodes to the right graphs, which will trigger update events (which ACTUALLY stores the nodes) //if in the meantime requests get made that involve this node as a value of a property, then the data of this node will no longer be sent over //even though it may NOT be known yet on the server. If this is a problem, we may want to (somehow) wait with setting temporaryNode to false untill after all the quads are moved AND stored // nodes.forEach((node) => { // // node.isTemporaryNode = false; // //this may need to move to a later point, after quads are stored // //this also resolves the promise that was returned when the node was .saved() // node.isStoring = false; // }); //move all the quads to the right graph. //note that IF this is a new graph, this will trigger onQuadsAltered, which will notify the right stores to store these quads const quads = new QuadSet_1.QuadSet(); nodes.forEach((node) => { node.getAllQuads().forEach((quad) => { quads.add(quad); }); }); this.assignQuadsToGraph(quads); nodes.forEach((node) => { //mark node as no longer temporary node.isTemporaryNode = false; //but do mark it as fully loaded, because locally we must have set all the properties known so far this.fullyLoadedNodes.add(node); node.isStoring = false; }); }); } static getStoreForNode(node) { const graph = this.getGraphForNode(node); return this.getStoreForGraph(graph); } static getStoreForGraph(graph) { return this.graphToStore.get(graph); // if(this.graphToStore.has(graph)) // { // } // return this.defaultStore; } static groupQuadsBySubject(quads) { const subjectsToQuads = new CoreMap_1.CoreMap(); quads.forEach((quad) => { if (!subjectsToQuads.has(quad.subject)) { subjectsToQuads.set(quad.subject, new QuadArray_1.QuadArray()); } subjectsToQuads.get(quad.subject).push(quad); }); return subjectsToQuads; } static getTargetGraphMap(quads) { const graphMap = new CoreMap_1.CoreMap(); const quadsBySubject = this.groupQuadsBySubject(quads); quadsBySubject.forEach((quads, subjectNode) => { const targetGraph = this.getGraphForNode(subjectNode); if (!graphMap.has(targetGraph)) { graphMap.set(targetGraph, new QuadArray_1.QuadArray()); } // try // { // graphMap.set(targetGraph,new QuadArray(...graphMap.get(targetGraph).concat(quads))); // } catch (e) { // console.log(e); const t = graphMap.get(targetGraph); quads.forEach(q => { t.push(q); }); // const t2 = t.concat(quads); // const t3 = new QuadArray(); // t2.forEach((q) => t3.push(q)); // graphMap.set(targetGraph,t3); // } }); return graphMap; } static getStoreMapForNodes(nodes) { return this.getStoreMapForIGraphObjects(nodes); } static getStoreMapForShapes(shapes) { return this.getStoreMapForIGraphObjects(shapes); } static getShapeToStoreMap() { return this.shapesToGraph.map(graph => { return this.getStoreForGraph(graph); }); } static getStoreMapForIGraphObjects(objects) { const storeMap = new CoreMap_1.CoreMap(); objects.forEach((object) => { const store = this.getStoreForNode(object.node || object); //if store is null, this means no store is observing this node. This will usually happen for the default graph which contains temporary nodes if (store) { if (!storeMap.has(store)) { storeMap.set(store, []); } storeMap.get(store).push(object); } }); return storeMap; } static getTargetStoreMap(quads, basedOnSubject = false) { const storeMap = new CoreMap_1.CoreMap(); quads.forEach((quad) => { //if basedOnSubject and the quad is not in the default graph, then we find the store for the subject of the quad //this is used for removing properties of a node, who's quads are in the default graph but should be stored in the graph of the default store // const store = basedOnSubject && quad.graph === defaultGraph ? this.getStoreForNode(quad.subject) : this.getStoreForGraph(quad.graph); //if store is null, this means no store is observing this quad. This will usually happen for the default graph which contains temporary nodes //UPDATE2: the above caused issues when saving a new shape/node, because the new quads were MOVED (removed from old graph) and then stored in the target graph, // but the removed quads were also sent to the same store with the code above, causing nothing to be saved const store = this.getStoreForGraph(quad.graph); if (store) { if (!storeMap.has(store)) { storeMap.set(store, new QuadArray_1.QuadArray()); } storeMap.get(store).push(quad); } }); return storeMap; } static setURIs(nodeUriMap) { return __awaiter(this, void 0, void 0, function* () { //organise the nodes by their appropriate store //and because these are nodes that are about to receive a URI so they can be stored //we temporarily make them non-temporary :) //this way the store map will contain the right target store for STORED nodes //so that THAT store can determine the URI const nodes = new NodeSet_1.NodeSet(); nodeUriMap.forEach((currentEnvironmentURI, node) => { node['tmp'] = node.isTemporaryNode; node.isTemporaryNode = false; nodes.add(node); }); const storeMap = this.getStoreMapForNodes(nodes); nodes.forEach((node) => { node.isTemporaryNode = node['tmp']; }); const promises = []; //let each store update the URI's storeMap.forEach((nodes, store) => { const storeNodeUriMap = new CoreMap_1.CoreMap(); nodes.forEach((node) => { storeNodeUriMap.set(node, nodeUriMap.get(node)); }); promises.push(store.setURIs(storeNodeUriMap)); }); //combine the results to return an array of old to new URI's return Promise.all(promises).then((results) => { const combinedResults = [].concat(...results); return combinedResults; }); }); } static update(toAdd, toRemove) { // let storeMap = this.getStoreMapForNodes(toRemove.getSubjects()); // storeMap.forEach((nodes, store) => { // let quads = toRemove.filter(q => nodes.includes(q.subject)); // store.deleteMultiple(quads); // // }); return this.onQuadsAltered(toAdd, toRemove, true, true); } static clearProperties(subjectToPredicates) { const subjects = [...subjectToPredicates.keys()]; const storeMap = this.getStoreMapForNodes(subjects); const promises = []; storeMap.forEach((nodes, store) => { const map = new CoreMap_1.CoreMap(nodes.map((node) => { return [node, subjectToPredicates.get(node)]; })); promises.push(store.clearProperties(map)); }); return Promise.all(promises).then((results) => { return results.every((result) => result === true); }); } static loadShapes(shapeSet, shapeOrRequest, byPassCache = false) { const nodes = shapeSet.getNodes(); if (!byPassCache) { const cachedResult = this.nodesAreLoaded(nodes, shapeOrRequest); if (cachedResult) { //return the load promise that's already in progress, // or a promise that resolves to true straight away if it's already been loaded return cachedResult === true ? Promise.resolve(true) : cachedResult; } } const storeMap = this.getStoreMapForShapes(shapeSet); const storePromises = []; storeMap.map((shapes, store) => { storePromises.push(store.loadShapes(new ShapeSet_1.ShapeSet(shapes), shapeOrRequest)); }); const loadPromise = Promise.all(storePromises).then((results) => { // return new QuadArray(); const quads = new QuadArray_1.QuadArray(); results.forEach((result) => { if (result instanceof QuadArray_1.QuadArray) { quads.push(...result); } }); //update the cache to indicate these property shapes have finished loading for these nodes this.setNodesLoaded(nodes, shapeOrRequest, true); return quads; }); //update the cache to indicate these property shapes are being loaded for these nodes LinkedStorage.setNodesLoaded(nodes, shapeOrRequest, loadPromise); return loadPromise; } /** * Loads the requested Shape(s) from storage for a specific node. * To form a request see the LinkedDataRequest interface * Returns a promise that resolves when the loading has completed. * Requests are cached so the second time you request the same data you will get the same answer. Use byPassCache if you want to ensure the data is loaded again. * The returned promise resolves to null if no target store was found for this node (the app may not have a defaultStore set up then) * @param shapeInstance * @param shapeOrRequest * @param byPassCache */ static loadShape(shapeInstance, shapeOrRequest, byPassCache = false) { //if no shape is requested then we automatically request all properties of the shape if (!shapeOrRequest) { //TODO: maybe we can optimise requests by not sending all the shapes and letting the backend fill in the property shapes shapeOrRequest = [...shapeInstance.nodeShape.getPropertyShapes()]; //also add the property shapes of all classes that extend this shape const shapeClass = (0, ShapeClass_1.getShapeClass)(shapeInstance.nodeShape.namedNode); const superShapes = (0, ShapeClass_1.getSuperShapesClasses)(shapeClass); superShapes.forEach((superShapeClass) => { shapeOrRequest.push(...superShapeClass.shape.getPropertyShapes()); }); } //@TODO: optimise the shapeOrRequest. Currently if the same property is requested twice, but once with more sub properties, then both will be requested. // This can be merged into 1 shape request because the longer one automatically loads the shorter one const node = shapeInstance.node; if (!byPassCache) { const cachedResult = this.isLoaded(node, shapeOrRequest); if (cachedResult) { //return the load promise that's already in progress, // or a promise that resolves to true straight away if it's already been loaded return cachedResult === true ? Promise.resolve(true) : cachedResult; } } const store = this.getStoreForNode(shapeInstance.namedNode); if (store) { const promise = store.loadShape(shapeInstance, shapeOrRequest).then((res) => { //indicate that these property shapes have finished loading for this node this.setNodeLoaded(node, shapeOrRequest); return res; }); //indicate that these property shapes are being loaded for this node this.setNodeLoaded(node, shapeOrRequest, promise); return promise; } else { //NOTE: if we ever need to know that we could not find a store to load this node from //then we could setNodeLoaded to false, and we need to account for the possibility of a false value in other places //any place using cached results would need to differentiate between null and false this.setNodeLoaded(node, shapeOrRequest); return Promise.resolve(null); } } static nodesAreLoaded(nodes, dataRequest) { const stillLoading = []; if (!nodes.every((node) => { const cached = this.isLoaded(node, dataRequest); if (!cached) { return false; } if (cached !== true) { //then it's a promise, this node is still loading stillLoading.push(node); } return true; })) { return false; } return stillLoading ? Promise.all(stillLoading) : true; } static setFullyLoaded(node) { this.fullyLoadedNodes.add(node); } static isLoaded(node, dataRequest) { //temporary nodes are always local and fully known if (node instanceof models_1.NamedNode && node.isTemporaryNode) { return true; } if (this.fullyLoadedNodes.has(node)) { return true; } if (!this.nodeToPropertyRequests.has(node)) { return false; } const propertiesRequested = this.nodeToPropertyRequests.get(node); //return true if every top level property request has been loaded for this source const stillLoading = []; if (!dataRequest.every((propertyRequest) => { let propertyReqResult; //for sub requests if (Array.isArray(propertyRequest)) { //first check that property shape (that is the first entry) is loaded (same as for non-sub requests) propertyReqResult = propertiesRequested.get(propertyRequest[0].namedNode); //then let check if the subRequest is also loaded for each currently available value if (propertyReqResult) { //if not every currently loaded value for this property-shape is loaded, then the subRequest is not loaded if (!propertyRequest[0].resolveFor(node).every((valueNode) => { const subRequestLoaded = this.isLoaded(valueNode, propertyRequest[1]); if (!subRequestLoaded) { return false; } //if the sub request is still loading that's fine, we add it to the array of promises to wait for if (subRequestLoaded !== true) { stillLoading.push(subRequestLoaded); } return true; })) { propertyReqResult = false; } } } else { propertyReqResult = propertiesRequested.get(propertyRequest.namedNode); } if (!propertyReqResult) { //not every propertyRequest is loaded, return false, which stops the every() loop and resolves it to false return false; } if (propertyReqResult !== true) { stillLoading.push(propertyReqResult); } //if not false, continue to evaluate the next propertyRequest return true; })) { return false; } //all propertyRequests had an entry, if some are still loading, return the promise that resolves when they're all loaded //else return true (everything is loaded) return stillLoading.length > 0 ? Promise.all(stillLoading) : true; } static getPredicateToPropertyShapesMap() { if (!this.propShapeMap) { this.propShapeMap = new Map(); SHACL_1.PropertyShape.getLocalInstances().forEach((propertyShape) => { if (!this.propShapeMap.has(propertyShape.path)) { this.propShapeMap.set(propertyShape.path, []); } this.propShapeMap.get(propertyShape.path).push(propertyShape); }); } return this.propShapeMap; } /** * Sets all the property paths of the subject nodes to be loaded * Handy for example when the server returned data already and you don't want the automatic loading to kick in. * WARNING: this assumes that ALL the values of each subject-predicate pair are loaded. * If the server returned just one and there are more, using this method means the other values will not automatically be loaded. */ static setQuadsLoaded(quads) { const propShapeMap = this.getPredicateToPropertyShapesMap(); //build a map of subject to property shapes const subjectToPropShapes = new Map(); quads.forEach((quad) => { if (!subjectToPropShapes.has(quad.subject)) { subjectToPropShapes.set(quad.subject, []); } const currentPropShapes = subjectToPropShapes.get(quad.subject); //get all the property shapes that match this predicate and add them to the property shapes of this subject if (propShapeMap.has(quad.predicate)) { propShapeMap.get(quad.predicate).forEach((propShape) => currentPropShapes.push(propShape)); } }); subjectToPropShapes.forEach((propertyShapes, subject) => { this.setNodeLoaded(subject, propertyShapes); }); } static setNodesLoaded(nodes, dataRequest, requestState = true) { nodes.forEach((source) => { this.setNodeLoaded(source, dataRequest, requestState); }); } static setNodeLoaded(node, request, requestState = true) { if (!this.nodeToPropertyRequests.get(node)) { this.nodeToPropertyRequests.set(node, new CoreMap_1.CoreMap()); } const requestedProperties = this.nodeToPropertyRequests.get(node); request.map((propertyRequest) => { if (Array.isArray(propertyRequest)) { //propertyRequest is of the shape [propertyShape,subRequest] //update the cache for the property-shapes that regard this source requestedProperties.set(propertyRequest[0].namedNode, requestState); //if loading has finished if (requestState === true) { //then resolve the property shape for this node (so follow the property shape from this node) //then update the cache to indicate that the subRequest has been loaded // for each of the nodes you get to from that property shape this.setNodesLoaded(propertyRequest[0].resolveFor(node), propertyRequest[1], requestState); } } else { //propertyRequest is a PropertyShape requestedProperties.set(propertyRequest.namedNode, requestState); } }); } } exports.LinkedStorage = LinkedStorage; LinkedStorage.graphToStore = new CoreMap_1.CoreMap(); LinkedStorage.shapesToGraph = new CoreMap_1.CoreMap(); LinkedStorage.nodeShapesToGraph = new CoreMap_1.CoreMap(); LinkedStorage.graphToTargetClasses = new CoreMap_1.CoreMap(); LinkedStorage.nodeToPropertyRequests = new CoreMap_1.CoreMap(); LinkedStorage.fullyLoadedNodes = new CoreSet_1.CoreSet(); //# sourceMappingURL=LinkedStorage.js.map