Compare commits

..

1 Commits

Author SHA1 Message Date
CodeRabbit Fixer
b677fbfc7a fix: refactor: replace two-pass error flag reconciliation with single-pass diff in lastNodeErrors watcher (#9796)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:23:43 +01:00
12 changed files with 76 additions and 70 deletions

View File

@@ -59,7 +59,10 @@ function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
}
const pathPlusQueryParams = api.apiURL(
'/view?' + params.toString() + app.getPreviewFormatParam()
'/view?' +
params.toString() +
app.getPreviewFormatParam() +
app.getRandParam()
)
const imageElement = new Image()
imageElement.crossOrigin = 'anonymous'

View File

@@ -17,7 +17,7 @@ type MockTask = {
executionEndTimestamp?: number
previewOutput?: {
isImage: boolean
url: string
urlWithTimestamp: string
}
}
@@ -94,7 +94,7 @@ describe(useQueueNotificationBanners, () => {
if (previewUrl) {
task.previewOutput = {
isImage,
url: previewUrl
urlWithTimestamp: previewUrl
}
}

View File

@@ -231,7 +231,7 @@ export const useQueueNotificationBanners = () => {
completedCount++
const preview = task.previewOutput
if (preview?.isImage) {
imagePreviews.push(preview.url)
imagePreviews.push(preview.urlWithTimestamp)
}
} else if (state === 'failed') {
failedCount++

View File

@@ -1,6 +1,7 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeOutputWith } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
type ImageCompareOutput = NodeOutputWith<{
@@ -23,10 +24,11 @@ useExtensionService().registerExtension({
onExecuted?.call(this, output)
const { a_images: aImages, b_images: bImages } = output
const rand = app.getRandParam()
const toUrl = (record: Record<string, string>) => {
const params = new URLSearchParams(record)
return api.apiURL(`/view?${params}`)
return api.apiURL(`/view?${params}${rand}`)
}
const beforeImages =

View File

@@ -2,6 +2,7 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
class Load3dUtils {
static async generateThumbnailIfNeeded(
@@ -132,7 +133,8 @@ class Load3dUtils {
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder
'subfolder=' + subfolder,
app.getRandParam().substring(1)
].join('&')
return `/view?${params}`

View File

@@ -1,5 +1,5 @@
<template>
<WidgetLayoutField v-slot="{ borderStyle }" :widget :no-border="!hasLabels">
<WidgetLayoutField :widget>
<!-- Use ToggleGroup when explicit labels are provided -->
<ToggleGroup
v-if="hasLabels"
@@ -25,13 +25,7 @@
<!-- Use ToggleSwitch for implicit boolean states -->
<div
v-else
:class="
cn(
'-m-1 flex w-fit items-center gap-2 rounded-full p-1',
hideLayoutField || 'ml-auto',
borderStyle
)
"
:class="cn('flex w-fit items-center gap-2', hideLayoutField || 'ml-auto')"
>
<ToggleSwitch
v-model="modelValue"

View File

@@ -1,26 +1,17 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { useHideLayoutField } from '@/types/widgetTypes'
import { cn } from '@/utils/tailwindUtil'
const { widget, rootClass } = defineProps<{
const { rootClass } = defineProps<{
widget: Pick<
SimplifiedWidget<string | number | undefined>,
'name' | 'label' | 'borderStyle'
>
rootClass?: string
noBorder?: boolean
}>()
const hideLayoutField = useHideLayoutField()
const borderStyle = computed(() =>
cn(
'focus-within:ring focus-within:ring-component-node-widget-background-highlighted',
widget.borderStyle
)
)
</script>
<template>
@@ -42,15 +33,15 @@ const borderStyle = computed(() =>
<div
:class="
cn(
'min-w-0 cursor-default rounded-lg transition-all',
!noBorder && borderStyle
'min-w-0 cursor-default rounded-lg transition-all has-focus-visible:ring has-focus-visible:ring-component-node-widget-background-highlighted',
widget.borderStyle
)
"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<slot :border-style />
<slot />
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import type { ResultItemType } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
/**
* Format time in MM:SS format
@@ -19,7 +20,8 @@ export function getResourceURL(
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder
'subfolder=' + subfolder,
app.getRandParam().substring(1)
].join('&')
return `/view?${params}`

View File

@@ -382,6 +382,11 @@ export class ComfyApp {
else return ''
}
getRandParam() {
if (isCloud) return ''
return '&rand=' + Math.random()
}
static onClipspaceEditorSave() {
if (ComfyApp.clipspace_return_node) {
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node)

View File

@@ -35,44 +35,56 @@ interface MissingNodesError {
nodeTypes: MissingNodeType[]
}
function clearAllNodeErrorFlags(rootGraph: LGraph): void {
function reconcileNodeErrorFlags(
rootGraph: LGraph,
nodeErrors: Record<NodeId, NodeError> | null
): void {
const errorNodeIds = new Set<LGraphNode>()
const errorSlotNames = new Map<LGraphNode, Set<string>>()
if (nodeErrors) {
for (const [executionId, nodeError] of Object.entries(nodeErrors)) {
const node = getNodeByExecutionId(rootGraph, executionId)
if (!node) continue
errorNodeIds.add(node)
for (const error of nodeError.errors) {
const slotName = error.extra_info?.input_name
if (!slotName) continue
let slots = errorSlotNames.get(node)
if (!slots) {
slots = new Set()
errorSlotNames.set(node, slots)
}
slots.add(slotName)
}
for (const parentId of getParentExecutionIds(executionId)) {
const parentNode = getNodeByExecutionId(rootGraph, parentId)
if (parentNode) errorNodeIds.add(parentNode)
}
}
}
forEachNode(rootGraph, (node) => {
node.has_errors = false
const shouldHaveErrors = errorNodeIds.has(node)
if (node.has_errors !== shouldHaveErrors) {
node.has_errors = shouldHaveErrors
}
if (node.inputs) {
const slotNames = errorSlotNames.get(node)
for (const slot of node.inputs) {
slot.hasErrors = false
const shouldSlotHaveErrors = !!slotNames?.has(slot.name)
if (slot.hasErrors !== shouldSlotHaveErrors) {
slot.hasErrors = shouldSlotHaveErrors
}
}
}
})
}
function markNodeSlotErrors(node: LGraphNode, nodeError: NodeError): void {
if (!node.inputs) return
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
}
}
function applyNodeError(
rootGraph: LGraph,
executionId: NodeExecutionId,
nodeError: NodeError
): void {
const node = getNodeByExecutionId(rootGraph, executionId)
if (!node) return
node.has_errors = true
markNodeSlotErrors(node, nodeError)
for (const parentId of getParentExecutionIds(executionId)) {
const parentNode = getNodeByExecutionId(rootGraph, parentId)
if (parentNode) parentNode.has_errors = true
}
}
/** Execution error state: node errors, runtime errors, prompt errors, and missing assets. */
export const useExecutionErrorStore = defineStore('executionError', () => {
const workflowStore = useWorkflowStore()
@@ -377,17 +389,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
watch(lastNodeErrors, () => {
if (!app.isGraphReady) return
const rootGraph = app.rootGraph
clearAllNodeErrorFlags(rootGraph)
if (!lastNodeErrors.value) return
for (const [executionId, nodeError] of Object.entries(
lastNodeErrors.value
)) {
applyNodeError(rootGraph, executionId, nodeError)
}
reconcileNodeErrorFlags(app.rootGraph, lastNodeErrors.value)
})
return {

View File

@@ -117,11 +117,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
const outputs = getNodeOutputs(node)
if (!outputs?.images?.length) return
const rand = app.getRandParam()
const previewParam = getPreviewParam(node, outputs)
return outputs.images.map((image) => {
const params = new URLSearchParams(image)
return api.apiURL(`/view?${params}${previewParam}`)
return api.apiURL(`/view?${params}${previewParam}${rand}`)
})
}

View File

@@ -104,6 +104,10 @@ export class ResultItemImpl {
return api.apiURL('/view?' + params)
}
get urlWithTimestamp(): string {
return `${this.url}&t=${+new Date()}`
}
get isVhsFormat(): boolean {
return !!this.format && !!this.frame_rate
}