mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
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:
102
src/stores/jobPreviewStore.test.ts
Normal file
102
src/stores/jobPreviewStore.test.ts
Normal 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({})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user