There are some syntax errors in the graph object you shared, but assuming that the children are identified by strings, you would typically use a depth first traversal to find out if you bump into an edge that is a back reference to a node that was already on the current path.
If that happens you have a cycle, and the cycle can be easily derived from the current path and the back-referenced node.
To save repetition of traversals, you would also keep track of nodes that have been visited (whether on the current path or not). There is no need to continue a search from a node that was already visited.
For marking nodes as visited, you could use a Set.
function findCycle(graph) {
let visited = new Set;
let result;
// dfs set the result to a cycle when the given node was already on the current path.
// If not on the path, and also not visited, it is marked as such. It then
// iterates the node's children and calls the function recursively.
// If any of those calls returns true, exit with true also
function dfs(node, path) {
if (path.has(node)) {
result = [...path, node]; // convert to array (a Set maintains insertion order)
result.splice(0, result.indexOf(node)); // remove part that precedes the cycle
return true;
}
if (visited.has(node)) return;
path.add(node);
visited.add(node);
if ((graph[node]?.children || []).some(child => dfs(child, path))) return path;
// Backtrack
path.delete(node);
// No cycle found here: return undefined
}
// Perform a DFS traversal for each node (except nodes that get
// visited in the process)
for (let node in graph) {
if (!visited.has(node) && dfs(node, new Set)) return result;
}
}
// Your example graph (with corrections):
const graph = {
a: {value: 1, children: ["b", "d"]},
b: {value: 2, children: ["c"]},
c: {value: 3, children: ["a", "d", "e"]}
};
// Find the cycle
console.log(findCycle(graph)); // ["a","b","c","a"]
// Break the cycle, and run again
graph.c.children.shift(); // drop the edge c->a
console.log(findCycle(graph)); // undefined (i.e. no cycle)