mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 01:34:07 +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">
|
||||
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