86 lines
3.1 KiB
JavaScript
86 lines
3.1 KiB
JavaScript
/**
|
|
* Detects nodes that are part of circular dependency chains.
|
|
*
|
|
* Uses a depth-first search (DFS) algorithm with three-color marking to
|
|
* identify cycles in the dependency graph. When a cycle is detected, all nodes
|
|
* in that cycle are added to the result set.
|
|
*
|
|
* The algorithm tracks three states for each node:
|
|
*
|
|
* - Not visited: Node hasn't been processed yet
|
|
* - Visiting: Currently in the DFS path (gray in three-color marking)
|
|
* - Visited: Completely processed (black in three-color marking).
|
|
*
|
|
* A cycle is detected when we encounter a node that is already in the
|
|
* "visiting" state, meaning we've found a back edge in the graph.
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const nodes = [
|
|
* { name: 'A', dependencies: ['B'], dependencyNames: ['A'] },
|
|
* { name: 'B', dependencies: ['C'], dependencyNames: ['B'] },
|
|
* { name: 'C', dependencies: ['A'], dependencyNames: ['C'] },
|
|
* ]
|
|
* const circularNodes = computeNodesInCircularDependencies(nodes)
|
|
* // Returns: Set containing all three nodes (A, B, C)
|
|
* ```
|
|
*
|
|
* @template T - Type of sorting node with dependencies.
|
|
* @param elements - Array of nodes with dependency information.
|
|
* @returns Set of nodes that participate in circular dependencies.
|
|
*/
|
|
function computeNodesInCircularDependencies(elements) {
|
|
let elementsInCycles = /* @__PURE__ */ new Set()
|
|
let visitingElements = /* @__PURE__ */ new Set()
|
|
let visitedElements = /* @__PURE__ */ new Set()
|
|
/**
|
|
* Performs depth-first search to detect cycles starting from the given
|
|
* element.
|
|
*
|
|
* Recursively traverses the dependency graph, maintaining a path of the
|
|
* current traversal. If a node in the current path is encountered again, a
|
|
* cycle is detected and all nodes in the cycle are marked.
|
|
*
|
|
* @param element - Current node being visited.
|
|
* @param path - Array of nodes in the current DFS path.
|
|
*/
|
|
function depthFirstSearch(element, path) {
|
|
if (visitedElements.has(element)) {
|
|
return
|
|
}
|
|
if (visitingElements.has(element)) {
|
|
let cycleStartIndex = path.indexOf(element)
|
|
/* v8 ignore else -- @preserve Visiting path already contains the element when this branch executes. */
|
|
if (cycleStartIndex !== -1) {
|
|
for (let cycleElements of path.slice(cycleStartIndex)) {
|
|
elementsInCycles.add(cycleElements)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
visitingElements.add(element)
|
|
path.push(element)
|
|
for (let dependency of element.dependencies) {
|
|
let dependencyElement = elements
|
|
.filter(currentElement => currentElement !== element)
|
|
.find(currentElement =>
|
|
currentElement.dependencyNames.includes(dependency),
|
|
)
|
|
/* v8 ignore next -- @preserve Dependencies are pre-filtered; missing entries are defensive fallback. */
|
|
if (dependencyElement) {
|
|
depthFirstSearch(dependencyElement, [...path])
|
|
}
|
|
}
|
|
visitingElements.delete(element)
|
|
visitedElements.add(element)
|
|
}
|
|
for (let element of elements) {
|
|
if (!visitedElements.has(element)) {
|
|
depthFirstSearch(element, [])
|
|
}
|
|
}
|
|
return elementsInCycles
|
|
}
|
|
export { computeNodesInCircularDependencies }
|