/** * Graph path utilities: compute which nodes/edges are "on the path" of an update. * Used to show connection ant trail only along the full chain (upstream → updating → downstream). * Works with any node types; any node can signal it is updating via startConnectionPathUpdate(id). */ export type GraphEdge = { source: string; target: string } /** Nodes reachable from seedIds by following edges forward (source → target). */ export function getDownstreamNodeIds(edges: GraphEdge[], seedIds: string[]): Set { const out = new Set(seedIds) let added = true while (added) { added = false for (const e of edges) { if (out.has(e.source) && !out.has(e.target)) { out.add(e.target) added = true } } } return out } /** Nodes that can reach any seed by following edges backward (target → source). */ export function getUpstreamNodeIds(edges: GraphEdge[], seedIds: string[]): Set { const out = new Set(seedIds) let added = true while (added) { added = false for (const e of edges) { if (out.has(e.target) && !out.has(e.source)) { out.add(e.source) added = true } } } return out } /** * All node ids that lie on the path of an update. * - If updatingNodeIds non-empty: path = downstream(trigger) ∩ (upstream(updating) ∪ downstream(updating)) * so we include config→agent→rendering when agent is running. * - Else if triggerNodeIds and pausedNodeIds non-empty: path = downstream(trigger) ∩ upstream(paused) * so we show yellow (config→agent) when config changed and agent is on hold. * An edge should show color iff both its source and target are in this set. */ export function getPathNodeIds( edges: GraphEdge[], updatingNodeIds: string[], triggerNodeIds?: string[], pausedNodeIds?: string[] ): Set { const hasUpdating = updatingNodeIds.length > 0 const hasPausedPath = pausedNodeIds != null && pausedNodeIds.length > 0 && triggerNodeIds != null && triggerNodeIds.length > 0 if (hasUpdating && triggerNodeIds != null && triggerNodeIds.length > 0) { const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds) const upstreamOfUpdating = getUpstreamNodeIds(edges, updatingNodeIds) const downstreamOfUpdating = getDownstreamNodeIds(edges, updatingNodeIds) const path = new Set() downstreamOfTrigger.forEach((id) => { if (upstreamOfUpdating.has(id) || downstreamOfUpdating.has(id)) path.add(id) }) return path } if (hasPausedPath && !hasUpdating) { const upstream = getUpstreamNodeIds(edges, pausedNodeIds!) const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds!) const path = new Set() upstream.forEach((id) => { if (downstreamOfTrigger.has(id)) path.add(id) }) return path } if (hasUpdating) { const upstream = getUpstreamNodeIds(edges, updatingNodeIds) const downstream = getDownstreamNodeIds(edges, updatingNodeIds) const path = new Set(upstream) downstream.forEach((id) => path.add(id)) return path } return new Set() } /** * Path nodes from triggers up to and including the first paused node (e.g. Renderer waiting for Run). * Used to color those edges yellow; rest of path stays blue. */ export function getPausedSegmentNodeIds( edges: GraphEdge[], pathNodeIds: Set, triggerNodeIds: string[], pausedNodeIds: string[] ): Set { if (pausedNodeIds.length === 0 || triggerNodeIds.length === 0) return new Set() const pausedSet = new Set(pausedNodeIds) const seeds = triggerNodeIds.filter((id) => pathNodeIds.has(id)) if (seeds.length === 0) return new Set() const out = new Set(seeds) const frontier: string[] = [...seeds] const visited = new Set(seeds) while (frontier.length > 0) { const n = frontier.shift()! if (pausedSet.has(n)) continue for (const e of edges) { if (e.source !== n || !pathNodeIds.has(e.target) || visited.has(e.target)) continue visited.add(e.target) out.add(e.target) if (pausedSet.has(e.target)) continue frontier.push(e.target) } } return out }