[refactor] Extract executionErrorStore from executionStore (#9060)

## Summary
Extracts error-related state and logic from `executionStore` into a
dedicated `executionErrorStore` for better separation of concerns.

## Changes
- **New store**: `executionErrorStore` with all error state
(`lastNodeErrors`, `lastExecutionError`, `lastPromptError`), computed
properties (`hasAnyError`, `totalErrorCount`,
`activeGraphErrorNodeIds`), and UI state (`isErrorOverlayOpen`,
`showErrorOverlay`, `dismissErrorOverlay`)
- **Moved util**: `executionIdToNodeLocatorId` extracted to
`graphTraversalUtil`, reusing `traverseSubgraphPath` and accepting
`rootGraph` as parameter
- **Updated consumers**: 12 files updated to import from
`executionErrorStore`
- **Backward compat**: Deprecated getters retained in `ComfyApp` for
extension compatibility

## Review Focus
- Deprecated getters in `app.ts` — can be removed in a future
breaking-change PR once extension authors migrate

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9060-refactor-Extract-executionErrorStore-from-executionStore-30e6d73d36508101973de835ab6b199f)
by [Unito](https://www.unito.io)
This commit is contained in:
jaeone94
2026-02-22 09:51:22 +09:00
committed by GitHub
parent d9fdb01d9b
commit 8aa4e36fd5
20 changed files with 425 additions and 373 deletions

View File

@@ -0,0 +1,280 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import type {
ExecutionErrorWsMessage,
NodeError,
PromptError
} from '@/schemas/apiSchema'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import {
executionIdToNodeLocatorId,
forEachNode,
getNodeByExecutionId
} from '@/utils/graphTraversalUtil'
/**
* Store dedicated to execution error state management.
*
* Extracted from executionStore to separate error-related concerns
* (state, computed properties, graph flag propagation, overlay UI)
* from execution flow management (progress, queuing, events).
*/
export const useExecutionErrorStore = defineStore('executionError', () => {
const workflowStore = useWorkflowStore()
const canvasStore = useCanvasStore()
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
const lastPromptError = ref<PromptError | null>(null)
const isErrorOverlayOpen = ref(false)
function showErrorOverlay() {
isErrorOverlayOpen.value = true
}
function dismissErrorOverlay() {
isErrorOverlayOpen.value = false
}
/** Clear all error state. Called at execution start. */
function clearAllErrors() {
lastExecutionError.value = null
lastPromptError.value = null
lastNodeErrors.value = null
isErrorOverlayOpen.value = false
}
/** Clear only prompt-level errors. Called during resetExecutionState. */
function clearPromptError() {
lastPromptError.value = null
}
const lastExecutionErrorNodeLocatorId = computed(() => {
const err = lastExecutionError.value
if (!err) return null
return executionIdToNodeLocatorId(app.rootGraph, String(err.node_id))
})
const lastExecutionErrorNodeId = computed(() => {
const locator = lastExecutionErrorNodeLocatorId.value
if (!locator) return null
const localId = workflowStore.nodeLocatorIdToNodeId(locator)
return localId != null ? String(localId) : null
})
/** Whether a runtime execution error is present */
const hasExecutionError = computed(() => !!lastExecutionError.value)
/** Whether a prompt-level error is present (e.g. invalid_prompt, prompt_no_outputs) */
const hasPromptError = computed(() => !!lastPromptError.value)
/** Whether any node validation errors are present */
const hasNodeError = computed(
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
)
/** Whether any error (node validation, runtime execution, or prompt-level) is present */
const hasAnyError = computed(
() => hasExecutionError.value || hasPromptError.value || hasNodeError.value
)
const allErrorExecutionIds = computed<string[]>(() => {
const ids: string[] = []
if (lastNodeErrors.value) {
ids.push(...Object.keys(lastNodeErrors.value))
}
if (lastExecutionError.value) {
const nodeId = lastExecutionError.value.node_id
if (nodeId !== null && nodeId !== undefined) {
ids.push(String(nodeId))
}
}
return ids
})
/** Count of prompt-level errors (0 or 1) */
const promptErrorCount = computed(() => (lastPromptError.value ? 1 : 0))
/** Count of all individual node validation errors */
const nodeErrorCount = computed(() => {
if (!lastNodeErrors.value) return 0
let count = 0
for (const nodeError of Object.values(lastNodeErrors.value)) {
count += nodeError.errors.length
}
return count
})
/** Count of runtime execution errors (0 or 1) */
const executionErrorCount = computed(() => (lastExecutionError.value ? 1 : 0))
/** Total count of all individual errors */
const totalErrorCount = computed(
() =>
promptErrorCount.value + nodeErrorCount.value + executionErrorCount.value
)
/** Pre-computed Set of graph node IDs (as strings) that have errors in the current graph scope. */
const activeGraphErrorNodeIds = computed<Set<string>>(() => {
const ids = new Set<string>()
if (!app.rootGraph) return ids
// Fall back to rootGraph when currentGraph hasn't been initialized yet
const activeGraph = canvasStore.currentGraph ?? app.rootGraph
if (lastNodeErrors.value) {
for (const executionId of Object.keys(lastNodeErrors.value)) {
const graphNode = getNodeByExecutionId(app.rootGraph, executionId)
if (graphNode?.graph === activeGraph) {
ids.add(String(graphNode.id))
}
}
}
if (lastExecutionError.value) {
const execNodeId = String(lastExecutionError.value.node_id)
const graphNode = getNodeByExecutionId(app.rootGraph, execNodeId)
if (graphNode?.graph === activeGraph) {
ids.add(String(graphNode.id))
}
}
return ids
})
/** Map of node errors indexed by locator ID. */
const nodeErrorsByLocatorId = computed<Record<NodeLocatorId, NodeError>>(
() => {
if (!lastNodeErrors.value) return {}
const map: Record<NodeLocatorId, NodeError> = {}
for (const [executionId, nodeError] of Object.entries(
lastNodeErrors.value
)) {
const locatorId = executionIdToNodeLocatorId(app.rootGraph, executionId)
if (locatorId) {
map[locatorId] = nodeError
}
}
return map
}
)
/** Get node errors by locator ID. */
const getNodeErrors = (
nodeLocatorId: NodeLocatorId
): NodeError | undefined => {
return nodeErrorsByLocatorId.value[nodeLocatorId]
}
/** Check if a specific slot has validation errors. */
const slotHasError = (
nodeLocatorId: NodeLocatorId,
slotName: string
): boolean => {
const nodeError = getNodeErrors(nodeLocatorId)
if (!nodeError) return false
return nodeError.errors.some((e) => e.extra_info?.input_name === slotName)
}
function hasInternalErrorForNode(nodeId: string | number): boolean {
const prefix = `${nodeId}:`
return allErrorExecutionIds.value.some((id) => id.startsWith(prefix))
}
/**
* Update node and slot error flags when validation errors change.
* Propagates errors up subgraph chains.
*/
watch(lastNodeErrors, () => {
if (!app.rootGraph) return
// Clear all error flags
forEachNode(app.rootGraph, (node) => {
node.has_errors = false
if (node.inputs) {
for (const slot of node.inputs) {
slot.hasErrors = false
}
}
})
if (!lastNodeErrors.value) return
// Set error flags on nodes and slots
for (const [executionId, nodeError] of Object.entries(
lastNodeErrors.value
)) {
const node = getNodeByExecutionId(app.rootGraph, executionId)
if (!node) continue
node.has_errors = true
// Mark input slots with errors
if (node.inputs) {
for (const error of nodeError.errors) {
const slotName = error.extra_info?.input_name
if (!slotName) continue
const slot = node.inputs.find((s) => s.name === slotName)
if (slot) {
slot.hasErrors = true
}
}
}
// Propagate errors to parent subgraph nodes
const parts = executionId.split(':')
for (let i = parts.length - 1; i > 0; i--) {
const parentExecutionId = parts.slice(0, i).join(':')
const parentNode = getNodeByExecutionId(
app.rootGraph,
parentExecutionId
)
if (parentNode) {
parentNode.has_errors = true
}
}
}
})
return {
// Raw state
lastNodeErrors,
lastExecutionError,
lastPromptError,
// Clearing
clearAllErrors,
clearPromptError,
// Overlay UI
isErrorOverlayOpen,
showErrorOverlay,
dismissErrorOverlay,
// Derived state
hasExecutionError,
hasPromptError,
hasNodeError,
hasAnyError,
allErrorExecutionIds,
totalErrorCount,
lastExecutionErrorNodeId,
activeGraphErrorNodeIds,
// Lookup helpers
getNodeErrors,
slotHasError,
hasInternalErrorForNode
}
})