Fix potential timeline hangs from cycles and refresh cascades

Add cycle detection and O(n) path building to getAllBranches, and a
concurrency guard on timeline refresh to drop overlapping calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 11:29:58 +01:00
parent 53b377d2d1
commit 7782bda677

View File

@@ -986,18 +986,22 @@ function getAncestorIds(snapshotId, parentOf) {
function getAllBranches(tree) { function getAllBranches(tree) {
const branches = []; const branches = [];
const visited = new Set();
function walk(nodeId, path) { function walk(nodeId, path) {
if (visited.has(nodeId)) return; // cycle detection
visited.add(nodeId);
const record = tree.byId.get(nodeId); const record = tree.byId.get(nodeId);
if (!record) return; if (!record) return;
const currentPath = [...path, record]; path.push(record);
const children = tree.childrenOf.get(nodeId); const children = tree.childrenOf.get(nodeId);
if (!children || children.length === 0) { if (!children || children.length === 0) {
branches.push(currentPath); branches.push([...path]);
} else { } else {
for (const child of children) { for (const child of children) {
walk(child.id, currentPath); walk(child.id, path);
} }
} }
path.pop();
} }
for (const root of tree.roots) { for (const root of tree.roots) {
walk(root.id, []); walk(root.id, []);
@@ -3720,9 +3724,14 @@ function buildTimeline() {
return marker; return marker;
} }
let refreshPending = false;
async function refresh() { async function refresh() {
if (!showTimeline) return; if (!showTimeline) return;
if (refreshPending) return; // drop overlapping calls
refreshPending = true;
try { await _refreshInner(); } finally { refreshPending = false; }
}
async function _refreshInner() {
const wfKey = getWorkflowKey(); const wfKey = getWorkflowKey();
// Hide/show expand button based on branching // Hide/show expand button based on branching