@backstage/backend-app-api
Version:
Core API used by Backstage backend apps
199 lines (196 loc) • 5.85 kB
JavaScript
;
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