mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 17:37:46 +00:00
[refactor] Extract manager composables and execution utils (#9163)
## Summary Extracts inline logic from manager components into dedicated composables and utilities, and adds a cyclic subgraph fix. ## Changes - **`usePackInstall`**: New composable extracted from `PackInstallButton.vue` — handles conflict detection, payload construction, and `Promise.allSettled`-based batch installation - **`useApplyChanges`**: New shared composable extracted from `ManagerProgressToast.vue` — manages ComfyUI restart flow with reconnect timeout and post-reconnect refresh - **`executionIdUtil`**: New utility (`getAncestorExecutionIds`, `getParentExecutionIds`, `buildSubgraphExecutionPaths`) with unit tests; fixes infinite recursion on cyclic subgraph definitions ## Review Focus - `useApplyChanges` reconnect timeout (2 min) and setting restore logic - `buildSubgraphExecutionPaths` visited-set guard for cyclic subgraph defs ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9163-refactor-Extract-manager-composables-and-execution-utils-3116d73d365081f293d3d5484775ad48) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, useScroll, whenever } from '@vueuse/core'
|
||||
import { useScroll, whenever } from '@vueuse/core'
|
||||
import Panel from 'primevue/panel'
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
@@ -8,18 +8,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||
import { useApplyChanges } from '@/workbench/extensions/manager/composables/useApplyChanges'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { runFullConflictAnalysis } = useConflictDetection()
|
||||
const { isRestarting, isRestartCompleted, applyChanges } = useApplyChanges()
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const activeTabIndex = ref(0)
|
||||
@@ -42,9 +36,6 @@ const focusedLogs = computed(() => {
|
||||
|
||||
const visible = computed(() => comfyManagerStore.taskLogs.length > 0)
|
||||
|
||||
const isRestarting = ref(false)
|
||||
const isRestartCompleted = ref(false)
|
||||
|
||||
const isInProgress = computed(
|
||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||
)
|
||||
@@ -148,47 +139,10 @@ function closeToast() {
|
||||
}
|
||||
|
||||
async function handleRestart() {
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
|
||||
isRestarting.value = true
|
||||
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
void runFullConflictAnalysis()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeToast()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeToast()
|
||||
throw error
|
||||
await applyChanges(closeToast)
|
||||
} catch (err) {
|
||||
console.error('[ManagerProgressToast] Restart failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,21 +27,15 @@ import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { ButtonVariants } from '@/components/ui/button/button.variants'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
import { useNodeConflictDialog } from '@/workbench/extensions/manager/composables/useNodeConflictDialog'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type {
|
||||
ConflictDetail,
|
||||
ConflictDetectionResult
|
||||
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const {
|
||||
nodePacks,
|
||||
isLoading = false,
|
||||
label = 'Install',
|
||||
label,
|
||||
size = 'sm',
|
||||
hasConflict,
|
||||
conflictInfo
|
||||
@@ -54,86 +48,13 @@ const {
|
||||
conflictInfo?: ConflictDetail[]
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Check if any of the packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
if (!nodePacks?.length) return false
|
||||
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
|
||||
})
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
if (!installItem.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: (installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']))
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
}
|
||||
}
|
||||
|
||||
const installPack = (item: NodePack) =>
|
||||
managerStore.installPack.call(createPayload(item))
|
||||
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
if (hasConflict && conflictInfo) {
|
||||
// Check each package individually for conflicts
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks
|
||||
.map((pack) => {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
return {
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: compatibilityCheck.hasConflict,
|
||||
conflicts: compatibilityCheck.conflicts,
|
||||
is_compatible: !compatibilityCheck.hasConflict
|
||||
}
|
||||
})
|
||||
.filter((result) => result.has_conflict) // Only show packages with conflicts
|
||||
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// Proceed with installation of uninstalled packages
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// No conflicts or conflicts acknowledged - proceed with installation
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
await Promise.all(packs.map(installPack))
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
const { isInstalling, installAllPacks } = usePackInstall(
|
||||
() => nodePacks,
|
||||
() => hasConflict,
|
||||
() => conflictInfo
|
||||
)
|
||||
|
||||
const computedLabel = computed(() =>
|
||||
isInstalling.value
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
import { useNodeConflictDialog } from '@/workbench/extensions/manager/composables/useNodeConflictDialog'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
export function usePackInstall(
|
||||
getNodePacks: () => NodePack[],
|
||||
getHasConflict?: () => boolean | undefined,
|
||||
getConflictInfo?: () => ConflictDetail[] | undefined
|
||||
) {
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Check if any of the packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
const nodePacks = getNodePacks()
|
||||
if (!nodePacks?.length) return false
|
||||
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
|
||||
})
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
if (!installItem.id) {
|
||||
throw new Error(t('manager.packInstall.nodeIdRequired'))
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: (installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']))
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
}
|
||||
}
|
||||
|
||||
const installPack = (item: NodePack) =>
|
||||
managerStore.installPack.call(createPayload(item))
|
||||
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
try {
|
||||
const results = await Promise.allSettled(packs.map(installPack))
|
||||
const failures = results.filter(
|
||||
(r): r is PromiseRejectedResult => r.status === 'rejected'
|
||||
)
|
||||
if (failures.length) {
|
||||
console.error(
|
||||
'[usePackInstall] Some installations failed:',
|
||||
failures.map((f) => f.reason)
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
managerStore.installPack.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const installAllPacks = async () => {
|
||||
const nodePacks = getNodePacks()
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
const hasConflict = getHasConflict?.()
|
||||
const conflictInfo = getConflictInfo?.()
|
||||
|
||||
if (hasConflict) {
|
||||
if (!conflictInfo) return
|
||||
|
||||
const conflictedPackages = nodePacks
|
||||
.map((pack) => {
|
||||
const compatibilityCheck = checkNodeCompatibility(pack)
|
||||
return {
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: compatibilityCheck.hasConflict,
|
||||
conflicts: compatibilityCheck.conflicts,
|
||||
is_compatible: !compatibilityCheck.hasConflict
|
||||
}
|
||||
})
|
||||
.filter((result) => result.has_conflict)
|
||||
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
await performInstallation(uninstalledPacks)
|
||||
}
|
||||
|
||||
return { isInstalling, installAllPacks, performInstallation }
|
||||
}
|
||||
118
src/workbench/extensions/manager/composables/useApplyChanges.ts
Normal file
118
src/workbench/extensions/manager/composables/useApplyChanges.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createSharedComposable, useEventListener } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
|
||||
export const useApplyChanges = createSharedComposable(() => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { runFullConflictAnalysis } = useConflictDetection()
|
||||
|
||||
const isRestarting = ref(false)
|
||||
const isRestartCompleted = ref(false)
|
||||
|
||||
async function applyChanges(onClose?: () => void) {
|
||||
if (isRestarting.value) return
|
||||
|
||||
isRestarting.value = true
|
||||
isRestartCompleted.value = false
|
||||
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
runFullConflictAnalysis().catch((err) => {
|
||||
console.error('[useApplyChanges] Conflict analysis failed:', err)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[useApplyChanges] Post-reconnect tasks failed:', err)
|
||||
} finally {
|
||||
try {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'[useApplyChanges] Failed to restore reconnect toast setting:',
|
||||
err
|
||||
)
|
||||
}
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
onClose?.()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
let hasReconnected = false
|
||||
const stopReconnectListener = useEventListener(
|
||||
api,
|
||||
'reconnected',
|
||||
() => {
|
||||
clearTimeout(reconnectTimeout)
|
||||
hasReconnected = true
|
||||
void onReconnect()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
const RECONNECT_TIMEOUT_MS = 120_000 // 2 minutes
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
reconnectTimeout = setTimeout(async () => {
|
||||
if (hasReconnected) return
|
||||
stopReconnectListener()
|
||||
try {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'[useApplyChanges] Failed to restore reconnect toast setting:',
|
||||
err
|
||||
)
|
||||
}
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
console.error('[useApplyChanges] Reconnect timed out')
|
||||
}, RECONNECT_TIMEOUT_MS)
|
||||
} catch (error) {
|
||||
stopReconnectListener()
|
||||
try {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
} catch (restoreErr) {
|
||||
console.error(
|
||||
'[useApplyChanges] Failed to restore reconnect toast setting:',
|
||||
restoreErr
|
||||
)
|
||||
} finally {
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
onClose?.()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return { isRestarting, isRestartCompleted, applyChanges }
|
||||
})
|
||||
Reference in New Issue
Block a user