From b9537d352a40030578041a0c405e70207474a606 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Wed, 3 Dec 2025 22:29:13 -0800 Subject: [PATCH] remove dummy type; revert queuestore --- src/composables/queue/useJobList.ts | 2 +- src/composables/queue/useResultGallery.ts | 23 +-- .../remote/comfyui/jobs/types/jobTypes.ts | 2 + src/stores/queueStore.ts | 78 ++++----- tests-ui/tests/composables/useJobList.test.ts | 8 +- .../composables/useResultGallery.test.ts | 155 +++++++++--------- 6 files changed, 127 insertions(+), 141 deletions(-) diff --git a/src/composables/queue/useJobList.ts b/src/composables/queue/useJobList.ts index 47996636b..724b74ae2 100644 --- a/src/composables/queue/useJobList.ts +++ b/src/composables/queue/useJobList.ts @@ -238,7 +238,7 @@ export function useJobList() { const activeId = workflowStore.activeWorkflow?.activeState?.id if (!activeId) return [] entries = entries.filter(({ task }) => { - const wid = task.workflow?.id + const wid = task.workflowId return !!wid && wid === activeId }) } diff --git a/src/composables/queue/useResultGallery.ts b/src/composables/queue/useResultGallery.ts index ebe5816cf..28da09404 100644 --- a/src/composables/queue/useResultGallery.ts +++ b/src/composables/queue/useResultGallery.ts @@ -1,23 +1,10 @@ import { ref, shallowRef } from 'vue' import type { JobListItem } from '@/composables/queue/useJobList' -import type { ResultItemImpl } from '@/stores/queueStore' +import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore' type FetchApi = (url: string) => Promise -/** - * Minimal interface for tasks used by the result gallery. - * This allows the gallery to work with any object that provides these properties, - * without coupling to the full TaskItemImpl class. - */ -interface GalleryTask { - readonly promptId: string - readonly outputsCount?: number - readonly flatOutputs: readonly ResultItemImpl[] - readonly previewOutput?: ResultItemImpl - loadFullOutputs(fetchApi: FetchApi): Promise -} - const getPreviewableOutputs = (outputs?: readonly ResultItemImpl[]) => outputs?.filter((o) => o.supportsPreview) ?? [] @@ -31,17 +18,17 @@ const findActiveIndex = (items: ResultItemImpl[], url?: string): number => { * Manages result gallery state and activation for queue items. */ export function useResultGallery( - getFilteredTasks: () => GalleryTask[], + getFilteredTasks: () => TaskItemImpl[], fetchApi?: FetchApi ) { const galleryActiveIndex = ref(-1) const galleryItems = shallowRef([]) - const loadedTasksCache = new Map() + const loadedTasksCache = new Map() let currentRequestId = 0 const getOutputsForTask = async ( - task: GalleryTask + task: TaskItemImpl ): Promise => { const outputsCount = task.outputsCount ?? 0 const needsLazyLoad = outputsCount > 1 && fetchApi @@ -67,7 +54,7 @@ export function useResultGallery( const requestId = ++currentRequestId - const targetTask = item.taskRef as GalleryTask | undefined + const targetTask = item.taskRef let targetOutputs: ResultItemImpl[] = [] if (targetTask) { diff --git a/src/platform/remote/comfyui/jobs/types/jobTypes.ts b/src/platform/remote/comfyui/jobs/types/jobTypes.ts index 17a67b564..1ca20f251 100644 --- a/src/platform/remote/comfyui/jobs/types/jobTypes.ts +++ b/src/platform/remote/comfyui/jobs/types/jobTypes.ts @@ -57,6 +57,8 @@ const zRawJobListItem = z id: z.string(), status: zJobStatus, create_time: z.number(), + execution_start_time: z.number().nullable().optional(), + execution_end_time: z.number().nullable().optional(), preview_output: zPreviewOutput.nullable().optional(), outputs_count: z.number().optional(), error_message: z.string().nullable().optional(), diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts index ef951c5ca..570eb984b 100644 --- a/src/stores/queueStore.ts +++ b/src/stores/queueStore.ts @@ -5,10 +5,7 @@ import { computed, ref, shallowRef, toRaw, toValue } from 'vue' import { reconcileJobs } from '@/platform/remote/comfyui/history/reconciliation' import { extractWorkflow, fetchJobDetail } from '@/platform/remote/comfyui/jobs' import type { JobListItem } from '@/platform/remote/comfyui/jobs' -import type { - ComfyWorkflowJSON, - NodeId -} from '@/platform/workflow/validation/schemas/workflowSchema' +import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' import type { ResultItem, StatusWsMessageStatus, @@ -321,8 +318,17 @@ export class TaskItemImpl { return this.job.execution_error ?? undefined } - get workflow(): ComfyWorkflowJSON | undefined { - // Workflow is only available after lazy loading via getWorkflowFromHistory + /** + * Workflow ID if available from the job + */ + get workflowId(): string | undefined { + return this.job.workflow_id ?? undefined + } + + /** + * Full workflow data - not available in list response, use loadWorkflow() + */ + get workflow(): undefined { return undefined } @@ -334,10 +340,9 @@ export class TaskItemImpl { } /** - * Execution messages - not available in JobListItem, would need JobDetail - * @deprecated Use job status instead + * Execution messages - not available in Jobs API */ - get messages(): Array<[string, any]> { + get messages(): Array<[string, unknown]> { return [] } @@ -348,38 +353,6 @@ export class TaskItemImpl { return this.job.status === 'cancelled' } - /** - * Execution start timestamp - not available in JobListItem - * @deprecated Not available in jobs list API - */ - get executionStartTimestamp(): number | undefined { - return undefined - } - - /** - * Execution end timestamp - not available in JobListItem - * @deprecated Not available in jobs list API - */ - get executionEndTimestamp(): number | undefined { - return undefined - } - - /** - * Execution time in ms - not available in JobListItem - * @deprecated Not available in jobs list API - */ - get executionTime(): number | undefined { - return undefined - } - - /** - * Execution time in seconds - not available in JobListItem - * @deprecated Not available in jobs list API - */ - get executionTimeInSeconds(): number | undefined { - return undefined - } - get isHistory() { return this.taskType === 'History' } @@ -403,6 +376,27 @@ export class TaskItemImpl { } } + get executionStartTimestamp() { + return this.job.execution_start_time ?? undefined + } + + get executionEndTimestamp() { + return this.job.execution_end_time ?? undefined + } + + get executionTime() { + if (!this.executionStartTimestamp || !this.executionEndTimestamp) { + return undefined + } + return this.executionEndTimestamp - this.executionStartTimestamp + } + + get executionTimeInSeconds() { + return this.executionTime !== undefined + ? this.executionTime / 1000 + : undefined + } + /** * Loads full outputs for tasks that only have preview data * Returns a new TaskItemImpl with full outputs and execution status @@ -547,7 +541,7 @@ export const useQueueStore = defineStore('queue', () => { const executionStore = useExecutionStore() appearedTasks.forEach((task) => { const promptIdString = String(task.promptId) - const workflowId = task.workflow?.id + const workflowId = task.workflowId if (workflowId && promptIdString) { executionStore.registerPromptWorkflowIdMapping( promptIdString, diff --git a/tests-ui/tests/composables/useJobList.test.ts b/tests-ui/tests/composables/useJobList.test.ts index 36a14162b..511eff381 100644 --- a/tests-ui/tests/composables/useJobList.test.ts +++ b/tests-ui/tests/composables/useJobList.test.ts @@ -13,7 +13,7 @@ type TestTask = { executionTime?: number executionEndTimestamp?: number createTime?: number - workflow?: { id?: string } + workflowId?: string } const translations: Record = { @@ -185,7 +185,7 @@ const createTask = ( executionTime: overrides.executionTime, executionEndTimestamp: overrides.executionEndTimestamp, createTime: overrides.createTime, - workflow: overrides.workflow + workflowId: overrides.workflowId }) const mountUseJobList = () => { @@ -387,13 +387,13 @@ describe('useJobList', () => { promptId: 'wf-1', queueIndex: 2, mockState: 'pending', - workflow: { id: 'workflow-1' } + workflowId: 'workflow-1' }), createTask({ promptId: 'wf-2', queueIndex: 1, mockState: 'pending', - workflow: { id: 'workflow-2' } + workflowId: 'workflow-2' }) ] diff --git a/tests-ui/tests/composables/useResultGallery.test.ts b/tests-ui/tests/composables/useResultGallery.test.ts index f19501e42..167ba31dc 100644 --- a/tests-ui/tests/composables/useResultGallery.test.ts +++ b/tests-ui/tests/composables/useResultGallery.test.ts @@ -1,71 +1,78 @@ import { describe, it, expect } from 'vitest' import { useResultGallery } from '@/composables/queue/useResultGallery' -import type { JobListItem } from '@/composables/queue/useJobList' -import type { ResultItemImpl } from '@/stores/queueStore' +import type { JobListItem as JobListViewItem } from '@/composables/queue/useJobList' +import type { JobListItem } from '@/platform/remote/comfyui/jobs' +import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore' -type PreviewLike = Pick - -/** - * Mock task interface matching what useResultGallery expects. - * Uses structural typing - no need to import the internal GalleryTask type. - */ -interface MockTask { - readonly promptId: string - readonly outputsCount?: number - readonly flatOutputs: readonly ResultItemImpl[] - readonly previewOutput?: ResultItemImpl - loadFullOutputs( - fetchApi: (url: string) => Promise - ): Promise +const createResultItem = ( + url: string, + supportsPreview = true +): ResultItemImpl => { + const item = new ResultItemImpl({ + filename: url, + subfolder: '', + type: 'output', + nodeId: 'node-1', + mediaType: supportsPreview ? 'images' : 'unknown' + }) + // Override url getter for test matching + Object.defineProperty(item, 'url', { get: () => url }) + Object.defineProperty(item, 'supportsPreview', { get: () => supportsPreview }) + return item } -const createPreview = (url: string, supportsPreview = true): PreviewLike => ({ - url, - supportsPreview +const createMockJob = (id: string, outputsCount = 1): JobListItem => ({ + id, + status: 'completed', + create_time: Date.now(), + preview_output: null, + outputs_count: outputsCount, + priority: 0 }) -const createMockTask = ( - preview?: PreviewLike, - allOutputs?: PreviewLike[] -): MockTask => ({ - previewOutput: preview as ResultItemImpl | undefined, - flatOutputs: (allOutputs ?? (preview ? [preview] : [])) as ResultItemImpl[], - outputsCount: 1, - promptId: `task-${Math.random().toString(36).slice(2)}`, - loadFullOutputs: async () => createMockTask(preview, allOutputs) -}) +const createTask = ( + preview?: ResultItemImpl, + allOutputs?: ResultItemImpl[], + outputsCount = 1 +): TaskItemImpl => { + const job = createMockJob( + `task-${Math.random().toString(36).slice(2)}`, + outputsCount + ) + const flatOutputs = allOutputs ?? (preview ? [preview] : []) + return new TaskItemImpl(job, {}, flatOutputs) +} -const createJobItem = ( +const createJobViewItem = ( id: string, - preview?: PreviewLike, - taskRef?: MockTask -): JobListItem => + taskRef?: TaskItemImpl +): JobListViewItem => ({ id, title: `Job ${id}`, meta: '', state: 'completed', showClear: false, - taskRef: taskRef ?? (preview ? { previewOutput: preview } : undefined) - }) as JobListItem + taskRef + }) as JobListViewItem describe('useResultGallery', () => { it('collects only previewable outputs and preserves their order', async () => { - const previewable = [createPreview('p-1'), createPreview('p-2')] - const nonPreviewable = { url: 'skip-me', supportsPreview: false } + const previewable = [createResultItem('p-1'), createResultItem('p-2')] + const nonPreviewable = createResultItem('skip-me', false) const tasks = [ - createMockTask(previewable[0]), - createMockTask(nonPreviewable), - createMockTask(previewable[1]), - createMockTask() + createTask(previewable[0]), + createTask(nonPreviewable), + createTask(previewable[1]), + createTask() ] const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery( () => tasks ) - await onViewItem(createJobItem('job-1', previewable[0], tasks[0])) + await onViewItem(createJobViewItem('job-1', tasks[0])) expect(galleryItems.value).toEqual([previewable[0]]) expect(galleryActiveIndex.value).toBe(0) @@ -76,7 +83,7 @@ describe('useResultGallery', () => { () => [] ) - await onViewItem(createJobItem('job-missing')) + await onViewItem(createJobViewItem('job-missing')) expect(galleryItems.value).toEqual([]) expect(galleryActiveIndex.value).toBe(-1) @@ -84,81 +91,77 @@ describe('useResultGallery', () => { it('activates the index that matches the viewed preview URL', async () => { const previewable = [ - createPreview('p-1'), - createPreview('p-2'), - createPreview('p-3') + createResultItem('p-1'), + createResultItem('p-2'), + createResultItem('p-3') ] - const tasks = previewable.map((preview) => createMockTask(preview)) + const tasks = previewable.map((preview) => createTask(preview)) const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery( () => tasks ) - const targetPreview = createPreview('p-2') - await onViewItem(createJobItem('job-2', targetPreview, tasks[1])) + await onViewItem(createJobViewItem('job-2', tasks[1])) expect(galleryItems.value).toEqual([previewable[1]]) expect(galleryActiveIndex.value).toBe(0) }) it('defaults to the first entry when the clicked job lacks a preview', async () => { - const previewable = [createPreview('p-1'), createPreview('p-2')] - const tasks = previewable.map((preview) => createMockTask(preview)) + const previewable = [createResultItem('p-1'), createResultItem('p-2')] + const tasks = previewable.map((preview) => createTask(preview)) const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery( () => tasks ) - await onViewItem(createJobItem('job-no-preview')) + await onViewItem(createJobViewItem('job-no-preview')) expect(galleryItems.value).toEqual(previewable) expect(galleryActiveIndex.value).toBe(0) }) it('defaults to the first entry when no gallery item matches the preview URL', async () => { - const previewable = [createPreview('p-1'), createPreview('p-2')] - const tasks = previewable.map((preview) => createMockTask(preview)) + const previewable = [createResultItem('p-1'), createResultItem('p-2')] + const tasks = previewable.map((preview) => createTask(preview)) const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery( () => tasks ) - await onViewItem(createJobItem('job-mismatch', createPreview('missing'))) + const taskWithMismatchedPreview = createTask(createResultItem('missing')) + await onViewItem( + createJobViewItem('job-mismatch', taskWithMismatchedPreview) + ) - expect(galleryItems.value).toEqual(previewable) + expect(galleryItems.value).toEqual([createResultItem('missing')]) expect(galleryActiveIndex.value).toBe(0) }) it('loads full outputs when task has only preview outputs', async () => { - const previewOutput = createPreview('preview-1') + const previewOutput = createResultItem('preview-1') const fullOutputs = [ - createPreview('full-1'), - createPreview('full-2'), - createPreview('full-3') - ] as ResultItemImpl[] + createResultItem('full-1'), + createResultItem('full-2'), + createResultItem('full-3') + ] - const mockTask: MockTask = { - promptId: 'task-1', - previewOutput: previewOutput as ResultItemImpl, - flatOutputs: [previewOutput] as ResultItemImpl[], - outputsCount: 3, // More than 1 triggers lazy loading - loadFullOutputs: async () => ({ - promptId: 'task-1', - previewOutput: previewOutput as ResultItemImpl, - flatOutputs: fullOutputs, - outputsCount: 3, - loadFullOutputs: async () => mockTask - }) - } + // Create a task with outputsCount > 1 to trigger lazy loading + const job = createMockJob('task-1', 3) + const task = new TaskItemImpl(job, {}, [previewOutput]) + + // Mock loadFullOutputs to return full outputs + const loadedTask = new TaskItemImpl(job, {}, fullOutputs) + task.loadFullOutputs = async () => loadedTask const mockFetchApi = async () => new Response() const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery( - () => [mockTask], + () => [task], mockFetchApi ) - await onViewItem(createJobItem('job-1', previewOutput, mockTask)) + await onViewItem(createJobViewItem('job-1', task)) expect(galleryItems.value).toEqual(fullOutputs) expect(galleryActiveIndex.value).toBe(0)