mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +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:
98
src/utils/executionIdUtil.test.ts
Normal file
98
src/utils/executionIdUtil.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
|
import {
|
||||||
|
buildSubgraphExecutionPaths,
|
||||||
|
getAncestorExecutionIds,
|
||||||
|
getParentExecutionIds
|
||||||
|
} from '@/utils/executionIdUtil'
|
||||||
|
|
||||||
|
function node(id: number, type: string): ComfyNode {
|
||||||
|
return { id, type } as ComfyNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function subgraphDef(id: string, nodes: ComfyNode[]) {
|
||||||
|
return { id, name: id, nodes, inputNode: {}, outputNode: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getAncestorExecutionIds', () => {
|
||||||
|
it('returns only itself for a root node', () => {
|
||||||
|
expect(getAncestorExecutionIds('65')).toEqual(['65'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all ancestors including self for nested IDs', () => {
|
||||||
|
expect(getAncestorExecutionIds('65:70')).toEqual(['65', '65:70'])
|
||||||
|
expect(getAncestorExecutionIds('65:70:63')).toEqual([
|
||||||
|
'65',
|
||||||
|
'65:70',
|
||||||
|
'65:70:63'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getParentExecutionIds', () => {
|
||||||
|
it('returns empty for a root node', () => {
|
||||||
|
expect(getParentExecutionIds('65')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all ancestors excluding self for nested IDs', () => {
|
||||||
|
expect(getParentExecutionIds('65:70')).toEqual(['65'])
|
||||||
|
expect(getParentExecutionIds('65:70:63')).toEqual(['65', '65:70'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildSubgraphExecutionPaths', () => {
|
||||||
|
it('returns empty map when there are no subgraph definitions', () => {
|
||||||
|
expect(buildSubgraphExecutionPaths([node(5, 'SomeNode')], [])).toEqual(
|
||||||
|
new Map()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty map when no root node matches a subgraph type', () => {
|
||||||
|
const def = subgraphDef('def-A', [])
|
||||||
|
expect(
|
||||||
|
buildSubgraphExecutionPaths([node(5, 'UnrelatedNode')], [def])
|
||||||
|
).toEqual(new Map())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maps a single subgraph instance to its execution path', () => {
|
||||||
|
const def = subgraphDef('def-A', [])
|
||||||
|
const result = buildSubgraphExecutionPaths([node(5, 'def-A')], [def])
|
||||||
|
expect(result.get('def-A')).toEqual(['5'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collects multiple instances of the same subgraph type', () => {
|
||||||
|
const def = subgraphDef('def-A', [])
|
||||||
|
const result = buildSubgraphExecutionPaths(
|
||||||
|
[node(5, 'def-A'), node(10, 'def-A')],
|
||||||
|
[def]
|
||||||
|
)
|
||||||
|
expect(result.get('def-A')).toEqual(['5', '10'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds nested execution paths for subgraphs within subgraphs', () => {
|
||||||
|
const innerDef = subgraphDef('def-B', [])
|
||||||
|
const outerDef = subgraphDef('def-A', [node(70, 'def-B')])
|
||||||
|
const result = buildSubgraphExecutionPaths(
|
||||||
|
[node(5, 'def-A')],
|
||||||
|
[outerDef, innerDef]
|
||||||
|
)
|
||||||
|
expect(result.get('def-A')).toEqual(['5'])
|
||||||
|
expect(result.get('def-B')).toEqual(['5:70'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not recurse infinitely on self-referential subgraph definitions', () => {
|
||||||
|
const cyclicDef = subgraphDef('def-A', [node(70, 'def-A')])
|
||||||
|
expect(() =>
|
||||||
|
buildSubgraphExecutionPaths([node(5, 'def-A')], [cyclicDef])
|
||||||
|
).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not recurse infinitely on mutually cyclic subgraph definitions', () => {
|
||||||
|
const defA = subgraphDef('def-A', [node(70, 'def-B')])
|
||||||
|
const defB = subgraphDef('def-B', [node(80, 'def-A')])
|
||||||
|
expect(() =>
|
||||||
|
buildSubgraphExecutionPaths([node(5, 'def-A')], [defA, defB])
|
||||||
|
).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
71
src/utils/executionIdUtil.ts
Normal file
71
src/utils/executionIdUtil.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||||
|
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
|
import { isSubgraphDefinition } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all ancestor execution IDs for a given execution ID, including itself.
|
||||||
|
*
|
||||||
|
* Example: "65:70:63" → ["65", "65:70", "65:70:63"]
|
||||||
|
* @knipIgnoreUsedByStackedPR
|
||||||
|
*/
|
||||||
|
export function getAncestorExecutionIds(
|
||||||
|
executionId: string | NodeExecutionId
|
||||||
|
): NodeExecutionId[] {
|
||||||
|
const parts = executionId.split(':')
|
||||||
|
return Array.from({ length: parts.length }, (_, i) =>
|
||||||
|
parts.slice(0, i + 1).join(':')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all ancestor execution IDs for a given execution ID, excluding itself.
|
||||||
|
*
|
||||||
|
* Example: "65:70:63" → ["65", "65:70"]
|
||||||
|
* @knipIgnoreUsedByStackedPR
|
||||||
|
*/
|
||||||
|
export function getParentExecutionIds(
|
||||||
|
executionId: string | NodeExecutionId
|
||||||
|
): NodeExecutionId[] {
|
||||||
|
return getAncestorExecutionIds(executionId).slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "def-A" → ["5", "10"] for each container node instantiating that subgraph definition.
|
||||||
|
* @knipIgnoreUsedByStackedPR
|
||||||
|
*/
|
||||||
|
export function buildSubgraphExecutionPaths(
|
||||||
|
rootNodes: ComfyNode[],
|
||||||
|
allSubgraphDefs: unknown[]
|
||||||
|
): Map<string, string[]> {
|
||||||
|
const subgraphDefMap = new Map(
|
||||||
|
allSubgraphDefs.filter(isSubgraphDefinition).map((s) => [s.id, s])
|
||||||
|
)
|
||||||
|
const pathMap = new Map<string, string[]>()
|
||||||
|
const visited = new Set<string>()
|
||||||
|
|
||||||
|
const build = (nodes: ComfyNode[], parentPrefix: string) => {
|
||||||
|
for (const n of nodes ?? []) {
|
||||||
|
if (typeof n.type !== 'string' || !subgraphDefMap.has(n.type)) continue
|
||||||
|
const path = parentPrefix ? `${parentPrefix}:${n.id}` : String(n.id)
|
||||||
|
const existing = pathMap.get(n.type)
|
||||||
|
if (existing) {
|
||||||
|
existing.push(path)
|
||||||
|
} else {
|
||||||
|
pathMap.set(n.type, [path])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visited.has(n.type)) continue
|
||||||
|
visited.add(n.type)
|
||||||
|
|
||||||
|
const innerDef = subgraphDefMap.get(n.type)
|
||||||
|
if (innerDef) {
|
||||||
|
build(innerDef.nodes, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.delete(n.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build(rootNodes, '')
|
||||||
|
return pathMap
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, useScroll, whenever } from '@vueuse/core'
|
import { useScroll, whenever } from '@vueuse/core'
|
||||||
import Panel from 'primevue/panel'
|
import Panel from 'primevue/panel'
|
||||||
import TabMenu from 'primevue/tabmenu'
|
import TabMenu from 'primevue/tabmenu'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
@@ -8,18 +8,12 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||||
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useApplyChanges } from '@/workbench/extensions/manager/composables/useApplyChanges'
|
||||||
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'
|
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
const comfyManagerStore = useComfyManagerStore()
|
||||||
const settingStore = useSettingStore()
|
const { isRestarting, isRestartCompleted, applyChanges } = useApplyChanges()
|
||||||
const { runFullConflictAnalysis } = useConflictDetection()
|
|
||||||
|
|
||||||
const isExpanded = ref(false)
|
const isExpanded = ref(false)
|
||||||
const activeTabIndex = ref(0)
|
const activeTabIndex = ref(0)
|
||||||
@@ -42,9 +36,6 @@ const focusedLogs = computed(() => {
|
|||||||
|
|
||||||
const visible = computed(() => comfyManagerStore.taskLogs.length > 0)
|
const visible = computed(() => comfyManagerStore.taskLogs.length > 0)
|
||||||
|
|
||||||
const isRestarting = ref(false)
|
|
||||||
const isRestartCompleted = ref(false)
|
|
||||||
|
|
||||||
const isInProgress = computed(
|
const isInProgress = computed(
|
||||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||||
)
|
)
|
||||||
@@ -148,47 +139,10 @@ function closeToast() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRestart() {
|
async function handleRestart() {
|
||||||
const originalToastSetting = settingStore.get(
|
|
||||||
'Comfy.Toast.DisableReconnectingToast'
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
await applyChanges(closeToast)
|
||||||
|
} catch (err) {
|
||||||
isRestarting.value = true
|
console.error('[ManagerProgressToast] Restart failed:', err)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,21 +27,15 @@ import DotSpinner from '@/components/common/DotSpinner.vue'
|
|||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import type { ButtonVariants } from '@/components/ui/button/button.variants'
|
import type { ButtonVariants } from '@/components/ui/button/button.variants'
|
||||||
import type { components } from '@/types/comfyRegistryTypes'
|
import type { components } from '@/types/comfyRegistryTypes'
|
||||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||||
import { useNodeConflictDialog } from '@/workbench/extensions/manager/composables/useNodeConflictDialog'
|
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
|
||||||
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'
|
|
||||||
|
|
||||||
type NodePack = components['schemas']['Node']
|
type NodePack = components['schemas']['Node']
|
||||||
|
|
||||||
const {
|
const {
|
||||||
nodePacks,
|
nodePacks,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
label = 'Install',
|
label,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
hasConflict,
|
hasConflict,
|
||||||
conflictInfo
|
conflictInfo
|
||||||
@@ -54,86 +48,13 @@ const {
|
|||||||
conflictInfo?: ConflictDetail[]
|
conflictInfo?: ConflictDetail[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const managerStore = useComfyManagerStore()
|
|
||||||
const { show: showNodeConflictDialog } = useNodeConflictDialog()
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Check if any of the packs are currently being installed
|
const { isInstalling, installAllPacks } = usePackInstall(
|
||||||
const isInstalling = computed(() => {
|
() => nodePacks,
|
||||||
if (!nodePacks?.length) return false
|
() => hasConflict,
|
||||||
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
|
() => conflictInfo
|
||||||
})
|
)
|
||||||
|
|
||||||
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 computedLabel = computed(() =>
|
const computedLabel = computed(() =>
|
||||||
isInstalling.value
|
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