Compare commits

...

1 Commits

Author SHA1 Message Date
huang47
ef478f1cdc refactor: thread validated state through subgraph save 2026-06-30 22:40:39 -07:00
2 changed files with 53 additions and 16 deletions

View File

@@ -285,6 +285,26 @@ describe('useSubgraphStore', () => {
consoleSpy.mockRestore()
})
it('should reject blueprints without a root node', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await mockFetch({
'empty-node.json': {
...mockGraph,
nodes: []
}
})
const error = consoleSpy.mock.calls.find(
([message]) => message === 'Failed to load subgraph blueprint'
)?.[1]
expect(error).toBeInstanceOf(TypeError)
expect((error as Error).message).toBe(
"Subgraph blueprint 'empty-node' must contain a root node"
)
expect(store.subgraphBlueprints).toHaveLength(0)
consoleSpy.mockRestore()
})
it('should handle global blueprint with rejected data promise gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await mockFetch(

View File

@@ -39,6 +39,10 @@ async function confirmOverwrite(name: string): Promise<boolean | null> {
})
}
type ValidSubgraphWorkflowJSON = ComfyWorkflowJSON & {
definitions: NonNullable<ComfyWorkflowJSON['definitions']>
}
export const useSubgraphStore = defineStore('subgraph', () => {
class SubgraphBlueprint extends ComfyWorkflow {
static override readonly basePath = 'subgraphs/'
@@ -54,18 +58,20 @@ export const useSubgraphStore = defineStore('subgraph', () => {
this.hasPromptedSave = !confirmFirstSave
}
validateSubgraph() {
if (!this.activeState?.definitions)
validateSubgraph(): ValidSubgraphWorkflowJSON {
const activeState = this.activeState
if (!activeState?.definitions)
throw new Error(
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
)
const { subgraphs } = this.activeState.definitions
const { nodes } = this.activeState
const validState = activeState as ValidSubgraphWorkflowJSON
const { subgraphs } = validState.definitions
const { nodes } = validState
//Instanceof doesn't function as nodes are serialized
function isSubgraphNode(node: ComfyNode) {
return node && subgraphs.some((s) => s.id === node.type)
}
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return validState
const errors: Record<SerializedNodeId, NodeError> = {}
//mark errors for all but first subgraph node
let firstSubgraphFound = false
@@ -88,7 +94,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
}
override async save(): Promise<UserFile> {
this.validateSubgraph()
const activeState = this.validateSubgraph()
if (
!this.hasPromptedSave &&
useSettingStore().get('Comfy.Workflow.WarnBlueprintOverwrite')
@@ -97,7 +103,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
this.hasPromptedSave = true
}
// Extract metadata from subgraph.extra to workflow.extra before saving
this.extractMetadataToWorkflowExtra()
this.extractMetadataToWorkflowExtra(activeState)
const ret = await super.save()
// Force reload to update initialState with saved metadata
registerNodeDef(await this.load({ force: true }), {
@@ -110,13 +116,14 @@ export const useSubgraphStore = defineStore('subgraph', () => {
* Moves all properties (except workflowRendererVersion) from subgraph.extra
* to workflow.extra, then removes from subgraph.extra to avoid duplication.
*/
private extractMetadataToWorkflowExtra(): void {
if (!this.activeState) return
const subgraph = this.activeState.definitions?.subgraphs?.[0]
private extractMetadataToWorkflowExtra(
activeState: ValidSubgraphWorkflowJSON
): void {
const subgraph = activeState.definitions.subgraphs?.[0]
if (!subgraph?.extra) return
const sgExtra = subgraph.extra as Record<string, unknown>
const workflowExtra = (this.activeState.extra ??= {}) as Record<
const workflowExtra = (activeState.extra ??= {}) as Record<
string,
unknown
>
@@ -129,10 +136,10 @@ export const useSubgraphStore = defineStore('subgraph', () => {
}
override async saveAs(path: string) {
this.validateSubgraph()
const activeState = this.validateSubgraph()
this.hasPromptedSave = true
// Extract metadata from subgraph.extra to workflow.extra before saving
this.extractMetadataToWorkflowExtra()
this.extractMetadataToWorkflowExtra(activeState)
const ret = await super.saveAs(path)
// Force reload to update initialState with saved metadata
registerNodeDef(await this.load({ force: true }), {
@@ -146,14 +153,20 @@ export const useSubgraphStore = defineStore('subgraph', () => {
if (!force && this.isLoaded) return await super.load({ force })
const loaded = await super.load({ force })
const st = loaded.activeState
const rootNode = st.nodes[0]
if (!rootNode) {
throw new TypeError(
`Subgraph blueprint '${this.filename}' must contain a root node`
)
}
const sg = (st.definitions?.subgraphs ?? []).find(
(sg) => sg.id == st.nodes[0].type
(sg) => sg.id == rootNode.type
)
if (!sg)
throw new Error(
'Loaded subgraph blueprint does not contain valid subgraph'
)
sg.name = st.nodes[0].title = this.filename
sg.name = rootNode.title = this.filename
// Copy blueprint metadata from workflow extra to subgraph extra
// so it's available when editing via canvas.subgraph.extra
@@ -277,7 +290,11 @@ export const useSubgraphStore = defineStore('subgraph', () => {
name: string = workflow.filename
) {
const subgraphNode = workflow.changeTracker.initialState.nodes[0]
if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint')
if (!subgraphNode) {
throw new TypeError(
`Subgraph blueprint '${name}' must contain a root node`
)
}
subgraphNode.inputs ??= []
subgraphNode.outputs ??= []
//NOTE: Types are cast to string. This is only used for input coloring on previews