[manager] Update composables and state management

Updated manager queue composable, server logs composable, workflow packs composable, and manager store to support the new manager API structure and state management patterns.
This commit is contained in:
bymyself
2025-06-13 13:56:27 -07:00
committed by Jin Yi
parent 3db88b7172
commit 42a7deae2b
4 changed files with 308 additions and 133 deletions

View File

@@ -7,7 +7,7 @@ import { app } from '@/scripts/app'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useNodeDefStore } from '@/stores/nodeDefStore' import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes' import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { collectAllNodes } from '@/utils/graphTraversalUtil' import { collectAllNodes } from '@/utils/graphTraversalUtil'
@@ -66,8 +66,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
return { return {
id: CORE_NODES_PACK_NAME, id: CORE_NODES_PACK_NAME,
version: version:
systemStatsStore.systemStats?.system?.comfyui_version ?? systemStatsStore.systemStats?.system?.comfyui_version ?? 'nightly'
SelectedVersion.NIGHTLY
} }
} }
@@ -77,7 +76,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
if (pack) { if (pack) {
return { return {
id: pack.id, id: pack.id,
version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY version: pack.latest_version?.version ?? 'nightly'
} }
} }

View File

@@ -1,101 +1,114 @@
import { useEventListener, whenever } from '@vueuse/core' import { useEventListener, whenever } from '@vueuse/core'
import { computed, readonly, ref } from 'vue' import { Ref, computed, ref } from 'vue'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { ManagerWsQueueStatus } from '@/types/comfyManagerTypes' import { useDialogService } from '@/services/dialogService'
import { components } from '@/types/generatedManagerTypes'
type QueuedTask<T> = { type ManagerTaskHistory = Record<
task: () => Promise<T> string,
onComplete?: () => void components['schemas']['TaskHistoryItem']
} >
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
const MANAGER_WS_MSG_TYPE = 'cm-queue-status' const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
export const useManagerQueue = () => { export const useManagerQueue = (
const clientQueueItems = ref<QueuedTask<unknown>[]>([]) taskHistory: Ref<ManagerTaskHistory>,
const clientQueueLength = computed(() => clientQueueItems.value.length) taskQueue: Ref<ManagerTaskQueue>,
const onCompletedQueue = ref<((() => void) | undefined)[]>([]) installedPacks: Ref<Record<string, any>>
const onCompleteWaitingCount = ref(0) ) => {
const uncompletedCount = computed( const { showManagerProgressDialog } = useDialogService()
() => clientQueueLength.value + onCompleteWaitingCount.value
// Task queue state (read-only from server)
const maxHistoryItems = ref(64)
const isLoading = ref(false)
const isProcessing = ref(false)
// Computed values
const currentQueueLength = computed(
() =>
taskQueue.value.running_queue.length +
taskQueue.value.pending_queue.length
) )
const serverQueueStatus = ref<ManagerWsQueueStatus>(ManagerWsQueueStatus.DONE) const updateProcessingState = () => {
const isServerIdle = computed( isProcessing.value = currentQueueLength.value > 0
() => serverQueueStatus.value === ManagerWsQueueStatus.DONE }
)
const allTasksDone = computed( const allTasksDone = computed(
() => isServerIdle.value && clientQueueLength.value === 0 () => !isProcessing.value && currentQueueLength.value === 0
)
const nextTaskReady = computed(
() => isServerIdle.value && clientQueueLength.value > 0
) )
const historyCount = computed(() => Object.keys(taskHistory.value).length)
const cleanupListener = useEventListener( // WebSocket event listener for task done
const cleanupTaskDoneListener = useEventListener(
api, api,
MANAGER_WS_MSG_TYPE, MANAGER_WS_TASK_DONE_NAME,
(event: CustomEvent<{ status: ManagerWsQueueStatus }>) => { (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
if (event?.type === MANAGER_WS_MSG_TYPE && event.detail?.status) { if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
serverQueueStatus.value = event.detail.status const { state } = event.detail
taskQueue.value.running_queue = state.running_queue
taskQueue.value.pending_queue = state.pending_queue
taskHistory.value = state.history
if (state.installed_packs) {
console.log(
'Updating installedPacks from WebSocket:',
Object.keys(state.installed_packs)
)
installedPacks.value = state.installed_packs
}
updateProcessingState()
} }
} }
) )
const startNextTask = () => { // WebSocket event listener for task started
const nextTask = clientQueueItems.value.shift() const cleanupTaskStartedListener = useEventListener(
if (!nextTask) return api,
MANAGER_WS_TASK_STARTED_NAME,
const { task, onComplete } = nextTask (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
if (onComplete) { if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
// Set the task's onComplete to be executed the next time the server is idle const { state } = event.detail
onCompletedQueue.value.push(onComplete) taskQueue.value.running_queue = state.running_queue
onCompleteWaitingCount.value++ taskQueue.value.pending_queue = state.pending_queue
} taskHistory.value = state.history
if (state.installed_packs) {
task().catch((e) => { console.log(
const message = `Error enqueuing task for ComfyUI Manager: ${e}` 'Updating installedPacks from WebSocket:',
console.error(message) Object.keys(state.installed_packs)
}) )
} installedPacks.value = state.installed_packs
}
const enqueueTask = <T>(task: QueuedTask<T>): void => { updateProcessingState()
clientQueueItems.value.push(task)
}
const clearQueue = () => {
clientQueueItems.value = []
onCompletedQueue.value = []
onCompleteWaitingCount.value = 0
}
const cleanup = () => {
clearQueue()
cleanupListener()
}
whenever(nextTaskReady, startNextTask)
whenever(isServerIdle, () => {
if (onCompletedQueue.value?.length) {
while (
onCompleteWaitingCount.value > 0 &&
onCompletedQueue.value.length > 0
) {
const onComplete = onCompletedQueue.value.shift()
onComplete?.()
onCompleteWaitingCount.value--
} }
} }
}) )
whenever(currentQueueLength, () => showManagerProgressDialog())
const stopListening = () => {
cleanupTaskDoneListener()
cleanupTaskStartedListener()
}
return { return {
allTasksDone, // Queue state (read-only from server)
statusMessage: readonly(serverQueueStatus), taskHistory,
queueLength: clientQueueLength, taskQueue,
uncompletedCount, maxHistoryItems,
isLoading,
enqueueTask, // Computed state
clearQueue, allTasksDone,
cleanup isProcessing,
queueLength: currentQueueLength,
historyCount,
// Actions
stopListening
} }
} }

View File

@@ -1,24 +1,30 @@
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import { onUnmounted, ref } from 'vue' import { ref } from 'vue'
import { LogsWsMessage } from '@/schemas/apiSchema' import { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { components } from '@/types/generatedManagerTypes'
const LOGS_MESSAGE_TYPE = 'logs' const LOGS_MESSAGE_TYPE = 'logs'
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
interface UseServerLogsOptions { interface UseServerLogsOptions {
ui_id: string
immediate?: boolean immediate?: boolean
messageFilter?: (message: string) => boolean messageFilter?: (message: string) => boolean
} }
export const useServerLogs = (options: UseServerLogsOptions = {}) => { export const useServerLogs = (options: UseServerLogsOptions) => {
const { const {
immediate = false, immediate = false,
messageFilter = (msg: string) => Boolean(msg.trim()) messageFilter = (msg: string) => Boolean(msg.trim())
} = options } = options
const logs = ref<string[]>([]) const logs = ref<string[]>([])
let stop: ReturnType<typeof useEventListener> | null = null let stopLogs: ReturnType<typeof useEventListener> | null = null
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) => const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0 event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
@@ -28,33 +34,55 @@ export const useServerLogs = (options: UseServerLogsOptions = {}) => {
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => { const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
if (isValidLogEvent(event)) { if (isValidLogEvent(event)) {
logs.value.push(...parseLogMessage(event)) const messages = parseLogMessage(event)
if (messages.length > 0) {
logs.value.push(...messages)
}
} }
} }
const start = async () => { const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
const { state } = event.detail
// Check if our task is now in the history (completed)
if (state.history[options.ui_id]) {
void stopListening()
}
}
}
const startListening = async () => {
await api.subscribeLogs(true) await api.subscribeLogs(true)
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage) stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
stopTaskDone = useEventListener(
api,
MANAGER_WS_TASK_DONE_NAME,
handleTaskDone
)
} }
const stopListening = async () => { const stopListening = async () => {
stop?.() console.log('stopListening')
stop = null stopLogs?.()
stopTaskDone?.()
stopLogs = null
stopTaskDone = null
await api.subscribeLogs(false) await api.subscribeLogs(false)
} }
if (immediate) { if (immediate) {
void start() void startListening()
} }
onUnmounted(async () => { const cleanup = async () => {
await stopListening() await stopListening()
logs.value = [] logs.value = []
}) }
return { return {
logs, logs,
startListening: start, startListening,
stopListening stopListening,
cleanup
} }
} }

View File

@@ -1,21 +1,28 @@
import { whenever } from '@vueuse/core' import { whenever } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useCachedRequest } from '@/composables/useCachedRequest' import { useCachedRequest } from '@/composables/useCachedRequest'
import { useManagerQueue } from '@/composables/useManagerQueue' import { useManagerQueue } from '@/composables/useManagerQueue'
import { useServerLogs } from '@/composables/useServerLogs' import { useServerLogs } from '@/composables/useServerLogs'
import { api } from '@/scripts/api'
import { useComfyManagerService } from '@/services/comfyManagerService' import { useComfyManagerService } from '@/services/comfyManagerService'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { import { TaskLog } from '@/types/comfyManagerTypes'
InstallPackParams, import { components } from '@/types/generatedManagerTypes'
InstalledPacksResponse,
ManagerPackInfo, type InstallPackParams = components['schemas']['InstallPackParams']
ManagerPackInstalled, type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
TaskLog, type ManagerPackInfo = components['schemas']['ManagerPackInfo']
UpdateAllPacksParams type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
} from '@/types/comfyManagerTypes' type ManagerTaskHistory = Record<
string,
components['schemas']['TaskHistoryItem']
>
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
/** /**
* Store for state of installed node packs * Store for state of installed node packs
@@ -31,14 +38,63 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
const installedPacksIds = ref<Set<string>>(new Set()) const installedPacksIds = ref<Set<string>>(new Set())
const isStale = ref(true) const isStale = ref(true)
const taskLogs = ref<TaskLog[]>([]) const taskLogs = ref<TaskLog[]>([])
const succeededTasksLogs = ref<TaskLog[]>([])
const failedTasksLogs = ref<TaskLog[]>([])
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } = const taskHistory = ref<ManagerTaskHistory>({})
useManagerQueue() const succeededTasksIds = ref<string[]>([])
const failedTasksIds = ref<string[]>([])
const taskQueue = ref<ManagerTaskQueue>({
history: {},
running_queue: [],
pending_queue: [],
installed_packs: {}
})
const managerQueue = useManagerQueue(taskHistory, taskQueue, installedPacks)
const setStale = () => { const setStale = () => {
isStale.value = true isStale.value = true
} }
const partitionTaskLogs = () => {
const successTaskLogs: TaskLog[] = []
const failTaskLogs: TaskLog[] = []
for (const log of taskLogs.value) {
if (failedTasksIds.value.includes(log.taskId)) {
failTaskLogs.push(log)
} else {
successTaskLogs.push(log)
}
}
succeededTasksLogs.value = successTaskLogs
failedTasksLogs.value = failTaskLogs
}
const partitionTasks = () => {
const successTasksIds = []
const failTasksIds = []
for (const task of Object.values(taskHistory.value)) {
if (task.status?.status_str === 'success') {
successTasksIds.push(task.ui_id)
} else {
failTasksIds.push(task.ui_id)
}
}
succeededTasksIds.value = successTasksIds
failedTasksIds.value = failTasksIds
}
whenever(
taskHistory,
() => {
partitionTasks()
partitionTaskLogs()
console.log('installed pack ids', installedPacksIds.value)
},
{ deep: true }
)
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
const isInstalledPackId = (packName: string | undefined): boolean => const isInstalledPackId = (packName: string | undefined): boolean =>
@@ -97,11 +153,27 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
} }
const updateInstalledIds = (packs: ManagerPackInstalled[]) => { const updateInstalledIds = (packs: ManagerPackInstalled[]) => {
installedPacksIds.value = packsToIdSet(packs) const newIds = packsToIdSet(packs)
console.log('updateInstalledIds: creating set with:', Array.from(newIds))
installedPacksIds.value = newIds
console.log(
'updateInstalledIds: final installedPacksIds:',
Array.from(installedPacksIds.value)
)
} }
const onPacksChanged = () => { const onPacksChanged = () => {
const packs = Object.values(installedPacks.value) const packs = Object.values(installedPacks.value)
console.log(
'onPacksChanged called with packs:',
packs.map((p) => ({
key: Object.keys(installedPacks.value).find(
(k) => installedPacks.value[k] === p
),
cnr_id: p.cnr_id,
aux_id: p.aux_id
}))
)
updateDisabledIds(packs) updateDisabledIds(packs)
updateInstalledIds(packs) updateInstalledIds(packs)
} }
@@ -115,23 +187,46 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
} }
whenever(isStale, refreshInstalledList, { immediate: true }) whenever(isStale, refreshInstalledList, { immediate: true })
whenever(uncompletedCount, () => showManagerProgressDialog())
const withLogs = (task: () => Promise<null>, taskName: string) => { const enqueueTaskWithLogs = async (
const { startListening, stopListening, logs } = useServerLogs() task: (taskId: string) => Promise<null>,
taskName: string
) => {
const taskId = uuidv4()
const { startListening, logs } = useServerLogs({
ui_id: taskId
})
const loggedTask = async () => { try {
taskLogs.value.push({ taskName, logs: logs.value }) // Show progress dialog immediately when task is queued
showManagerProgressDialog()
managerQueue.isProcessing.value = true
// Prepare logging hook
taskLogs.value.push({ taskName, taskId, logs: logs.value })
await startListening() await startListening()
return task()
}
const onComplete = async () => { // Queue the task to the server
await stopListening() await task(taskId)
setStale() } catch (error) {
} // Reset processing state on error
managerQueue.isProcessing.value = false
return { task: loggedTask, onComplete } // The server has authority over task history in general, but in rare
// case of client-side error, we add that to failed tasks from the client side
taskHistory.value[taskId] = {
ui_id: taskId,
client_id: api.clientId || 'unknown',
kind: 'error',
result: 'failed',
status: {
status_str: 'error',
completed: false,
messages: [error instanceof Error ? error.message : String(error)]
},
timestamp: new Date().toISOString()
}
}
} }
const installPack = useCachedRequest<InstallPackParams, void>( const installPack = useCachedRequest<InstallPackParams, void>(
@@ -152,39 +247,62 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
} }
} }
const task = () => managerService.installPack(params, signal) const task = (taskId: string) =>
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`)) managerService.installPack(params, taskId, signal)
await enqueueTaskWithLogs(task, `${actionDescription} ${params.id}`)
}, },
{ maxSize: 1 } { maxSize: 1 }
) )
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => { const uninstallPack = async (
params: ManagerPackInfo,
signal?: AbortSignal
) => {
installPack.clear() installPack.clear()
installPack.cancel() installPack.cancel()
const task = () => managerService.uninstallPack(params, signal) const uninstallParams: components['schemas']['UninstallPackParams'] = {
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id }))) node_name: params.id,
is_unknown: false
}
const task = (taskId: string) =>
managerService.uninstallPack(uninstallParams, taskId, signal)
await enqueueTaskWithLogs(
task,
t('manager.uninstalling', { id: params.id })
)
} }
const updatePack = useCachedRequest<ManagerPackInfo, void>( const updatePack = useCachedRequest<ManagerPackInfo, void>(
async (params: ManagerPackInfo, signal?: AbortSignal) => { async (params: ManagerPackInfo, signal?: AbortSignal) => {
updateAllPacks.cancel() updateAllPacks.cancel()
const task = () => managerService.updatePack(params, signal) const updateParams: components['schemas']['UpdatePackParams'] = {
enqueueTask(withLogs(task, t('g.updating', { id: params.id }))) node_name: params.id,
node_ver: params.version
}
const task = (taskId: string) =>
managerService.updatePack(updateParams, taskId, signal)
await enqueueTaskWithLogs(task, t('g.updating', { id: params.id }))
}, },
{ maxSize: 1 } { maxSize: 1 }
) )
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>( const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
async (params: UpdateAllPacksParams, signal?: AbortSignal) => { async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
const task = () => managerService.updateAllPacks(params, signal) const task = (taskId: string) =>
enqueueTask(withLogs(task, t('manager.updatingAllPacks'))) managerService.updateAllPacks(params, taskId, signal)
await enqueueTaskWithLogs(task, t('manager.updatingAllPacks'))
}, },
{ maxSize: 1 } { maxSize: 1 }
) )
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => { const disablePack = async (params: ManagerPackInfo, signal?: AbortSignal) => {
const task = () => managerService.disablePack(params, signal) const disableParams: components['schemas']['DisablePackParams'] = {
enqueueTask(withLogs(task, t('g.disabling', { id: params.id }))) node_name: params.id,
is_unknown: false
}
const task = (taskId: string) =>
managerService.disablePack(disableParams, taskId, signal)
await enqueueTaskWithLogs(task, t('g.disabling', { id: params.id }))
} }
const getInstalledPackVersion = (packId: string) => { const getInstalledPackVersion = (packId: string) => {
@@ -200,9 +318,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
// Manager state // Manager state
isLoading: managerService.isLoading, isLoading: managerService.isLoading,
error: managerService.error, error: managerService.error,
statusMessage,
allTasksDone,
uncompletedCount,
taskLogs, taskLogs,
clearLogs, clearLogs,
setStale, setStale,
@@ -215,6 +330,15 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
getInstalledPackVersion, getInstalledPackVersion,
refreshInstalledList, refreshInstalledList,
// Task queue state and actions
taskHistory,
isProcessingTasks: managerQueue.isProcessing,
succeededTasksIds,
failedTasksIds,
succeededTasksLogs,
failedTasksLogs,
managerQueue, // Expose full queue composable for advanced usage
// Pack actions // Pack actions
installPack, installPack,
uninstallPack, uninstallPack,
@@ -234,6 +358,15 @@ export const useManagerProgressDialogStore = defineStore(
'managerProgressDialog', 'managerProgressDialog',
() => { () => {
const isExpanded = ref(false) const isExpanded = ref(false)
const activeTabIndex = ref(0)
const setActiveTabIndex = (index: number) => {
activeTabIndex.value = index
}
const getActiveTabIndex = () => {
return activeTabIndex.value
}
const toggle = () => { const toggle = () => {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value
@@ -250,7 +383,9 @@ export const useManagerProgressDialogStore = defineStore(
isExpanded, isExpanded,
toggle, toggle,
collapse, collapse,
expand expand,
setActiveTabIndex,
getActiveTabIndex
} }
} }
) )