345 lines
10 KiB
JavaScript
345 lines
10 KiB
JavaScript
"use strict";
|
|||
|
|
|
||
|
|
exports.__esModule = true;
|
||
|
|
exports.Graph = void 0;
|
||
|
|
var _TopSort = require("./TopSort");
|
||
|
|
var _ProcessableValue = require("./ProcessableValue");
|
||
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright (c) 2024 Handsoncode. All rights reserved.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Provides directed graph structure.
|
||
|
|
*
|
||
|
|
* Idea for performance improvement:
|
||
|
|
* - use Set<Node>[] instead of NodeId[][] for edgesSparseArray
|
||
|
|
*/
|
||
|
|
class Graph {
|
||
|
|
constructor(dependencyQuery) {
|
||
|
|
this.dependencyQuery = dependencyQuery;
|
||
|
|
/**
|
||
|
|
* A sparse array. The value nodesSparseArray[n] exists if and only if node n is in the graph.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.nodesSparseArray = [];
|
||
|
|
/**
|
||
|
|
* A sparse array. The value edgesSparseArray[n] exists if and only if node n is in the graph.
|
||
|
|
* The edgesSparseArray[n] is also a sparse array. It may contain removed nodes. To make sure check nodesSparseArray.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.edgesSparseArray = [];
|
||
|
|
/**
|
||
|
|
* A mapping from node to its id. The value nodesIds.get(node) exists if and only if node is in the graph.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.nodesIds = new Map();
|
||
|
|
/**
|
||
|
|
* A ProcessableValue object.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.dirtyAndVolatileNodeIds = new _ProcessableValue.ProcessableValue({
|
||
|
|
dirty: [],
|
||
|
|
volatile: []
|
||
|
|
}, r => this.processDirtyAndVolatileNodeIds(r));
|
||
|
|
/**
|
||
|
|
* A set of node ids. The value infiniteRangeIds.get(nodeId) exists if and only if node is in the graph.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.infiniteRangeIds = new Set();
|
||
|
|
/**
|
||
|
|
* A dense array. It may contain duplicates and removed nodes.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this.changingWithStructureNodeIds = [];
|
||
|
|
this.nextId = 0;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Iterate over all nodes the in graph
|
||
|
|
*/
|
||
|
|
getNodes() {
|
||
|
|
return this.nodesSparseArray.filter(node => node !== undefined);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Checks whether a node is present in graph
|
||
|
|
*
|
||
|
|
* @param node - node to check
|
||
|
|
*/
|
||
|
|
hasNode(node) {
|
||
|
|
return this.nodesIds.has(node);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Checks whether exists edge between nodes. If one or both of nodes are not present in graph, returns false.
|
||
|
|
*
|
||
|
|
* @param fromNode - node from which edge is outcoming
|
||
|
|
* @param toNode - node to which edge is incoming
|
||
|
|
*/
|
||
|
|
existsEdge(fromNode, toNode) {
|
||
|
|
const fromId = this.getNodeId(fromNode);
|
||
|
|
const toId = this.getNodeId(toNode);
|
||
|
|
if (fromId === undefined || toId === undefined) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return this.edgesSparseArray[fromId].includes(toId);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns nodes adjacent to given node. May contain removed nodes.
|
||
|
|
*
|
||
|
|
* @param node - node to which adjacent nodes we want to retrieve
|
||
|
|
*
|
||
|
|
* Idea for performance improvement:
|
||
|
|
* - return an array instead of set
|
||
|
|
*/
|
||
|
|
adjacentNodes(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
throw this.missingNodeError(node);
|
||
|
|
}
|
||
|
|
return new Set(this.edgesSparseArray[id].filter(id => id !== undefined).map(id => this.nodesSparseArray[id]));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns number of nodes adjacent to given node. Contrary to adjacentNodes(), this method returns only nodes that are present in graph.
|
||
|
|
*
|
||
|
|
* @param node - node to which adjacent nodes we want to retrieve
|
||
|
|
*/
|
||
|
|
adjacentNodesCount(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
throw this.missingNodeError(node);
|
||
|
|
}
|
||
|
|
return this.fixEdgesArrayForNode(id).length;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Adds node to a graph
|
||
|
|
*
|
||
|
|
* @param node - a node to be added
|
||
|
|
*/
|
||
|
|
addNodeAndReturnId(node) {
|
||
|
|
const idOfExistingNode = this.nodesIds.get(node);
|
||
|
|
if (idOfExistingNode !== undefined) {
|
||
|
|
return idOfExistingNode;
|
||
|
|
}
|
||
|
|
const newId = this.nextId;
|
||
|
|
this.nextId++;
|
||
|
|
this.nodesSparseArray[newId] = node;
|
||
|
|
this.edgesSparseArray[newId] = [];
|
||
|
|
this.nodesIds.set(node, newId);
|
||
|
|
return newId;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Adds edge between nodes.
|
||
|
|
*
|
||
|
|
* The nodes had to be added to the graph before, or the error will be raised
|
||
|
|
*
|
||
|
|
* @param fromNode - node from which edge is outcoming
|
||
|
|
* @param toNode - node to which edge is incoming
|
||
|
|
*/
|
||
|
|
addEdge(fromNode, toNode) {
|
||
|
|
const fromId = this.getNodeIdIfNotNumber(fromNode);
|
||
|
|
const toId = this.getNodeIdIfNotNumber(toNode);
|
||
|
|
if (fromId === undefined) {
|
||
|
|
throw this.missingNodeError(fromNode);
|
||
|
|
}
|
||
|
|
if (toId === undefined) {
|
||
|
|
throw this.missingNodeError(toNode);
|
||
|
|
}
|
||
|
|
if (this.edgesSparseArray[fromId].includes(toId)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.edgesSparseArray[fromId].push(toId);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes node from graph
|
||
|
|
*/
|
||
|
|
removeNode(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
throw this.missingNodeError(node);
|
||
|
|
}
|
||
|
|
if (this.edgesSparseArray[id].length > 0) {
|
||
|
|
this.edgesSparseArray[id].forEach(adjacentId => this.dirtyAndVolatileNodeIds.rawValue.dirty.push(adjacentId));
|
||
|
|
this.dirtyAndVolatileNodeIds.markAsModified();
|
||
|
|
}
|
||
|
|
const dependencies = this.removeDependencies(node);
|
||
|
|
delete this.nodesSparseArray[id];
|
||
|
|
delete this.edgesSparseArray[id];
|
||
|
|
this.infiniteRangeIds.delete(id);
|
||
|
|
this.nodesIds.delete(node);
|
||
|
|
return dependencies;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes edge between nodes.
|
||
|
|
*/
|
||
|
|
removeEdge(fromNode, toNode) {
|
||
|
|
const fromId = this.getNodeIdIfNotNumber(fromNode);
|
||
|
|
const toId = this.getNodeIdIfNotNumber(toNode);
|
||
|
|
if (fromId === undefined) {
|
||
|
|
throw this.missingNodeError(fromNode);
|
||
|
|
}
|
||
|
|
if (toId === undefined) {
|
||
|
|
throw this.missingNodeError(toNode);
|
||
|
|
}
|
||
|
|
const indexOfToId = this.edgesSparseArray[fromId].indexOf(toId);
|
||
|
|
if (indexOfToId === -1) {
|
||
|
|
throw new Error('Edge does not exist');
|
||
|
|
}
|
||
|
|
delete this.edgesSparseArray[fromId][indexOfToId];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes edge between nodes if it exists.
|
||
|
|
*/
|
||
|
|
removeEdgeIfExists(fromNode, toNode) {
|
||
|
|
const fromId = this.getNodeId(fromNode);
|
||
|
|
const toId = this.getNodeId(toNode);
|
||
|
|
if (fromId === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (toId === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const indexOfToId = this.edgesSparseArray[fromId].indexOf(toId);
|
||
|
|
if (indexOfToId === -1) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
delete this.edgesSparseArray[fromId][indexOfToId];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sorts the whole graph topologically. Nodes that are on cycles are kept separate.
|
||
|
|
*/
|
||
|
|
topSortWithScc() {
|
||
|
|
return this.getTopSortedWithSccSubgraphFrom(this.getNodes(), () => true, () => {});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sorts the graph topologically. Nodes that are on cycles are kept separate.
|
||
|
|
*
|
||
|
|
* @param modifiedNodes - seed for computation. The algorithm assumes that only these nodes have changed since the last run.
|
||
|
|
* @param operatingFunction - recomputes value of a node, and returns whether a change occurred
|
||
|
|
* @param onCycle - action to be performed when node is on cycle
|
||
|
|
*/
|
||
|
|
getTopSortedWithSccSubgraphFrom(modifiedNodes, operatingFunction, onCycle) {
|
||
|
|
const topSortAlgorithm = new _TopSort.TopSort(this.nodesSparseArray, this.edgesSparseArray);
|
||
|
|
const modifiedNodesIds = modifiedNodes.map(node => this.getNodeId(node)).filter(id => id !== undefined);
|
||
|
|
return topSortAlgorithm.getTopSortedWithSccSubgraphFrom(modifiedNodesIds, operatingFunction, onCycle);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Marks node as volatile.
|
||
|
|
*/
|
||
|
|
markNodeAsVolatile(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.dirtyAndVolatileNodeIds.rawValue.volatile.push(id);
|
||
|
|
this.dirtyAndVolatileNodeIds.markAsModified();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Marks node as dirty.
|
||
|
|
*/
|
||
|
|
markNodeAsDirty(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.dirtyAndVolatileNodeIds.rawValue.dirty.push(id);
|
||
|
|
this.dirtyAndVolatileNodeIds.markAsModified();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns an array of nodes that are marked as dirty and/or volatile.
|
||
|
|
*/
|
||
|
|
getDirtyAndVolatileNodes() {
|
||
|
|
return this.dirtyAndVolatileNodeIds.getProcessedValue();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clears dirty nodes.
|
||
|
|
*/
|
||
|
|
clearDirtyNodes() {
|
||
|
|
this.dirtyAndVolatileNodeIds.rawValue.dirty = [];
|
||
|
|
this.dirtyAndVolatileNodeIds.markAsModified();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Marks node as changingWithStructure.
|
||
|
|
*/
|
||
|
|
markNodeAsChangingWithStructure(node) {
|
||
|
|
const id = this.getNodeId(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.changingWithStructureNodeIds.push(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Marks all nodes marked as changingWithStructure as dirty.
|
||
|
|
*/
|
||
|
|
markChangingWithStructureNodesAsDirty() {
|
||
|
|
if (this.changingWithStructureNodeIds.length <= 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.dirtyAndVolatileNodeIds.rawValue.dirty = [...this.dirtyAndVolatileNodeIds.rawValue.dirty, ...this.changingWithStructureNodeIds];
|
||
|
|
this.dirtyAndVolatileNodeIds.markAsModified();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Marks node as infinite range.
|
||
|
|
*/
|
||
|
|
markNodeAsInfiniteRange(node) {
|
||
|
|
const id = this.getNodeIdIfNotNumber(node);
|
||
|
|
if (id === undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.infiniteRangeIds.add(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns an array of nodes marked as infinite ranges
|
||
|
|
*/
|
||
|
|
getInfiniteRanges() {
|
||
|
|
return [...this.infiniteRangeIds].map(id => ({
|
||
|
|
node: this.nodesSparseArray[id],
|
||
|
|
id
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns the internal id of a node.
|
||
|
|
*/
|
||
|
|
getNodeId(node) {
|
||
|
|
return this.nodesIds.get(node);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
*/
|
||
|
|
getNodeIdIfNotNumber(node) {
|
||
|
|
return typeof node === 'number' ? node : this.nodesIds.get(node);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes invalid neighbors of a given node from the edges array and returns adjacent nodes for the input node.
|
||
|
|
*/
|
||
|
|
fixEdgesArrayForNode(id) {
|
||
|
|
const adjacentNodeIds = this.edgesSparseArray[id];
|
||
|
|
this.edgesSparseArray[id] = adjacentNodeIds.filter(adjacentId => adjacentId !== undefined && this.nodesSparseArray[adjacentId]);
|
||
|
|
return this.edgesSparseArray[id];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Removes edges from the given node to its dependencies based on the dependencyQuery function.
|
||
|
|
*/
|
||
|
|
removeDependencies(node) {
|
||
|
|
const dependencies = this.dependencyQuery(node);
|
||
|
|
dependencies.forEach(([_, dependency]) => {
|
||
|
|
this.removeEdgeIfExists(dependency, node);
|
||
|
|
});
|
||
|
|
return dependencies;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* processFn for dirtyAndVolatileNodeIds ProcessableValue instance
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
processDirtyAndVolatileNodeIds({
|
||
|
|
dirty,
|
||
|
|
volatile
|
||
|
|
}) {
|
||
|
|
return [...new Set([...dirty, ...volatile])].map(id => this.nodesSparseArray[id]).filter(node => node !== undefined);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns error for missing node.
|
||
|
|
*/
|
||
|
|
missingNodeError(node) {
|
||
|
|
return new Error(`Unknown node ${node}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
exports.Graph = Graph;
|