Add functions for mapping node locations to an id

These locations differ from either local ids (like `5`) or execution ids
(like `3:4:5`) in that all nodes which are 'linked' such that changes to
one node will apply changes to other nodes will map to the same id. This
is specifically relevant for multiple instances of the same subgraph.

These IDs will actually get used in a followup commit
This commit is contained in:
Jacob Segal
2025-07-06 20:41:58 -07:00
parent a70a81c9ae
commit c8371d6089
7 changed files with 765 additions and 4 deletions

View File

@@ -24,6 +24,7 @@ import type {
} from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import { useCanvasStore } from './graphStore'
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
@@ -309,6 +310,40 @@ export const useExecutionStore = defineStore('execution', () => {
)
}
/**
* Convert execution context node IDs to NodeLocatorIds
* @param nodeId The node ID from execution context (could be hierarchical)
* @returns The NodeLocatorId
*/
const executionIdToNodeLocatorId = (
nodeId: string | number
): NodeLocatorId => {
const nodeIdStr = String(nodeId)
// If it's a hierarchical ID, use the workflow store's conversion
if (nodeIdStr.includes(':')) {
const result = workflowStore.hierarchicalIdToNodeLocatorId(nodeIdStr)
// If conversion fails, return the original ID as-is
return result ?? (nodeIdStr as NodeLocatorId)
}
// For simple node IDs, we need the active subgraph context
return workflowStore.nodeIdToNodeLocatorId(nodeIdStr)
}
/**
* Convert a NodeLocatorId to an execution context ID (hierarchical ID)
* @param locatorId The NodeLocatorId
* @returns The hierarchical execution ID or null if conversion fails
*/
const nodeLocatorIdToExecutionId = (
locatorId: NodeLocatorId | string
): string | null => {
const hierarchicalId =
workflowStore.nodeLocatorIdToHierarchicalId(locatorId)
return hierarchicalId
}
return {
isIdle,
clientId,
@@ -368,6 +403,9 @@ export const useExecutionStore = defineStore('execution', () => {
unbindExecutionEvents,
storePrompt,
// Raw executing progress data for backward compatibility in ComfyApp.
_executingNodeProgress
_executingNodeProgress,
// NodeLocatorId conversion helpers
executionIdToNodeLocatorId,
nodeLocatorIdToExecutionId
}
})

View File

@@ -4,10 +4,21 @@ import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { defaultGraphJSON } from '@/scripts/defaultGraph'
import type {
HierarchicalNodeId,
NodeLocatorId
} from '@/types/nodeIdentification'
import {
createHierarchicalNodeId,
createNodeLocatorId,
parseHierarchicalNodeId,
parseNodeLocatorId
} from '@/types/nodeIdentification'
import { getPathDetails } from '@/utils/formatUtil'
import { syncEntities } from '@/utils/syncUtil'
import { isSubgraph } from '@/utils/typeGuardUtil'
@@ -163,6 +174,15 @@ export interface WorkflowStore {
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
updateActiveGraph: () => void
executionIdToCurrentId: (id: string) => any
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
hierarchicalIdToNodeLocatorId: (
hierarchicalId: HierarchicalNodeId | string
) => NodeLocatorId | null
nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null
nodeLocatorIdToHierarchicalId: (
locatorId: NodeLocatorId | string,
targetSubgraph?: Subgraph
) => HierarchicalNodeId | null
}
export const useWorkflowStore = defineStore('workflow', () => {
@@ -488,6 +508,136 @@ export const useWorkflowStore = defineStore('workflow', () => {
watch(activeWorkflow, updateActiveGraph)
/**
* Convert a node ID to a NodeLocatorId
* @param nodeId The local node ID
* @param subgraph The subgraph containing the node (defaults to active subgraph)
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
*/
const nodeIdToNodeLocatorId = (
nodeId: NodeId,
subgraph?: Subgraph
): NodeLocatorId => {
const targetSubgraph = subgraph ?? activeSubgraph.value
if (!targetSubgraph) {
// Node is in the root graph, return the node ID as-is
return String(nodeId) as NodeLocatorId
}
return createNodeLocatorId(targetSubgraph.id, nodeId)
}
/**
* Convert a hierarchical ID to a NodeLocatorId
* @param hierarchicalId The hierarchical node ID (e.g., "123:456:789")
* @returns The NodeLocatorId or null if conversion fails
*/
const hierarchicalIdToNodeLocatorId = (
hierarchicalId: HierarchicalNodeId | string
): NodeLocatorId | null => {
// Handle simple node IDs (root graph - no colons)
if (!hierarchicalId.includes(':')) {
return hierarchicalId as NodeLocatorId
}
const parts = parseHierarchicalNodeId(hierarchicalId)
if (!parts || parts.length === 0) return null
const nodeId = parts[parts.length - 1]
const subgraphNodeIds = parts.slice(0, -1)
if (subgraphNodeIds.length === 0) {
// Node is in root graph, return the node ID as-is
return String(nodeId) as NodeLocatorId
}
try {
const subgraphs = getSubgraphsFromInstanceIds(
comfyApp.graph,
subgraphNodeIds.map((id) => String(id))
)
const immediateSubgraph = subgraphs[subgraphs.length - 1]
return createNodeLocatorId(immediateSubgraph.id, nodeId)
} catch {
return null
}
}
/**
* Extract the node ID from a NodeLocatorId
* @param locatorId The NodeLocatorId
* @returns The local node ID or null if invalid
*/
const nodeLocatorIdToNodeId = (
locatorId: NodeLocatorId | string
): NodeId | null => {
const parsed = parseNodeLocatorId(locatorId)
return parsed?.localNodeId ?? null
}
/**
* Convert a NodeLocatorId to a hierarchical ID for a specific context
* @param locatorId The NodeLocatorId
* @param targetSubgraph The subgraph context (defaults to active subgraph)
* @returns The hierarchical ID or null if the node is not accessible from the target context
*/
const nodeLocatorIdToHierarchicalId = (
locatorId: NodeLocatorId | string,
targetSubgraph?: Subgraph
): HierarchicalNodeId | null => {
const parsed = parseNodeLocatorId(locatorId)
if (!parsed) return null
const { subgraphUuid, localNodeId } = parsed
// If no subgraph UUID, this is a root graph node
if (!subgraphUuid) {
return String(localNodeId) as HierarchicalNodeId
}
// Find the path from root to the subgraph with this UUID
const findSubgraphPath = (
graph: LGraph | Subgraph,
targetUuid: string,
path: NodeId[] = []
): NodeId[] | null => {
if (isSubgraph(graph) && graph.id === targetUuid) {
return path
}
for (const node of graph._nodes) {
if (node.isSubgraphNode?.() && (node as any).subgraph) {
const result = findSubgraphPath((node as any).subgraph, targetUuid, [
...path,
node.id
])
if (result) return result
}
}
return null
}
const path = findSubgraphPath(comfyApp.graph, subgraphUuid)
if (!path) return null
// If we have a target subgraph, check if the path goes through it
if (
targetSubgraph &&
!path.some((_, idx) => {
const subgraphs = getSubgraphsFromInstanceIds(
comfyApp.graph,
path.slice(0, idx + 1).map((id) => String(id))
)
return subgraphs[subgraphs.length - 1] === targetSubgraph
})
) {
return null
}
return createHierarchicalNodeId([...path, localNodeId])
}
return {
activeWorkflow,
isActive,
@@ -514,7 +664,11 @@ export const useWorkflowStore = defineStore('workflow', () => {
isSubgraphActive,
activeSubgraph,
updateActiveGraph,
executionIdToCurrentId
executionIdToCurrentId,
nodeIdToNodeLocatorId,
hierarchicalIdToNodeLocatorId,
nodeLocatorIdToNodeId,
nodeLocatorIdToHierarchicalId
}
}) satisfies () => WorkflowStore

View File

@@ -31,6 +31,16 @@ export type { ComfyApi } from '@/scripts/api'
export type { ComfyApp } from '@/scripts/app'
export type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
export type { InputSpec } from '@/schemas/nodeDefSchema'
export type {
NodeLocatorId,
HierarchicalNodeId,
isNodeLocatorId,
isHierarchicalNodeId,
parseNodeLocatorId,
createNodeLocatorId,
parseHierarchicalNodeId,
createHierarchicalNodeId
} from './nodeIdentification'
export type {
EmbeddingsResponse,
ExtensionsResponse,

View File

@@ -0,0 +1,127 @@
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
/**
* A globally unique identifier for nodes that maintains consistency across
* multiple instances of the same subgraph.
*
* Format:
* - For subgraph nodes: `<immediate-contained-subgraph-uuid>:<local-node-id>`
* - For root graph nodes: `<local-node-id>`
*
* Examples:
* - "a1b2c3d4-e5f6-7890-abcd-ef1234567890:123" (node in subgraph)
* - "456" (node in root graph)
*
* Unlike hierarchical IDs which change based on the instance path,
* NodeLocatorId remains the same for all instances of a particular node.
*/
export type NodeLocatorId = string & { __brand: 'NodeLocatorId' }
/**
* A hierarchical identifier representing a node's position in nested subgraphs.
* Also known as ExecutionId in some contexts.
*
* Format: Colon-separated path of node IDs
* Example: "123:456:789" (node 789 in subgraph 456 in subgraph 123)
*/
export type HierarchicalNodeId = string & { __brand: 'HierarchicalNodeId' }
/**
* Type guard to check if a value is a NodeLocatorId
*/
export function isNodeLocatorId(value: unknown): value is NodeLocatorId {
if (typeof value !== 'string') return false
// Check if it's a simple node ID (root graph node)
const parts = value.split(':')
if (parts.length === 1) {
// Simple node ID - must be non-empty
return value.length > 0
}
// Check for UUID:nodeId format
if (parts.length !== 2) return false
// Check that node ID part is not empty
if (!parts[1]) return false
// Basic UUID format check (8-4-4-4-12 hex characters)
const uuidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
return uuidPattern.test(parts[0])
}
/**
* Type guard to check if a value is a HierarchicalNodeId
*/
export function isHierarchicalNodeId(
value: unknown
): value is HierarchicalNodeId {
if (typeof value !== 'string') return false
// Must contain at least one colon to be hierarchical
return value.includes(':')
}
/**
* Parse a NodeLocatorId into its components
* @param id The NodeLocatorId to parse
* @returns The subgraph UUID and local node ID, or null if invalid
*/
export function parseNodeLocatorId(
id: string
): { subgraphUuid: string | null; localNodeId: NodeId } | null {
if (!isNodeLocatorId(id)) return null
const parts = id.split(':')
if (parts.length === 1) {
// Simple node ID (root graph)
return {
subgraphUuid: null,
localNodeId: isNaN(Number(id)) ? id : Number(id)
}
}
const [subgraphUuid, localNodeId] = parts
return {
subgraphUuid,
localNodeId: isNaN(Number(localNodeId)) ? localNodeId : Number(localNodeId)
}
}
/**
* Create a NodeLocatorId from components
* @param subgraphUuid The UUID of the immediate containing subgraph
* @param localNodeId The local node ID within that subgraph
* @returns A properly formatted NodeLocatorId
*/
export function createNodeLocatorId(
subgraphUuid: string,
localNodeId: NodeId
): NodeLocatorId {
return `${subgraphUuid}:${localNodeId}` as NodeLocatorId
}
/**
* Parse a HierarchicalNodeId into its component node IDs
* @param id The HierarchicalNodeId to parse
* @returns Array of node IDs from root to target, or null if not hierarchical
*/
export function parseHierarchicalNodeId(id: string): NodeId[] | null {
if (!isHierarchicalNodeId(id)) return null
return id
.split(':')
.map((part) => (isNaN(Number(part)) ? part : Number(part)))
}
/**
* Create a HierarchicalNodeId from an array of node IDs
* @param nodeIds Array of node IDs from root to target
* @returns A properly formatted HierarchicalNodeId
*/
export function createHierarchicalNodeId(
nodeIds: NodeId[]
): HierarchicalNodeId {
return nodeIds.join(':') as HierarchicalNodeId
}