UNPKG

@endo/compartment-mapper

Version:

The compartment mapper assembles Node applications in a sandbox

352 lines (311 loc) 8.66 kB
/** * Provides {@link GenericGraph} and {@link makeShortestPath}. * * Portions adapted from * {@link https://github.com/datavis-tech/graph-data-structure graph-data-structure}, * which is Copyright (c) 2016 Curran Kelleher and licensed under the MIT * License. * * @module */ /** * @import {TraversalContext} from './types/generic-graph.js'; */ const { stringify: q } = JSON; /** * Remove the node with the minimum weight from the priority queue. * * Performs linear search. * @template [T=string] * @param {TraversalContext<T>} tracks * @returns {T|undefined} */ const extractMin = ({ distances, queue }) => { let min = Infinity; /** @type {T|undefined} */ let minNode; queue.forEach(node => { const nodeWeight = distances.get(node) ?? Infinity; if (nodeWeight < min) { min = nodeWeight; minNode = node; } }); if (minNode === undefined) { queue.clear(); return undefined; } queue.delete(minNode); return minNode; }; /** * Update context with the new distance to the target node if the distance * through the source node is shorter than the current distance. * * @template [T=string] * @param {GenericGraph<T>} graph * @param {TraversalContext<NoInfer<T>>} context * @param {NoInfer<T>} source * @param {NoInfer<T>} target */ const relax = (graph, { distances, predecessors }, source, target) => { const number = graph.getEdgeWeight(source, target); const distanceSource = distances.get(source); const distanceTarget = distances.get(target); assert( distanceSource !== undefined, `Missing distance for source ${q(source)}`, ); assert( distanceTarget !== undefined, `Missing distance for target ${q(target)} target`, ); if (distanceTarget > distanceSource + number) { distances.set(target, distanceSource + number); predecessors.set(target, source); } }; /** * Assembles the shortest path by traversing the * predecessor subgraph from destination to source. * * @template [T=string] * @param {TraversalContext<NoInfer<T>>} context Traversal context object * @param {NoInfer<T>} source Source node * @param {NoInfer<T>} target Destination node * @returns {[T, T, ...T[]]} Nodes from `source` to `target` inclusive * @throws If no path is found * @throws If the path has less than two nodes */ const getPath = ({ predecessors }, source, target) => { /** @type {T[]} */ const nodeList = []; let node = target; while (predecessors.has(node)) { const currentNode = /** @type {T} */ (predecessors.get(node)); nodeList.push(node); node = currentNode; } assert.equal(node, source, `No path found from ${q(source)} to ${q(target)}`); nodeList.push(node); assert( nodeList.length >= 2, `The path from ${source} to ${target} should have a least two nodes`, ); return /** @type {[T, T, ...T[]]} */ (nodeList.reverse()); }; /** * @template [T=string] The type of nodes in the graph * * A generic graph implementation with edge weights. * * Edge weights are assumed to be non-negative numbers (including `Infinity`) */ export class GenericGraph { /** * @type {Set<T>} */ #nodes; /** * @type {Map<T, Set<T>>} */ #edges; /** * @type {Map<T, Map<T, number>>} */ #edgeWeights; /** * Initializes internal data structures. */ constructor() { this.#edgeWeights = new Map(); this.#edges = new Map(); this.#nodes = new Set(); } /** * Returns a shallow copy of the `Set` of nodes in the graph. */ get nodes() { return new Set(this.#nodes); } /** * Adds a node to the graph. * If node was already added, this function does nothing. * If node was not already added, this function sets up an empty adjacency list. * @param {T} node Node to add * @returns {this} This graph instance */ addNode(node) { if (!this.#nodes.has(node)) { this.#nodes.add(node); } if (!this.#edges.has(node)) { this.#edges.set(node, new Set()); } return this; } /** * Removes a node from the graph. * Also removes incoming and outgoing edges. * @param {T} node * @returns {this} */ removeNode(node) { this.#edges.delete(node); this.#nodes.delete(node); for (const adjacentNodes of this.#edges.values()) { adjacentNodes.delete(node); } return this; } /** * Gets the adjacent nodes set for the given node. * @param {T} node * @returns {Set<T>|undefined} */ adjacent(node) { return this.#edges.get(node); } /** * Sets the weight of the given edge between `source` and `target`. * * @param {T} source Source node * @param {T} target Target node * @param {number} weight New edge weight * @returns {this} */ setEdgeWeight(source, target, weight) { if (!this.#edgeWeights.has(source)) { this.#edgeWeights.set(source, new Map()); } const weights = /** @type {Map<T, number>} */ ( this.#edgeWeights.get(source) ); weights.set(target, weight); return this; } /** * Gets the weight of the given edge between `source` and `target`. * * @param {T} source Source node * @param {T} target Target node * @returns {number} Edge weight from source to target */ getEdgeWeight(source, target) { const weight = this.#edgeWeights.get(source)?.get(target); if (weight === undefined) { throw new ReferenceError( `Edge weight from ${q(source)} to ${q(target)} is not set`, ); } return weight; } /** * Adds an edge from the `source` node to `target` node. * * This method will create the `source` and `target` node(s) if they do not * already exist. * * If {@link T `T`} is an object, the comparison is by-reference. * * @param {T} source Source node * @param {T} target Target node * @param {number} weight Edge weight from source to target * @returns {this} This graph instance */ addEdge(source, target, weight) { this.addNode(source); this.addNode(target); const adjacentNodes = this.adjacent(source); assert(adjacentNodes, `Source ${q(source)} should have adjacent nodes`); adjacentNodes.add(target); this.setEdgeWeight(source, target, weight); return this; } /** * Removes the edge from the `source` node to `target` node. * Does not remove the nodes themselves. * Does nothing if the edge does not exist. * @param {T} source * @param {T} target * @returns {this} */ removeEdge(source, target) { this.#edges.get(source)?.delete(target); return this; } /** * Returns true if there is an edge from the `source` node to `target` node. * @param {T} source * @param {T} target * @returns {boolean} */ hasEdge(source, target) { return this.#edges.get(source)?.has(target) ?? false; } } /** * Dijkstra's algorithm for shortest paths in a graph. * @template [T=string] The type of nodes in the graph * @param {GenericGraph<T>} graph * @param {T} source * @param {T} target * @returns {TraversalContext<T>} */ const dijkstra = (graph, source, target) => { const { nodes } = graph; /** @type {TraversalContext<T>} */ const context = { distances: new Map(), predecessors: new Map(), queue: new Set(), }; const { queue, distances } = context; for (const node of nodes) { distances.set(node, Infinity); } assert( distances.get(source) === Infinity, `Source ${q(source)} is not in the graph`, ); assert( distances.get(target) === Infinity, `Target ${q(target)} is not in the graph`, ); distances.set(source, 0); for (const node of nodes) { queue.add(node); } while (queue.size !== 0) { const node = extractMin(context); if (node === undefined) { return context; } const adjacent = graph.adjacent(node); if (adjacent) { for (const edge of adjacent) { relax(graph, context, node, edge); } } } return context; }; /** * Returns a function which uses Dijkstra's shortest path algorithm to compute * the shortest path from `source` to `destination` in the given `graph`. * * @template [T=string] * @param {GenericGraph<T>} graph Graph to use */ export const makeShortestPath = graph => { /** * @param {NoInfer<T>} source Source node * @param {NoInfer<T>} target Target node * @returns {[T, T, ...T[]]} Nodes from `source` to `target` inclusive (minimum of two nodes) */ const shortestPath = (source, target) => { const context = dijkstra(graph, source, target); return getPath(context, source, target); }; return shortestPath; };