Compare commits

...

1 Commits

Author SHA1 Message Date
Matt Miller
8d9b95473e refactor: extract shared missing-candidate helpers
Extract the byte-identical abort-controller factory and the two pure
Set-deriving helpers (ancestor execution IDs, active graph IDs) shared by
missingModelStore and missingMediaStore into a new
platform/missing/missingCandidateHelpers module. Each store's divergent
remove/clear/state logic is left untouched.
2026-07-02 12:33:46 -07:00
4 changed files with 197 additions and 69 deletions

View File

@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
computeActiveGraphIds,
computeAncestorExecutionIds,
createVerificationAbortController
} from './missingCandidateHelpers'
const mocks = vi.hoisted(() => ({
rootGraph: null as unknown,
getActiveGraphNodeIds: vi.fn()
}))
vi.mock('@/scripts/app', () => ({
app: {
get rootGraph() {
return mocks.rootGraph
}
}
}))
vi.mock('@/utils/graphTraversalUtil', () => ({
getActiveGraphNodeIds: mocks.getActiveGraphNodeIds
}))
describe('createVerificationAbortController', () => {
it('create returns a fresh, non-aborted controller', () => {
const manager = createVerificationAbortController()
const controller = manager.create()
expect(controller.signal.aborted).toBe(false)
})
it('create aborts the previously issued controller', () => {
const manager = createVerificationAbortController()
const first = manager.create()
manager.create()
expect(first.signal.aborted).toBe(true)
})
it('abort aborts the current controller', () => {
const manager = createVerificationAbortController()
const controller = manager.create()
manager.abort()
expect(controller.signal.aborted).toBe(true)
})
it('abort after abort is a no-op (no current controller)', () => {
const manager = createVerificationAbortController()
manager.create()
manager.abort()
expect(() => manager.abort()).not.toThrow()
})
})
describe('computeAncestorExecutionIds', () => {
it('expands each node id into its execution-id prefixes, inclusive', () => {
const result = computeAncestorExecutionIds(['65:70:63'])
expect([...result]).toEqual(['65', '65:70', '65:70:63'])
})
it('deduplicates shared ancestor prefixes across node ids', () => {
const result = computeAncestorExecutionIds(['65:70', '65:71'])
expect([...result]).toEqual(['65', '65:70', '65:71'])
})
it('returns an empty set for no node ids', () => {
expect(computeAncestorExecutionIds([]).size).toBe(0)
})
})
describe('computeActiveGraphIds', () => {
beforeEach(() => {
mocks.rootGraph = null
mocks.getActiveGraphNodeIds.mockReset()
})
it('returns an empty set when the root graph is unavailable', () => {
const result = computeActiveGraphIds(null, new Set())
expect(result.size).toBe(0)
expect(mocks.getActiveGraphNodeIds).not.toHaveBeenCalled()
})
it('delegates to getActiveGraphNodeIds with the current graph', () => {
const rootGraph = { id: 'root' }
const currentGraph = { id: 'current' }
mocks.rootGraph = rootGraph
mocks.getActiveGraphNodeIds.mockReturnValue(new Set(['1']))
const ancestors = computeAncestorExecutionIds(['65'])
const result = computeActiveGraphIds(currentGraph as never, ancestors)
expect(result).toEqual(new Set(['1']))
expect(mocks.getActiveGraphNodeIds).toHaveBeenCalledWith(
rootGraph,
currentGraph,
ancestors
)
})
it('falls back to the root graph when no current graph is given', () => {
const rootGraph = { id: 'root' }
mocks.rootGraph = rootGraph
mocks.getActiveGraphNodeIds.mockReturnValue(new Set<string>())
computeActiveGraphIds(null, new Set())
expect(mocks.getActiveGraphNodeIds).toHaveBeenCalledWith(
rootGraph,
rootGraph,
expect.any(Set)
)
})
})

View File

@@ -0,0 +1,55 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { getAncestorExecutionIds } from '@/types/nodeIdentification'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { getActiveGraphNodeIds } from '@/utils/graphTraversalUtil'
interface VerificationAbortController {
create(): AbortController
abort(): void
}
export function createVerificationAbortController(): VerificationAbortController {
let controller: AbortController | null = null
return {
create() {
controller?.abort()
controller = new AbortController()
return controller
},
abort() {
controller?.abort()
controller = null
}
}
}
/**
* Set of all execution ID prefixes derived from the given node IDs,
* including the nodes themselves.
*
* Example: node "65:70:63" → Set { "65", "65:70", "65:70:63" }
*/
export function computeAncestorExecutionIds(
nodeIds: Iterable<string>
): Set<NodeExecutionId> {
const ids = new Set<NodeExecutionId>()
for (const nodeId of nodeIds) {
for (const id of getAncestorExecutionIds(nodeId)) {
ids.add(id)
}
}
return ids
}
export function computeActiveGraphIds(
currentGraph: LGraph | null,
ancestorExecutionIds: Set<NodeExecutionId>
): Set<string> {
if (!app.rootGraph) return new Set()
return getActiveGraphNodeIds(
app.rootGraph,
currentGraph ?? app.rootGraph,
ancestorExecutionIds
)
}

View File

@@ -3,11 +3,12 @@ import { computed, ref } from 'vue'
// eslint-disable-next-line import-x/no-restricted-paths
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
import { getAncestorExecutionIds } from '@/types/nodeIdentification'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { getActiveGraphNodeIds } from '@/utils/graphTraversalUtil'
import {
computeActiveGraphIds,
computeAncestorExecutionIds,
createVerificationAbortController
} from '@/platform/missing/missingCandidateHelpers'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
/**
@@ -31,38 +32,18 @@ export const useMissingMediaStore = defineStore('missingMedia', () => {
new Set(missingMediaCandidates.value?.map((m) => String(m.nodeId)) ?? [])
)
/**
* Set of all execution ID prefixes derived from missing media node IDs,
* including the missing media nodes themselves.
*/
const missingMediaAncestorExecutionIds = computed<Set<NodeExecutionId>>(
() => {
const ids = new Set<NodeExecutionId>()
for (const nodeId of missingMediaNodeIds.value) {
for (const id of getAncestorExecutionIds(nodeId)) {
ids.add(id)
}
}
return ids
}
const missingMediaAncestorExecutionIds = computed(() =>
computeAncestorExecutionIds(missingMediaNodeIds.value)
)
const activeMissingMediaGraphIds = computed<Set<string>>(() => {
if (!app.rootGraph) return new Set()
return getActiveGraphNodeIds(
app.rootGraph,
canvasStore.currentGraph ?? app.rootGraph,
const activeMissingMediaGraphIds = computed(() =>
computeActiveGraphIds(
canvasStore.currentGraph,
missingMediaAncestorExecutionIds.value
)
})
)
let _verificationAbortController: AbortController | null = null
function createVerificationAbortController(): AbortController {
_verificationAbortController?.abort()
_verificationAbortController = new AbortController()
return _verificationAbortController
}
const verificationAbortController = createVerificationAbortController()
function setMissingMedia(media: MissingMediaCandidate[]) {
missingMediaCandidates.value = media.length ? media : null
@@ -132,8 +113,7 @@ export const useMissingMediaStore = defineStore('missingMedia', () => {
}
function clearMissingMedia() {
_verificationAbortController?.abort()
_verificationAbortController = null
verificationAbortController.abort()
missingMediaCandidates.value = null
}
@@ -151,7 +131,7 @@ export const useMissingMediaStore = defineStore('missingMedia', () => {
removeMissingMediaByNodeId,
removeMissingMediaByPrefix,
clearMissingMedia,
createVerificationAbortController,
createVerificationAbortController: verificationAbortController.create,
isContainerWithMissingMedia
}

View File

@@ -9,9 +9,12 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { MissingModelCandidate } from '@/platform/missingModel/types'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { getAncestorExecutionIds } from '@/types/nodeIdentification'
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
import { getActiveGraphNodeIds } from '@/utils/graphTraversalUtil'
import {
computeActiveGraphIds,
computeAncestorExecutionIds,
createVerificationAbortController
} from '@/platform/missing/missingCandidateHelpers'
import type { NodeLocatorId } from '@/types/nodeIdentification'
/**
* Missing model error state and interaction state.
@@ -51,32 +54,16 @@ export const useMissingModelStore = defineStore('missingModel', () => {
return keys
})
/**
* Set of all execution ID prefixes derived from missing model node IDs,
* including the missing model nodes themselves.
*
* Example: missing model on node "65:70:63" → Set { "65", "65:70", "65:70:63" }
*/
const missingModelAncestorExecutionIds = computed<Set<NodeExecutionId>>(
() => {
const ids = new Set<NodeExecutionId>()
for (const nodeId of missingModelNodeIds.value) {
for (const id of getAncestorExecutionIds(nodeId)) {
ids.add(id)
}
}
return ids
}
const missingModelAncestorExecutionIds = computed(() =>
computeAncestorExecutionIds(missingModelNodeIds.value)
)
const activeMissingModelGraphIds = computed<Set<string>>(() => {
if (!app.rootGraph) return new Set()
return getActiveGraphNodeIds(
app.rootGraph,
canvasStore.currentGraph ?? app.rootGraph,
const activeMissingModelGraphIds = computed(() =>
computeActiveGraphIds(
canvasStore.currentGraph,
missingModelAncestorExecutionIds.value
)
})
)
// Persists across component re-mounts so that download progress
// survives tab switches within the right-side panel.
@@ -86,13 +73,7 @@ export const useMissingModelStore = defineStore('missingModel', () => {
const folderPaths = ref<Record<string, string[]>>({})
const fileSizes = ref<Record<string, number>>({})
let _verificationAbortController: AbortController | null = null
function createVerificationAbortController(): AbortController {
_verificationAbortController?.abort()
_verificationAbortController = new AbortController()
return _verificationAbortController
}
const verificationAbortController = createVerificationAbortController()
function setMissingModels(models: MissingModelCandidate[]) {
missingModelCandidates.value = models.length ? models : null
@@ -246,8 +227,7 @@ export const useMissingModelStore = defineStore('missingModel', () => {
}
function clearMissingModels() {
_verificationAbortController?.abort()
_verificationAbortController = null
verificationAbortController.abort()
missingModelCandidates.value = null
modelExpandState.value = {}
selectedLibraryModel.value = {}
@@ -298,7 +278,7 @@ export const useMissingModelStore = defineStore('missingModel', () => {
removeMissingModelsBySourceScope,
clearMissingModels,
refreshMissingModels,
createVerificationAbortController,
createVerificationAbortController: verificationAbortController.create,
hasMissingModelOnNode,
isWidgetMissingModel,