import type { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph' import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums' import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification' import { createNodeLocatorId, getParentExecutionIds, parseNodeLocatorId } from '@/types/nodeIdentification' import { isSubgraphIoNode } from './typeGuardUtil' /** * Constructs a locator ID from node data with optional subgraph context. * * @param nodeData - Node data containing id and optional subgraphId * @returns The locator ID string */ export function getLocatorIdFromNodeData(nodeData: { id: string | number subgraphId?: string | null }): string { return nodeData.subgraphId ? `${nodeData.subgraphId}:${String(nodeData.id)}` : String(nodeData.id) } /** * Parses an execution ID into its component parts. * * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns Array of node IDs in the path, or null if invalid */ export function parseExecutionId(executionId: string): string[] | null { if (!executionId || typeof executionId !== 'string') return null return executionId.split(':').filter((part) => part.length > 0) } /** * Extracts the local node ID from an execution ID. * * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns The local node ID or null if invalid */ export function getLocalNodeIdFromExecutionId( executionId: string ): string | null { const parts = parseExecutionId(executionId) return parts ? parts[parts.length - 1] : null } /** * Extracts the subgraph path from an execution ID. * * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns Array of subgraph node IDs (excluding the final node ID), or empty array */ export function getSubgraphPathFromExecutionId(executionId: string): string[] { const parts = parseExecutionId(executionId) return parts ? parts.slice(0, -1) : [] } /** * Visits each node in a graph (non-recursive, single level). * * @param graph - The graph to visit nodes from * @param visitor - Function called for each node */ export function visitGraphNodes( graph: LGraph | Subgraph, visitor: (node: LGraphNode) => void ): void { for (const node of graph.nodes) { visitor(node) } } /** * Traverses a path of subgraphs to reach a target graph. * * @param startGraph - The graph to start from * @param path - Array of subgraph node IDs to traverse * @returns The target graph or null if path is invalid */ export function traverseSubgraphPath( startGraph: LGraph | Subgraph, path: string[] ): LGraph | Subgraph | null { let currentGraph: LGraph | Subgraph = startGraph for (const nodeId of path) { const node = currentGraph.getNodeById(nodeId) if (!node?.isSubgraphNode?.() || !node.subgraph) return null currentGraph = node.subgraph } return currentGraph } /** * Traverses all nodes in a graph hierarchy (including subgraphs) and invokes * a callback on each node that has the specified property. * * @param graph - The root graph to start traversal from * @param callbackProperty - The name of the callback property to invoke on each node */ export function triggerCallbackOnAllNodes( graph: LGraph | Subgraph, callbackProperty: keyof LGraphNode ): void { forEachNode(graph, (node) => { const callback = node[callbackProperty] if (typeof callback === 'function') { callback.call(node) } }) } /** * Maps a function over all nodes in a graph hierarchy (including subgraphs). * This is a pure functional traversal that doesn't mutate the graph. * * @param graph - The root graph to traverse * @param mapFn - Function to apply to each node * @returns Array of mapped results (excluding undefined values) */ export function mapAllNodes( graph: LGraph | Subgraph, mapFn: (node: LGraphNode) => T | undefined ): T[] { const results: T[] = [] visitGraphNodes(graph, (node) => { // Recursively map over subgraphs first if (node.isSubgraphNode?.() && node.subgraph) { results.push(...mapAllNodes(node.subgraph, mapFn)) } // Apply map function to current node const result = mapFn(node) if (result !== undefined) { results.push(result) } }) return results } /** * Executes a side-effect function on all nodes in a graph hierarchy. * This is for operations that modify nodes or perform side effects. * * @param graph - The root graph to traverse * @param fn - Function to execute on each node */ export function forEachNode( graph: LGraph | Subgraph, fn: (node: LGraphNode) => void ): void { visitGraphNodes(graph, (node) => { // Recursively process subgraphs first if (node.isSubgraphNode?.() && node.subgraph) { forEachNode(node.subgraph, fn) } // Execute function on current node fn(node) }) } /** * Collects all nodes in a graph hierarchy (including subgraphs) into a flat array. * * @param graph - The root graph to collect nodes from * @param filter - Optional filter function to include only specific nodes * @returns Array of all nodes in the graph hierarchy */ export function collectAllNodes( graph: LGraph | Subgraph, filter?: (node: LGraphNode) => boolean ): LGraphNode[] { return mapAllNodes(graph, (node) => { if (!filter || filter(node)) { return node } return undefined }) } /** * Finds a node by ID anywhere in the graph hierarchy. * * @param graph - The root graph to search * @param nodeId - The ID of the node to find * @returns The node if found, null otherwise */ export function findNodeInHierarchy( graph: LGraph | Subgraph, nodeId: string | number ): LGraphNode | null { // Check current graph const node = graph.getNodeById(nodeId) if (node) return node // Search in subgraphs for (const node of graph.nodes) { if (node.isSubgraphNode?.() && node.subgraph) { const found = findNodeInHierarchy(node.subgraph, nodeId) if (found) return found } } return null } /** * Find a subgraph by its UUID anywhere in the graph hierarchy. * * @param graph - The root graph to search * @param targetUuid - The UUID of the subgraph to find * @returns The subgraph if found, null otherwise */ export function findSubgraphByUuid( graph: LGraph | Subgraph, targetUuid: string ): Subgraph | null { // Fast O(1) lookup via the root graph's centralized subgraph registry. if ('subgraphs' in graph && graph.subgraphs instanceof Map) { return graph.subgraphs.get(targetUuid) ?? null } // Fallback: recursive traversal for non-root graphs without the registry. for (const node of graph.nodes) { if (node.isSubgraphNode?.() && node.subgraph) { if (node.subgraph.id === targetUuid) { return node.subgraph } const found = findSubgraphByUuid(node.subgraph, targetUuid) if (found) return found } } return null } /** * Iteratively finds the path of subgraph IDs to a target subgraph. * @param rootGraph The graph to start searching from. * @param targetId The ID of the subgraph to find. * @returns An array of subgraph IDs representing the path, or `null` if not found. */ export function findSubgraphPathById( rootGraph: LGraph, targetId: string ): string[] | null { const stack: { graph: LGraph | Subgraph; path: string[] }[] = [ { graph: rootGraph, path: [] } ] while (stack.length > 0) { const { graph, path } = stack.pop()! // Check if graph exists and has _nodes property if (!graph || !graph._nodes || !Array.isArray(graph._nodes)) { continue } for (const node of graph._nodes) { if (node.isSubgraphNode?.() && node.subgraph) { const newPath = [...path, String(node.subgraph.id)] if (node.subgraph.id === targetId) { return newPath } stack.push({ graph: node.subgraph, path: newPath }) } } } return null } /** * Gets the root parent node associated with a hierarchical execution ID. * Both Group Nodes and Subgraph Nodes use hierarchical IDs (e.g. "rootId:childId:..."). * The root parent is always located in the rootGraph. * * @param rootGraph - The root graph to search from * @param executionId - The execution ID (e.g., "123:456") * @returns The root parent node if found, null otherwise */ export function getRootParentNode( rootGraph: LGraph, executionId: string ): LGraphNode | null { const parts = parseExecutionId(executionId) if (!parts || parts.length < 2) return null const parentId = parts[0] if (!rootGraph) return null return rootGraph.getNodeById(Number(parentId)) || null } /** * Get a node by its execution ID from anywhere in the graph hierarchy. * Execution IDs use hierarchical format like "123:456:789" for nested nodes. * * @param rootGraph - The root graph to search from * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns The node if found, null otherwise */ export function getNodeByExecutionId( rootGraph: LGraph, executionId: string ): LGraphNode | null { if (!rootGraph) return null const localNodeId = getLocalNodeIdFromExecutionId(executionId) if (!localNodeId) return null const subgraphPath = getSubgraphPathFromExecutionId(executionId) // If no subgraph path, it's in the root graph if (subgraphPath.length === 0) { return rootGraph.getNodeById(localNodeId) || null } // Traverse to the target subgraph const targetGraph = traverseSubgraphPath(rootGraph, subgraphPath) if (!targetGraph) return null // Get the node from the target graph return targetGraph.getNodeById(localNodeId) || null } /** * Returns the execution ID for a node relative to the root graph. * * Root-level nodes return their ID directly (e.g. "42"). * Nodes inside subgraphs return a colon-separated chain (e.g. "65:70:63"). * * @param rootGraph - The root graph to resolve from * @param node - The node whose execution ID to compute * @returns The execution ID string, or null if the node has no graph */ export function getExecutionIdByNode( rootGraph: LGraph, node: LGraphNode ): NodeExecutionId | null { if (!node.graph) return null if (node.graph === rootGraph || node.graph.isRootGraph) { return String(node.id) } const parentPath = findPartialExecutionPathToGraph( node.graph as LGraph, rootGraph ) if (parentPath === undefined) return null return `${parentPath}:${node.id}` } /** * True when every ancestor container in the execution path is active * (not muted, not bypassed). Self is not checked — caller is expected to * have already verified the target node's own mode. * * For root-level nodes (single-segment execution ID) there are no * ancestors and the result is always true. * * Use after an initial full-graph scan to suppress missing-asset entries * whose enclosing subgraph is muted/bypassed. At scan time only each * node's own mode is checked; ancestor context is applied here so the * effect cascades to interior nodes without requiring every scanner to * carry the ancestor flag. */ export function isAncestorPathActive( rootGraph: LGraph | null | undefined, executionId: string ): boolean { if (!rootGraph) return true for (const ancestorId of getParentExecutionIds(executionId)) { const ancestor = getNodeByExecutionId(rootGraph, ancestorId) if (!ancestor) continue if ( ancestor.mode === LGraphEventMode.NEVER || ancestor.mode === LGraphEventMode.BYPASS ) { return false } } return true } /** * Predicate used after async verification resolves: a missing-asset * candidate is surfaceable when it is confirmed missing and its * enclosing subgraph is still active. Null `nodeId` (workflow-level * models) bypasses the ancestor check since it has no scope to * validate. Unified helper so the initial pipeline post-filter and the * three async-resolution call sites cannot drift. */ export function isMissingCandidateActive( rootGraph: LGraph | null | undefined, candidate: { nodeId?: string | number | null | undefined isMissing?: boolean | undefined } ): boolean { if (candidate.isMissing !== true) return false if (candidate.nodeId == null) return true return isAncestorPathActive(rootGraph, String(candidate.nodeId)) } /** * Returns the execution ID for a node identified by its (graph, nodeId) pair. * * Unlike {@link getExecutionIdByNode}, this does not rely on `node.graph`. * Use this when the node reference may be detached (e.g. inside * `onNodeRemoved`, which LiteGraph fires after clearing `node.graph`). * * @param rootGraph - The root graph to resolve from * @param graph - The graph the node currently lives in (or lived in) * @param nodeId - The local node ID within `graph` */ export function getExecutionIdForNodeInGraph( rootGraph: LGraph, graph: LGraph | Subgraph, nodeId: string | number ): string { if (graph === rootGraph || graph.isRootGraph) return String(nodeId) const parentPath = findPartialExecutionPathToGraph(graph as LGraph, rootGraph) return parentPath !== undefined ? `${parentPath}:${nodeId}` : String(nodeId) } /** * Returns the execution ID for a node described by plain data (id + subgraphId), * without requiring a pre-existing {@link LGraphNode} reference. * Subgraph nodes return the full colon-separated path (e.g. `"65:70:63"`). * Falls back to `String(nodeData.id)` if the node cannot be resolved. * * @param rootGraph - The root graph to resolve from * @param nodeData - Object with `id` (local node ID) and optional `subgraphId` (UUID) */ export function getExecutionIdFromNodeData( rootGraph: LGraph, nodeData: { id: string | number; subgraphId?: string | null } ): string { const locatorId = getLocatorIdFromNodeData(nodeData) const node = getNodeByLocatorId(rootGraph, locatorId) return node ? (getExecutionIdByNode(rootGraph, node) ?? String(nodeData.id)) : String(nodeData.id) } /** * Get a node by its locator ID from anywhere in the graph hierarchy. * Locator IDs use UUID format like "uuid:nodeId" for subgraph nodes. * * @param rootGraph - The root graph to search from * @param locatorId - The locator ID (e.g., "uuid:123" or "123") * @returns The node if found, null otherwise */ export function getNodeByLocatorId( rootGraph: LGraph, locatorId: NodeLocatorId | string ): LGraphNode | null { if (!rootGraph) return null const parsedIds = parseNodeLocatorId(locatorId) if (!parsedIds) return null const { subgraphUuid, localNodeId } = parsedIds // If no subgraph UUID, it's in the root graph if (!subgraphUuid) { return rootGraph.getNodeById(localNodeId) || null } // Find the subgraph with the matching UUID const targetSubgraph = findSubgraphByUuid(rootGraph, subgraphUuid) if (!targetSubgraph) return null return targetSubgraph.getNodeById(localNodeId) || null } /** * Convert execution context node IDs to NodeLocatorIds. * Uses traverseSubgraphPath to resolve the subgraph chain. * * @param rootGraph - The root graph to resolve against * @param nodeId - The node ID from execution context (could be execution ID like "123:456:789") * @returns The NodeLocatorId, or undefined if resolution fails */ export function executionIdToNodeLocatorId( rootGraph: LGraph, nodeId: string | number ): NodeLocatorId | undefined { const nodeIdStr = String(nodeId) if (!nodeIdStr.includes(':')) { // It's a top-level node ID return nodeIdStr } // It's an execution node ID — resolve subgraph path const parts = nodeIdStr.split(':') const localNodeId = parts.at(-1)! const subgraphPath = parts.slice(0, -1) const targetGraph = traverseSubgraphPath(rootGraph, subgraphPath) if (!targetGraph) return undefined return createNodeLocatorId(targetGraph.id, localNodeId) } /** * Finds the root graph from any graph in the hierarchy. * * @param graph - Any graph or subgraph in the hierarchy * @returns The root graph */ export function getRootGraph(graph: LGraph | Subgraph): LGraph | Subgraph { let current: LGraph | Subgraph = graph while ('rootGraph' in current && current.rootGraph) { current = current.rootGraph } return current } /** * Applies a function to all nodes whose type matches a subgraph ID. * Operates on the entire graph hierarchy starting from the root. * * @param rootGraph - The root graph to search in * @param subgraphId - The ID/type of the subgraph to match nodes against * @param fn - Function to apply to each matching node */ export function forEachSubgraphNode( rootGraph: LGraph | Subgraph | null | undefined, subgraphId: string | null | undefined, fn: (node: LGraphNode) => void ): void { if (!rootGraph || !subgraphId) return forEachNode(rootGraph, (node) => { if (node.type === subgraphId) { fn(node) } }) } /** * Maps a function over all nodes whose type matches a subgraph ID. * Operates on the entire graph hierarchy starting from the root. * * @param rootGraph - The root graph to search in * @param subgraphId - The ID/type of the subgraph to match nodes against * @param mapFn - Function to apply to each matching node * @returns Array of mapped results */ export function mapSubgraphNodes( rootGraph: LGraph | Subgraph | null | undefined, subgraphId: string | null | undefined, mapFn: (node: LGraphNode) => T ): T[] { if (!rootGraph || !subgraphId) return [] return mapAllNodes(rootGraph, (node) => { if (node.type === subgraphId) { return mapFn(node) } return undefined }) } /** * Gets all non-IO nodes from a subgraph (excludes SubgraphInputNode and SubgraphOutputNode). * These are the user-created nodes that can be safely removed when clearing a subgraph. * * @param subgraph - The subgraph to get non-IO nodes from * @returns Array of non-IO nodes (user-created nodes) */ export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] { return subgraph.nodes.filter((node) => !isSubgraphIoNode(node)) } /** * Options for traverseNodesDepthFirst function */ interface TraverseNodesOptions { /** Function called for each node during traversal */ visitor?: (node: LGraphNode, context: T) => T /** Initial context value */ initialContext?: T /** Whether to traverse into subgraph nodes (default: true) */ expandSubgraphs?: boolean } /** * Performs depth-first traversal of nodes and their subgraphs. * Generic visitor pattern that can be used for various node processing tasks. * * @param nodes - Starting nodes for traversal * @param options - Optional traversal configuration */ export function traverseNodesDepthFirst( nodes: LGraphNode[], options?: TraverseNodesOptions ): void { const { visitor = () => undefined as T, initialContext = undefined as T, expandSubgraphs = true } = options || {} type StackItem = { node: LGraphNode; context: T } const stack: StackItem[] = [] // Initialize stack with starting nodes for (const node of nodes) { stack.push({ node, context: initialContext }) } // Process stack iteratively (DFS) while (stack.length > 0) { const { node, context } = stack.pop()! // Visit node and get updated context for children const childContext = visitor(node, context) // If it's a subgraph and we should expand, add children to stack if (expandSubgraphs && node.isSubgraphNode?.() && node.subgraph) { // Process children in reverse order to maintain left-to-right DFS processing // when popping from stack (LIFO). Iterate backwards to avoid array reversal. const children = node.subgraph.nodes for (let i = children.length - 1; i >= 0; i--) { stack.push({ node: children[i], context: childContext }) } } } } /** * Reduces all nodes in a graph hierarchy to a single value using a reducer function. * Single-pass traversal for efficient aggregation. * * @param graph - The root graph to traverse * @param reducer - Function that reduces each node into the accumulator * @param initialValue - The initial accumulator value * @returns The final reduced value */ export function reduceAllNodes( graph: LGraph | Subgraph, reducer: (accumulator: T, node: LGraphNode) => T, initialValue: T ): T { let result = initialValue forEachNode(graph, (node) => { result = reducer(result, node) }) return result } /** * Options for collectFromNodes function */ interface CollectFromNodesOptions { /** Function that returns data to collect for each node */ collector?: (node: LGraphNode, context: C) => T | null /** Function that builds context for child nodes */ contextBuilder?: (node: LGraphNode, parentContext: C) => C /** Initial context value */ initialContext?: C /** Whether to traverse into subgraph nodes (default: true) */ expandSubgraphs?: boolean } /** * Collects nodes with custom data during depth-first traversal. * Generic collector that can gather any type of data per node. * * @param nodes - Starting nodes for traversal * @param options - Optional collection configuration * @returns Array of collected data */ export function collectFromNodes( nodes: LGraphNode[], options?: CollectFromNodesOptions ): T[] { const { collector = (node: LGraphNode) => node as unknown as T, contextBuilder = () => undefined as C, initialContext = undefined as C, expandSubgraphs = true } = options || {} const results: T[] = [] traverseNodesDepthFirst(nodes, { visitor: (node, context) => { const data = collector(node, context) if (data !== null) { results.push(data) } return contextBuilder(node, context) }, initialContext, expandSubgraphs }) return results } /** * Collects execution IDs for selected nodes and all their descendants. * Uses the generic DFS traversal with optimized string building. * * @param selectedNodes - The selected nodes to process * @returns Array of execution IDs for selected nodes and all nodes within selected subgraphs */ export function getExecutionIdsForSelectedNodes( selectedNodes: LGraphNode[], startGraph = selectedNodes[0]?.graph ): NodeExecutionId[] { if (!startGraph) return [] const rootGraph = startGraph.rootGraph const parentPath = startGraph.isRootGraph ? '' : findPartialExecutionPathToGraph(startGraph, rootGraph) if (parentPath === undefined) return [] const buildExecId = (node: LGraphNode, parentExecutionId: string) => { const nodeId = String(node.id) return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId } return collectFromNodes(selectedNodes, { collector: buildExecId, contextBuilder: buildExecId, initialContext: parentPath, expandSubgraphs: true }) } /** * Returns the set of local graph node IDs (as strings) for nodes that live in * `activeGraph` and whose execution ID appears in `executionIds`. * * @param rootGraph - The root graph used to resolve execution IDs * @param activeGraph - The currently-visible graph scope * @param executionIds - Set of execution IDs to look up * @returns Set of stringified local node IDs belonging to activeGraph */ export function getActiveGraphNodeIds( rootGraph: LGraph, activeGraph: LGraph | Subgraph, executionIds: Set ): Set { const ids = new Set() for (const executionId of executionIds) { const graphNode = getNodeByExecutionId(rootGraph, executionId) if (graphNode?.graph === activeGraph) { ids.add(String(graphNode.id)) } } return ids } function findPartialExecutionPathToGraph( target: LGraph, root: LGraph ): string | undefined { for (const node of root.nodes) { if (!node.isSubgraphNode()) continue if (node.subgraph === target) return `${node.id}` const subpath = findPartialExecutionPathToGraph(target, node.subgraph) if (subpath !== undefined) return node.id + ':' + subpath } return undefined }