@rushstack/operation-graph
Version:
Library for managing and executing operations in a directed acyclic graph.
89 lines • 4.04 kB
JavaScript
;
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateCriticalPathLength = exports.calculateShortestPath = exports.calculateCriticalPathLengths = void 0;
/**
* For every operation in the input, computes the length of the longest chain of operations that depend on it.
* This value is stored as `operation.criticalPathLength`.
*/
function calculateCriticalPathLengths(operations) {
// Clone the set of operations as an array, so that we can sort it.
const queue = Array.from(operations);
// Create a collection for detecting visited nodes
const cycleDetectorStack = new Set();
for (const operation of queue) {
calculateCriticalPathLength(operation, cycleDetectorStack);
}
return queue;
}
exports.calculateCriticalPathLengths = calculateCriticalPathLengths;
/**
* Calculates the shortest path from `startOperation` to `endOperation`.
* Used when printing out circular dependencies.
*/
function calculateShortestPath(startOperation, endOperation) {
// Map of each operation to the most optimal parent
const parents = new Map([[endOperation, undefined]]);
let finalParent;
// Run a breadth-first search to find the shortest path between the start and end operations
outer: for (const [operation] of parents) {
for (const consumer of operation.consumers) {
// Since this is a breadth-first traversal, the first encountered path to a given node
// will be tied for shortest, so only the first encountered path needs to be tracked
if (!parents.has(consumer)) {
parents.set(consumer, operation);
}
if (consumer === startOperation) {
finalParent = operation;
break outer;
}
}
}
if (!finalParent) {
throw new Error(`Could not find a path from "${startOperation.name}" to "${endOperation.name}"`);
}
// Walk back up the path from the end operation to the start operation
let currentOperation = finalParent;
const path = [startOperation];
while (currentOperation !== undefined) {
path.push(currentOperation);
currentOperation = parents.get(currentOperation);
}
return path;
}
exports.calculateShortestPath = calculateShortestPath;
/**
* Perform a depth-first search to find critical path length to the provided operation.
* Cycle detection comes at minimal additional cost.
*/
function calculateCriticalPathLength(operation, dependencyChain) {
if (dependencyChain.has(operation)) {
// Ensure we have the shortest path to the cycle
const shortestPath = calculateShortestPath(operation, operation);
throw new Error('A cyclic dependency was encountered:\n ' +
shortestPath.map((visitedTask) => visitedTask.name).join('\n -> '));
}
let { criticalPathLength } = operation;
if (criticalPathLength !== undefined) {
// This has been visited already
return criticalPathLength;
}
criticalPathLength = 0;
if (operation.consumers.size) {
dependencyChain.add(operation);
for (const consumer of operation.consumers) {
criticalPathLength = Math.max(criticalPathLength, calculateCriticalPathLength(consumer, dependencyChain));
}
dependencyChain.delete(operation);
}
// Include the contribution from the current operation
criticalPathLength += operation.weight ?? 1;
// Record result
operation.criticalPathLength = criticalPathLength;
// Directly writing operations to an output collection here would yield a topological sorted set
// However, we want a bit more fine-tuning of the output than just the raw topology
return criticalPathLength;
}
exports.calculateCriticalPathLength = calculateCriticalPathLength;
//# sourceMappingURL=calculateCriticalPath.js.map