UNPKG

@backstage/backend-app-api

Version:

Core API used by Backstage backend apps

199 lines (196 loc) • 5.85 kB
'use strict'; class Node { constructor(value, consumes, provides) { this.value = value; this.consumes = consumes; this.provides = provides; } static from(input) { return new Node( input.value, input.consumes ? new Set(input.consumes) : /* @__PURE__ */ new Set(), input.provides ? new Set(input.provides) : /* @__PURE__ */ new Set() ); } } class CycleKeySet { static from(nodes) { return new CycleKeySet(nodes); } #nodeIds; #cycleKeys; constructor(nodes) { this.#nodeIds = new Map(nodes.map((n, i) => [n.value, i])); this.#cycleKeys = /* @__PURE__ */ new Set(); } tryAdd(path) { const cycleKey = this.#getCycleKey(path); if (this.#cycleKeys.has(cycleKey)) { return false; } this.#cycleKeys.add(cycleKey); return true; } #getCycleKey(path) { return path.map((n) => this.#nodeIds.get(n)).sort().join(","); } } class DependencyGraph { static fromMap(nodes) { return this.fromIterable( Object.entries(nodes).map(([key, node]) => ({ value: String(key), ...node })) ); } static fromIterable(nodeInputs) { const nodes = new Array(); for (const nodeInput of nodeInputs) { nodes.push(Node.from(nodeInput)); } return new DependencyGraph(nodes); } #nodes; #allProvided; constructor(nodes) { this.#nodes = nodes; this.#allProvided = /* @__PURE__ */ new Set(); for (const node of this.#nodes.values()) { for (const produced of node.provides) { this.#allProvided.add(produced); } } } /** * Find all nodes that consume dependencies that are not provided by any other node. */ findUnsatisfiedDeps() { const unsatisfiedDependencies = []; for (const node of this.#nodes.values()) { const unsatisfied = Array.from(node.consumes).filter( (id) => !this.#allProvided.has(id) ); if (unsatisfied.length > 0) { unsatisfiedDependencies.push({ value: node.value, unsatisfied }); } } return unsatisfiedDependencies; } /** * Detect the first circular dependency within the graph, returning the path of nodes that * form a cycle, with the same node as the first and last element of the array. */ detectCircularDependency() { return this.detectCircularDependencies().next().value; } /** * Detect circular dependencies within the graph, returning the path of nodes that * form a cycle, with the same node as the first and last element of the array. */ *detectCircularDependencies() { const cycleKeys = CycleKeySet.from(this.#nodes); for (const startNode of this.#nodes) { const visited = /* @__PURE__ */ new Set(); const stack = new Array([ startNode, [startNode.value] ]); while (stack.length > 0) { const [node, path] = stack.pop(); if (visited.has(node)) { continue; } visited.add(node); for (const consumed of node.consumes) { const providerNodes = this.#nodes.filter( (other) => other.provides.has(consumed) ); for (const provider of providerNodes) { if (provider === startNode) { if (cycleKeys.tryAdd(path)) { yield [...path, startNode.value]; } break; } if (!visited.has(provider)) { stack.push([provider, [...path, provider.value]]); } } } } } return void 0; } /** * Traverses the dependency graph in topological order, calling the provided * function for each node and waiting for it to resolve. * * The nodes are traversed in parallel, but in such a way that no node is * visited before all of its dependencies. * * Dependencies of nodes that are not produced by any other nodes will be ignored. */ async parallelTopologicalTraversal(fn) { const allProvided = this.#allProvided; const waiting = new Set(this.#nodes.values()); const visited = /* @__PURE__ */ new Set(); const results = new Array(); let inFlight = 0; const producedRemaining = /* @__PURE__ */ new Map(); for (const node of this.#nodes) { for (const provided of node.provides) { producedRemaining.set( provided, (producedRemaining.get(provided) ?? 0) + 1 ); } } async function processMoreNodes() { if (waiting.size === 0) { return; } const nodesToProcess = []; for (const node of waiting) { let ready = true; for (const consumed of node.consumes) { if (allProvided.has(consumed) && producedRemaining.get(consumed) !== 0) { ready = false; continue; } } if (ready) { nodesToProcess.push(node); } } for (const node of nodesToProcess) { waiting.delete(node); } if (nodesToProcess.length === 0 && inFlight === 0) { throw new Error("Circular dependency detected"); } await Promise.all(nodesToProcess.map(processNode)); } async function processNode(node) { visited.add(node); inFlight += 1; const result = await fn(node.value); results.push(result); node.provides.forEach((produced) => { const remaining = producedRemaining.get(produced); if (!remaining) { throw new Error( `Internal error: Node provided superfluous dependency '${produced}'` ); } producedRemaining.set(produced, remaining - 1); }); inFlight -= 1; await processMoreNodes(); } await processMoreNodes(); return results; } } exports.DependencyGraph = DependencyGraph; //# sourceMappingURL=DependencyGraph.cjs.js.map