Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/stores/subgraphStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
49 changes: 33 additions & 16 deletions src/stores/subgraphStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/'
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -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 }), {
Expand All @@ -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
>
Expand All @@ -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 }), {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading