App mode - discard slow preview messages to prevent overwriting output image (#9261)

## Summary

Prevent latent previews received after the job/node has already finished
processing overwriting the actual output display

## Changes

- **What**: 
- updates job preview store to also track which node the preview was for
- updates linear progress tracking to store executed nodes enabling
skipping previews of these

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9261-App-mode-discard-slow-preview-messages-to-prevent-overwriting-output-image-3136d73d3650817884c2ce2ff5993b9e)
by [Unito](https://www.unito.io)
This commit is contained in:
pythongosssss
2026-02-27 18:58:41 +00:00
committed by GitHub
parent c090d189f0
commit f83daa6f3b
5 changed files with 265 additions and 29 deletions

View File

@@ -0,0 +1,102 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
import { releaseSharedObjectUrl } from '@/utils/objectUrlUtil'
const previewMethodRef = ref('latent2rgb')
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: (key: string) => {
if (key === 'Comfy.Execution.PreviewMethod') return previewMethodRef.value
return undefined
}
})
}))
vi.mock('@/utils/objectUrlUtil', () => ({
retainSharedObjectUrl: vi.fn(),
releaseSharedObjectUrl: vi.fn()
}))
describe('jobPreviewStore', () => {
beforeEach(() => {
vi.clearAllMocks()
setActivePinia(createPinia())
previewMethodRef.value = 'latent2rgb'
})
it('stores preview with nodeId', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('prompt-1', 'blob:url-1', 'node-5')
expect(store.nodePreviewsByPromptId['prompt-1']).toEqual({
url: 'blob:url-1',
nodeId: 'node-5'
})
})
it('stores preview without nodeId', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('prompt-1', 'blob:url-1')
expect(store.nodePreviewsByPromptId['prompt-1']).toEqual({
url: 'blob:url-1',
nodeId: undefined
})
})
it('derives previewsByPromptId as url-only map', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('p1', 'blob:a', 'node-1')
store.setPreviewUrl('p2', 'blob:b', 'node-2')
expect(store.previewsByPromptId).toEqual({
p1: 'blob:a',
p2: 'blob:b'
})
})
it('clears a single preview', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('p1', 'blob:a', 'node-1')
store.setPreviewUrl('p2', 'blob:b', 'node-2')
store.clearPreview('p1')
expect(store.nodePreviewsByPromptId['p1']).toBeUndefined()
expect(store.nodePreviewsByPromptId['p2']).toBeDefined()
expect(store.previewsByPromptId).toEqual({ p2: 'blob:b' })
})
it('clears all previews', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('p1', 'blob:a', 'node-1')
store.setPreviewUrl('p2', 'blob:b', 'node-2')
store.clearAllPreviews()
expect(store.nodePreviewsByPromptId).toEqual({})
expect(store.previewsByPromptId).toEqual({})
})
it('skips duplicate url', () => {
const store = useJobPreviewStore()
store.setPreviewUrl('p1', 'blob:a', 'node-1')
store.setPreviewUrl('p1', 'blob:a', 'node-1')
expect(releaseSharedObjectUrl).not.toHaveBeenCalled()
})
it('ignores setPreviewUrl when previews are disabled', () => {
previewMethodRef.value = 'none'
const store = useJobPreviewStore()
store.setPreviewUrl('p1', 'blob:a', 'node-1')
expect(store.nodePreviewsByPromptId).toEqual({})
})
})

View File

@@ -8,44 +8,59 @@ import {
} from '@/utils/objectUrlUtil'
type PromptPreviewMap = Record<string, string>
interface NodePromptPreview {
url: string
nodeId?: string
}
export const useJobPreviewStore = defineStore('jobPreview', () => {
const settingStore = useSettingStore()
const previewsByPromptId = ref<PromptPreviewMap>({})
const readonlyPreviewsByPromptId = readonly(previewsByPromptId)
const nodePreviewsByPromptId = ref<Record<string, NodePromptPreview>>({})
const previewMethod = computed(() =>
settingStore.get('Comfy.Execution.PreviewMethod')
)
const isPreviewEnabled = computed(() => previewMethod.value !== 'none')
function setPreviewUrl(promptId: string | undefined, url: string) {
const previewsByPromptId = computed(() => {
const result: PromptPreviewMap = {}
for (const [k, v] of Object.entries(nodePreviewsByPromptId.value)) {
result[k] = v.url
}
return result
})
function setPreviewUrl(
promptId: string | undefined,
url: string,
nodeId?: string
) {
if (!promptId || !isPreviewEnabled.value) return
const current = previewsByPromptId.value[promptId]
if (current === url) return
if (current) releaseSharedObjectUrl(current)
const current = nodePreviewsByPromptId.value[promptId]
if (current?.url === url) return
if (current) releaseSharedObjectUrl(current.url)
retainSharedObjectUrl(url)
previewsByPromptId.value = {
...previewsByPromptId.value,
[promptId]: url
nodePreviewsByPromptId.value = {
...nodePreviewsByPromptId.value,
[promptId]: { url, nodeId }
}
}
function clearPreview(promptId: string | undefined) {
if (!promptId) return
const current = previewsByPromptId.value[promptId]
const current = nodePreviewsByPromptId.value[promptId]
if (!current) return
releaseSharedObjectUrl(current)
const next = { ...previewsByPromptId.value }
releaseSharedObjectUrl(current.url)
const next = { ...nodePreviewsByPromptId.value }
delete next[promptId]
previewsByPromptId.value = next
nodePreviewsByPromptId.value = next
}
function clearAllPreviews() {
Object.values(previewsByPromptId.value).forEach((url) => {
for (const { url } of Object.values(nodePreviewsByPromptId.value)) {
releaseSharedObjectUrl(url)
})
previewsByPromptId.value = {}
}
nodePreviewsByPromptId.value = {}
}
watch(isPreviewEnabled, (enabled) => {
@@ -53,7 +68,8 @@ export const useJobPreviewStore = defineStore('jobPreview', () => {
})
return {
previewsByPromptId: readonlyPreviewsByPromptId,
nodePreviewsByPromptId: readonly(nodePreviewsByPromptId),
previewsByPromptId,
isPreviewEnabled,
setPreviewUrl,
clearPreview,