mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-16 04:30:59 +00:00
Compare commits
1 Commits
fix/remove
...
feat/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56da748234 |
@@ -58,7 +58,9 @@ const config: KnipConfig = {
|
||||
// Pending integration in stacked PR
|
||||
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js'
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// Public API shim for custom nodes, consumed by comfyAPIPlugin at build time
|
||||
'src/scripts/nodeEvents.ts'
|
||||
],
|
||||
vite: {
|
||||
config: ['vite?(.*).config.mts']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { NodeEvent } from '@/lib/litegraph/src/infrastructure/LGraphNodeEventMap'
|
||||
import type { NodeOutputWith } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -18,12 +19,9 @@ useExtensionService().registerExtension({
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
node.setSize([Math.max(oldWidth, 400), Math.max(oldHeight, 350)])
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (output: ImageCompareOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const { a_images: aImages, b_images: bImages } = output
|
||||
node.on(NodeEvent.EXECUTED, ({ output }) => {
|
||||
const { a_images: aImages, b_images: bImages } =
|
||||
output as ImageCompareOutput
|
||||
const rand = app.getRandParam()
|
||||
|
||||
const toUrl = (record: Record<string, string>) => {
|
||||
@@ -42,6 +40,6 @@ useExtensionService().registerExtension({
|
||||
widget.value = { beforeImages, afterImages }
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SUPPORTED_EXTENSIONS_ACCEPT } from '@/extensions/core/load3d/constants'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { NodeEvent } from '@/lib/litegraph/src/infrastructure/LGraphNodeEventMap'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -475,8 +476,6 @@ useExtensionService().registerExtension({
|
||||
|
||||
await nextTick()
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
useLoad3d(node).waitForLoad3d((load3d) => {
|
||||
const config = new Load3DConfiguration(load3d, node.properties)
|
||||
|
||||
@@ -502,10 +501,8 @@ useExtensionService().registerExtension({
|
||||
config.configure(settings)
|
||||
}
|
||||
|
||||
node.onExecuted = function (output: Load3dPreviewOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const result = output.result
|
||||
node.on(NodeEvent.EXECUTED, ({ output }) => {
|
||||
const result = (output as Load3dPreviewOutput).result
|
||||
const filePath = result?.[0]
|
||||
|
||||
if (!filePath) {
|
||||
@@ -533,7 +530,7 @@ useExtensionService().registerExtension({
|
||||
if (bgImagePath) {
|
||||
load3d.setBackgroundImage(bgImagePath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { NodeEvent } from '@/lib/litegraph/src/infrastructure/LGraphNodeEventMap'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
@@ -81,12 +82,8 @@ useExtensionService().registerExtension({
|
||||
|
||||
await nextTick()
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (output: SaveMeshOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const fileInfo = output['3d']?.[0]
|
||||
node.on(NodeEvent.EXECUTED, ({ output }) => {
|
||||
const fileInfo = (output as SaveMeshOutput)['3d']?.[0]
|
||||
|
||||
if (!fileInfo) return
|
||||
|
||||
@@ -119,6 +116,6 @@ useExtensionService().registerExtension({
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LiteGraph, NodeEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
@@ -315,6 +315,12 @@ export class PrimitiveNode extends LGraphNode {
|
||||
{} as CanvasPointerEvent
|
||||
)
|
||||
this.onWidgetChanged?.(widget.name, newValue, oldValue, widget)
|
||||
this.emit(NodeEvent.WIDGET_CHANGED, {
|
||||
name: widget.name,
|
||||
value: newValue,
|
||||
oldValue,
|
||||
widget
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import type { NodeId } from './LGraphNode'
|
||||
import { NodeEvent } from './infrastructure/LGraphNodeEventMap'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId, SerialisedLLinkArray } from './LLink'
|
||||
import { MapProxyHandler } from './MapProxyHandler'
|
||||
@@ -394,6 +395,8 @@ export class LGraph
|
||||
if (this._nodes) {
|
||||
for (const _node of this._nodes) {
|
||||
_node.onRemoved?.()
|
||||
_node.emit?.(NodeEvent.REMOVED)
|
||||
_node._removeAllListeners?.()
|
||||
this.onNodeRemoved?.(_node)
|
||||
}
|
||||
}
|
||||
@@ -1004,6 +1007,7 @@ export class LGraph
|
||||
this._nodes_by_id[node.id] = node
|
||||
|
||||
node.onAdded?.(this)
|
||||
node.emit?.(NodeEvent.ADDED, { graph: this })
|
||||
|
||||
if (this.config.align_to_grid) node.alignToGrid()
|
||||
|
||||
@@ -1100,6 +1104,8 @@ export class LGraph
|
||||
if (!hasRemainingReferences) {
|
||||
forEachNode(node.subgraph, (innerNode) => {
|
||||
innerNode.onRemoved?.()
|
||||
innerNode.emit?.(NodeEvent.REMOVED)
|
||||
innerNode._removeAllListeners?.()
|
||||
innerNode.graph?.onNodeRemoved?.(innerNode)
|
||||
})
|
||||
this.rootGraph.subgraphs.delete(node.subgraph.id)
|
||||
@@ -1108,6 +1114,8 @@ export class LGraph
|
||||
|
||||
// callback
|
||||
node.onRemoved?.()
|
||||
node.emit?.(NodeEvent.REMOVED)
|
||||
node._removeAllListeners?.()
|
||||
|
||||
node.graph = null
|
||||
this._version++
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { LGraph } from './LGraph'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import type { NodeId, NodeProperty } from './LGraphNode'
|
||||
import { NodeEvent } from './infrastructure/LGraphNodeEventMap'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId } from './LLink'
|
||||
import { Reroute } from './Reroute'
|
||||
@@ -3080,6 +3081,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// value changed
|
||||
if (oldValue != widget.value) {
|
||||
node.onWidgetChanged?.(widget.name, widget.value, oldValue, widget)
|
||||
node.emit(NodeEvent.WIDGET_CHANGED, {
|
||||
name: widget.name,
|
||||
value: widget.value,
|
||||
oldValue,
|
||||
widget
|
||||
})
|
||||
if (!node.graph) throw new NullGraphError()
|
||||
node.graph._version++
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ import type {
|
||||
TWidgetType,
|
||||
TWidgetValue
|
||||
} from './types/widgets'
|
||||
import type { INodeEventEmitter } from './infrastructure/NodeEventEmitter'
|
||||
import { applyNodeEventEmitter } from './infrastructure/NodeEventEmitter'
|
||||
import type { LGraphNodeEventMap } from './infrastructure/LGraphNodeEventMap'
|
||||
import { NodeEvent } from './infrastructure/LGraphNodeEventMap'
|
||||
import { findFreeSlotOfType } from './utils/collections'
|
||||
import { warnDeprecated } from './utils/feedback'
|
||||
import { distributeSpace } from './utils/spaceDistribution'
|
||||
@@ -229,9 +233,15 @@ export interface LGraphNode {
|
||||
* @param type a type for the node
|
||||
*/
|
||||
|
||||
export interface LGraphNode extends INodeEventEmitter<LGraphNodeEventMap> {}
|
||||
|
||||
export class LGraphNode
|
||||
implements NodeLike, Positionable, IPinnable, IColorable
|
||||
{
|
||||
/** @internal Lazily-created instance event listener map. */
|
||||
declare _eventListeners?: Map<string, Set<(detail: unknown) => void>>
|
||||
/** @internal Class-level event listener map (static). */
|
||||
static _classEventListeners?: Map<string, Set<(detail: unknown) => void>>
|
||||
// Static properties used by dynamic child classes
|
||||
static title?: string
|
||||
static MAX_CONSOLE?: number
|
||||
@@ -877,6 +887,13 @@ export class LGraphNode
|
||||
? this.graph._links.get(input.link)
|
||||
: null
|
||||
this.onConnectionsChange?.(NodeSlotType.INPUT, i, true, link, input)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.INPUT,
|
||||
index: i,
|
||||
isConnected: true,
|
||||
link,
|
||||
inputOrOutput: input
|
||||
})
|
||||
this.onInputAdded?.(input)
|
||||
}
|
||||
|
||||
@@ -890,6 +907,13 @@ export class LGraphNode
|
||||
for (const linkId of output.links) {
|
||||
const link = this.graph ? this.graph._links.get(linkId) : null
|
||||
this.onConnectionsChange?.(NodeSlotType.OUTPUT, i, true, link, output)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.OUTPUT,
|
||||
index: i,
|
||||
isConnected: true,
|
||||
link,
|
||||
inputOrOutput: output
|
||||
})
|
||||
}
|
||||
this.onOutputAdded?.(output)
|
||||
}
|
||||
@@ -935,6 +959,7 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
this.onConfigure?.(info)
|
||||
this.emit(NodeEvent.CONFIGURED, { serialisedNode: info })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1594,6 +1619,7 @@ export class LGraphNode
|
||||
setSize(size: Size): void {
|
||||
this.size = size
|
||||
this.onResize?.(this.size)
|
||||
this.emit(NodeEvent.RESIZE, { size: this.size })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2996,6 +3022,13 @@ export class LGraphNode
|
||||
link,
|
||||
output
|
||||
)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.OUTPUT,
|
||||
index: outputIndex,
|
||||
isConnected: true,
|
||||
link,
|
||||
inputOrOutput: output
|
||||
})
|
||||
|
||||
inputNode.onConnectionsChange?.(
|
||||
NodeSlotType.INPUT,
|
||||
@@ -3004,6 +3037,13 @@ export class LGraphNode
|
||||
link,
|
||||
input
|
||||
)
|
||||
inputNode.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.INPUT,
|
||||
index: inputIndex,
|
||||
isConnected: true,
|
||||
link,
|
||||
inputOrOutput: input
|
||||
})
|
||||
|
||||
this.setDirtyCanvas(false, true)
|
||||
graph.afterChange()
|
||||
@@ -3145,6 +3185,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
input
|
||||
)
|
||||
target.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.INPUT,
|
||||
index: link_info.target_slot,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: input
|
||||
})
|
||||
this.onConnectionsChange?.(
|
||||
NodeSlotType.OUTPUT,
|
||||
slot,
|
||||
@@ -3152,6 +3199,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
output
|
||||
)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.OUTPUT,
|
||||
index: slot,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: output
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
@@ -3197,6 +3251,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
input
|
||||
)
|
||||
target.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.INPUT,
|
||||
index: link_info.target_slot,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: input
|
||||
})
|
||||
}
|
||||
// remove the link from the links pool
|
||||
link_info.disconnect(graph, 'input')
|
||||
@@ -3208,6 +3269,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
output
|
||||
)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.OUTPUT,
|
||||
index: slot,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: output
|
||||
})
|
||||
}
|
||||
output.links = null
|
||||
}
|
||||
@@ -3310,6 +3378,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
input
|
||||
)
|
||||
this.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.INPUT,
|
||||
index: slot,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: input
|
||||
})
|
||||
target_node.onConnectionsChange?.(
|
||||
NodeSlotType.OUTPUT,
|
||||
i,
|
||||
@@ -3317,6 +3392,13 @@ export class LGraphNode
|
||||
link_info,
|
||||
output
|
||||
)
|
||||
target_node.emit(NodeEvent.CONNECTIONS_CHANGE, {
|
||||
type: NodeSlotType.OUTPUT,
|
||||
index: i,
|
||||
isConnected: false,
|
||||
link: link_info,
|
||||
inputOrOutput: output
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4283,3 +4365,5 @@ export class LGraphNode
|
||||
ctx.fillStyle = originalFillStyle
|
||||
}
|
||||
}
|
||||
|
||||
applyNodeEventEmitter(LGraphNode)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LGraph } from './LGraph'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import { NodeEvent } from './infrastructure/LGraphNodeEventMap'
|
||||
import { LLink } from './LLink'
|
||||
import { Reroute } from './Reroute'
|
||||
import { InputIndicators } from './canvas/InputIndicators'
|
||||
@@ -571,6 +572,7 @@ export class LiteGraphGlobal {
|
||||
|
||||
// callback
|
||||
node.onNodeCreated?.()
|
||||
node.emit?.(NodeEvent.NODE_CREATED)
|
||||
return node
|
||||
}
|
||||
|
||||
|
||||
43
src/lib/litegraph/src/infrastructure/LGraphNodeEventMap.ts
Normal file
43
src/lib/litegraph/src/infrastructure/LGraphNodeEventMap.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
Size
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
export const NodeEvent = {
|
||||
NODE_CREATED: 'node-created',
|
||||
CONFIGURED: 'configured',
|
||||
CONNECTIONS_CHANGE: 'connections-change',
|
||||
WIDGET_CHANGED: 'widget-changed',
|
||||
ADDED: 'added',
|
||||
REMOVED: 'removed',
|
||||
RESIZE: 'resize',
|
||||
EXECUTED: 'executed'
|
||||
} as const
|
||||
|
||||
export type LGraphNodeEventMap = {
|
||||
[NodeEvent.NODE_CREATED]: never
|
||||
[NodeEvent.CONFIGURED]: { serialisedNode: ISerialisedNode }
|
||||
[NodeEvent.CONNECTIONS_CHANGE]: {
|
||||
type: ISlotType
|
||||
index: number
|
||||
isConnected: boolean
|
||||
link: LLink | null | undefined
|
||||
inputOrOutput: INodeInputSlot | INodeOutputSlot
|
||||
}
|
||||
[NodeEvent.WIDGET_CHANGED]: {
|
||||
name: string
|
||||
value: unknown
|
||||
oldValue: unknown
|
||||
widget: IBaseWidget
|
||||
}
|
||||
[NodeEvent.ADDED]: { graph: LGraph }
|
||||
[NodeEvent.REMOVED]: never
|
||||
[NodeEvent.RESIZE]: { size: Size }
|
||||
[NodeEvent.EXECUTED]: { output: Record<string, unknown> }
|
||||
}
|
||||
399
src/lib/litegraph/src/infrastructure/NodeEventEmitter.test.ts
Normal file
399
src/lib/litegraph/src/infrastructure/NodeEventEmitter.test.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
NodeEvent,
|
||||
onAllNodeEvents,
|
||||
offAllNodeEvents
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
function createGraph(): LGraph {
|
||||
return new LGraph()
|
||||
}
|
||||
|
||||
function createConnectedNodes(graph: LGraph) {
|
||||
const source = new LGraphNode('Source')
|
||||
source.addOutput('out', 'number')
|
||||
const target = new LGraphNode('Target')
|
||||
target.addInput('in', 'number')
|
||||
graph.add(source)
|
||||
graph.add(target)
|
||||
return { source, target }
|
||||
}
|
||||
|
||||
describe('NodeEventEmitter', () => {
|
||||
let origLiteGraph: typeof LiteGraph
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
origLiteGraph = Object.assign({}, LiteGraph)
|
||||
// @ts-expect-error Intended: Force remove an otherwise readonly non-optional property
|
||||
delete origLiteGraph.Classes
|
||||
|
||||
Object.assign(LiteGraph, {
|
||||
NODE_TITLE_HEIGHT: 20,
|
||||
NODE_SLOT_HEIGHT: 15,
|
||||
NODE_TEXT_SIZE: 14,
|
||||
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0.5)',
|
||||
DEFAULT_GROUP_FONT_SIZE: 24,
|
||||
isValidConnection: vi.fn().mockReturnValue(true)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.assign(LiteGraph, origLiteGraph)
|
||||
})
|
||||
|
||||
describe('instance-level on/off/emit', () => {
|
||||
test('fires listener when event is emitted', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.NODE_CREATED, listener)
|
||||
node.emit(NodeEvent.NODE_CREATED)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('passes detail payload to listener', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.EXECUTED, listener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: { text: ['hello'] } })
|
||||
|
||||
expect(listener).toHaveBeenCalledWith({ output: { text: ['hello'] } })
|
||||
})
|
||||
|
||||
test('supports multiple listeners on the same event', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
node.on(NodeEvent.EXECUTED, listener1)
|
||||
node.on(NodeEvent.EXECUTED, listener2)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener1).toHaveBeenCalledOnce()
|
||||
expect(listener2).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('removes listener with off()', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.EXECUTED, listener)
|
||||
node.off(NodeEvent.EXECUTED, listener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('on() returns an unsubscribe function', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
const unsub = node.on(NodeEvent.EXECUTED, listener)
|
||||
unsub()
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does not fire listeners for other event types', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.EXECUTED, listener)
|
||||
node.emit(NodeEvent.NODE_CREATED)
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('isolates listeners between node instances', () => {
|
||||
const node1 = new LGraphNode('A')
|
||||
const node2 = new LGraphNode('B')
|
||||
const listener = vi.fn()
|
||||
|
||||
node1.on(NodeEvent.EXECUTED, listener)
|
||||
node2.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('once()', () => {
|
||||
test('fires listener only once', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.once(NodeEvent.EXECUTED, listener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: { a: 1 } })
|
||||
node.emit(NodeEvent.EXECUTED, { output: { a: 2 } })
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({ output: { a: 1 } })
|
||||
})
|
||||
|
||||
test('returns an unsubscribe function that cancels before firing', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
const unsub = node.once(NodeEvent.EXECUTED, listener)
|
||||
unsub()
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('does not interfere with other listeners', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const onceListener = vi.fn()
|
||||
const permanentListener = vi.fn()
|
||||
|
||||
node.once(NodeEvent.EXECUTED, onceListener)
|
||||
node.on(NodeEvent.EXECUTED, permanentListener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(onceListener).toHaveBeenCalledOnce()
|
||||
expect(permanentListener).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('class-level onAllNodeEvents', () => {
|
||||
afterEach(() => {
|
||||
LGraphNode._classEventListeners?.clear()
|
||||
})
|
||||
|
||||
test('fires class-level listener on any instance emit', () => {
|
||||
const listener = vi.fn()
|
||||
onAllNodeEvents(LGraphNode, NodeEvent.EXECUTED, listener)
|
||||
|
||||
const node = new LGraphNode('Test')
|
||||
node.emit(NodeEvent.EXECUTED, { output: { data: 1 } })
|
||||
|
||||
expect(listener).toHaveBeenCalledWith({ output: { data: 1 } })
|
||||
})
|
||||
|
||||
test('fires class-level listener with node as this', () => {
|
||||
let capturedThis: unknown
|
||||
const listener = function (this: unknown) {
|
||||
capturedThis = this
|
||||
}
|
||||
onAllNodeEvents(LGraphNode, NodeEvent.NODE_CREATED, listener)
|
||||
|
||||
const node = new LGraphNode('Test')
|
||||
node.emit(NodeEvent.NODE_CREATED)
|
||||
|
||||
expect(capturedThis).toBe(node)
|
||||
})
|
||||
|
||||
test('fires both instance and class listeners', () => {
|
||||
const instanceListener = vi.fn()
|
||||
const classListener = vi.fn()
|
||||
|
||||
onAllNodeEvents(LGraphNode, NodeEvent.EXECUTED, classListener)
|
||||
const node = new LGraphNode('Test')
|
||||
node.on(NodeEvent.EXECUTED, instanceListener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(instanceListener).toHaveBeenCalledOnce()
|
||||
expect(classListener).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('removes class-level listener with offAllNodeEvents', () => {
|
||||
const listener = vi.fn()
|
||||
onAllNodeEvents(LGraphNode, NodeEvent.EXECUTED, listener)
|
||||
offAllNodeEvents(LGraphNode, NodeEvent.EXECUTED, listener)
|
||||
|
||||
const node = new LGraphNode('Test')
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('onAllNodeEvents returns an unsubscribe function', () => {
|
||||
const listener = vi.fn()
|
||||
const unsub = onAllNodeEvents(LGraphNode, NodeEvent.EXECUTED, listener)
|
||||
unsub()
|
||||
|
||||
const node = new LGraphNode('Test')
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('error isolation', () => {
|
||||
test('continues calling listeners if one throws', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const errorListener = vi.fn(() => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
const normalListener = vi.fn()
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
node.on(NodeEvent.EXECUTED, errorListener)
|
||||
node.on(NodeEvent.EXECUTED, normalListener)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(errorListener).toHaveBeenCalledOnce()
|
||||
expect(normalListener).toHaveBeenCalledOnce()
|
||||
expect(console.error).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('automatic cleanup on node removal', () => {
|
||||
test('clears all listeners when node is removed from graph', () => {
|
||||
const graph = createGraph()
|
||||
const node = new LGraphNode('Test')
|
||||
graph.add(node)
|
||||
|
||||
const listener = vi.fn()
|
||||
node.on(NodeEvent.EXECUTED, listener)
|
||||
|
||||
graph.remove(node)
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('clears listeners on graph.clear()', () => {
|
||||
const graph = createGraph()
|
||||
const node = new LGraphNode('Test')
|
||||
graph.add(node)
|
||||
|
||||
const listener = vi.fn()
|
||||
node.on(NodeEvent.EXECUTED, listener)
|
||||
|
||||
graph.clear()
|
||||
node.emit(NodeEvent.EXECUTED, { output: {} })
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lifecycle event integration', () => {
|
||||
test('emits added when node is added to graph', () => {
|
||||
const graph = createGraph()
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.ADDED, listener)
|
||||
graph.add(node)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({ graph })
|
||||
})
|
||||
|
||||
test('emits removed when node is removed from graph', () => {
|
||||
const graph = createGraph()
|
||||
const node = new LGraphNode('Test')
|
||||
graph.add(node)
|
||||
|
||||
const listener = vi.fn()
|
||||
node.on(NodeEvent.REMOVED, listener)
|
||||
graph.remove(node)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('emits configured after node.configure()', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const graph = createGraph()
|
||||
graph.add(node)
|
||||
|
||||
const listener = vi.fn()
|
||||
node.on(NodeEvent.CONFIGURED, listener)
|
||||
|
||||
const serialised: ISerialisedNode = {
|
||||
id: node.id,
|
||||
type: 'Test',
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0
|
||||
}
|
||||
node.configure(serialised)
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
serialisedNode: serialised
|
||||
})
|
||||
})
|
||||
|
||||
test('emits resize when setSize is called', () => {
|
||||
const node = new LGraphNode('Test')
|
||||
const listener = vi.fn()
|
||||
|
||||
node.on(NodeEvent.RESIZE, listener)
|
||||
node.setSize([200, 300])
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
const detail = listener.mock.calls[0][0]
|
||||
expect(detail.size[0]).toBe(200)
|
||||
expect(detail.size[1]).toBe(300)
|
||||
})
|
||||
})
|
||||
|
||||
describe('connections-change event', () => {
|
||||
test('emits on both nodes when connected', () => {
|
||||
const graph = createGraph()
|
||||
const { source, target } = createConnectedNodes(graph)
|
||||
const sourceListener = vi.fn()
|
||||
const targetListener = vi.fn()
|
||||
|
||||
source.on(NodeEvent.CONNECTIONS_CHANGE, sourceListener)
|
||||
target.on(NodeEvent.CONNECTIONS_CHANGE, targetListener)
|
||||
|
||||
source.connect(0, target, 0)
|
||||
|
||||
expect(sourceListener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 2, // NodeSlotType.OUTPUT
|
||||
index: 0,
|
||||
isConnected: true
|
||||
})
|
||||
)
|
||||
expect(targetListener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 1, // NodeSlotType.INPUT
|
||||
index: 0,
|
||||
isConnected: true
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('emits on both nodes when disconnected', () => {
|
||||
const graph = createGraph()
|
||||
const { source, target } = createConnectedNodes(graph)
|
||||
source.connect(0, target, 0)
|
||||
|
||||
const sourceListener = vi.fn()
|
||||
const targetListener = vi.fn()
|
||||
source.on(NodeEvent.CONNECTIONS_CHANGE, sourceListener)
|
||||
target.on(NodeEvent.CONNECTIONS_CHANGE, targetListener)
|
||||
|
||||
target.disconnectInput(0)
|
||||
|
||||
expect(sourceListener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isConnected: false
|
||||
})
|
||||
)
|
||||
expect(targetListener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isConnected: false
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
143
src/lib/litegraph/src/infrastructure/NodeEventEmitter.ts
Normal file
143
src/lib/litegraph/src/infrastructure/NodeEventEmitter.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
type Listener<T = unknown> = (detail: T) => void
|
||||
|
||||
type ListenerMap = Map<string, Set<Listener>>
|
||||
|
||||
export type Unsubscribe = () => void
|
||||
|
||||
interface NodeWithEvents {
|
||||
_eventListeners?: ListenerMap
|
||||
constructor: { _classEventListeners?: ListenerMap }
|
||||
}
|
||||
|
||||
type EventMapConstraint = { [K: string]: unknown }
|
||||
|
||||
export interface INodeEventEmitter<EventMap extends EventMapConstraint> {
|
||||
on<K extends keyof EventMap & string>(
|
||||
type: K,
|
||||
listener: Listener<EventMap[K]>
|
||||
): Unsubscribe
|
||||
|
||||
once<K extends keyof EventMap & string>(
|
||||
type: K,
|
||||
listener: Listener<EventMap[K]>
|
||||
): Unsubscribe
|
||||
|
||||
off<K extends keyof EventMap & string>(
|
||||
type: K,
|
||||
listener: Listener<EventMap[K]>
|
||||
): void
|
||||
|
||||
emit<K extends keyof EventMap & string>(
|
||||
type: K,
|
||||
...args: EventMap[K] extends never ? [] : [detail: EventMap[K]]
|
||||
): void
|
||||
|
||||
_removeAllListeners?(): void
|
||||
}
|
||||
|
||||
function addListener(
|
||||
node: NodeWithEvents,
|
||||
type: string,
|
||||
listener: Listener
|
||||
): Unsubscribe {
|
||||
if (!node._eventListeners) node._eventListeners = new Map()
|
||||
let listeners = node._eventListeners.get(type)
|
||||
if (!listeners) {
|
||||
listeners = new Set()
|
||||
node._eventListeners.set(type, listeners)
|
||||
}
|
||||
listeners.add(listener)
|
||||
return () => node._eventListeners?.get(type)?.delete(listener)
|
||||
}
|
||||
|
||||
function nodeOn(
|
||||
this: NodeWithEvents,
|
||||
type: string,
|
||||
listener: Listener
|
||||
): Unsubscribe {
|
||||
return addListener(this, type, listener)
|
||||
}
|
||||
|
||||
function nodeOnce(
|
||||
this: NodeWithEvents,
|
||||
type: string,
|
||||
listener: Listener
|
||||
): Unsubscribe {
|
||||
const wrapper: Listener = (detail) => {
|
||||
this._eventListeners?.get(type)?.delete(wrapper)
|
||||
listener(detail)
|
||||
}
|
||||
return addListener(this, type, wrapper)
|
||||
}
|
||||
|
||||
function nodeOff(this: NodeWithEvents, type: string, listener: Listener) {
|
||||
this._eventListeners?.get(type)?.delete(listener)
|
||||
}
|
||||
|
||||
function nodeEmit(this: NodeWithEvents, type: string, detail?: unknown) {
|
||||
const listeners = this._eventListeners?.get(type)
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(detail)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[ComfyUI] Error in node event listener for "${type}":`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const classListeners = this.constructor._classEventListeners?.get(type)
|
||||
if (classListeners) {
|
||||
for (const listener of classListeners) {
|
||||
try {
|
||||
listener.call(this, detail)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[ComfyUI] Error in class event listener for "${type}":`,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nodeRemoveAllListeners(this: NodeWithEvents) {
|
||||
this._eventListeners?.clear()
|
||||
}
|
||||
|
||||
export function applyNodeEventEmitter(target: { prototype: object }): void {
|
||||
Object.assign(target.prototype, {
|
||||
on: nodeOn,
|
||||
once: nodeOnce,
|
||||
off: nodeOff,
|
||||
emit: nodeEmit,
|
||||
_removeAllListeners: nodeRemoveAllListeners
|
||||
})
|
||||
}
|
||||
|
||||
export function onAllNodeEvents(
|
||||
nodeClass: { _classEventListeners?: ListenerMap },
|
||||
type: string,
|
||||
listener: Listener
|
||||
): Unsubscribe {
|
||||
if (!nodeClass._classEventListeners)
|
||||
nodeClass._classEventListeners = new Map()
|
||||
let listeners = nodeClass._classEventListeners.get(type)
|
||||
if (!listeners) {
|
||||
listeners = new Set()
|
||||
nodeClass._classEventListeners.set(type, listeners)
|
||||
}
|
||||
listeners.add(listener)
|
||||
return () => nodeClass._classEventListeners?.get(type)?.delete(listener)
|
||||
}
|
||||
|
||||
export function offAllNodeEvents(
|
||||
nodeClass: { _classEventListeners?: ListenerMap },
|
||||
type: string,
|
||||
listener: Listener
|
||||
): void {
|
||||
nodeClass._classEventListeners?.get(type)?.delete(listener)
|
||||
}
|
||||
@@ -86,6 +86,13 @@ export { ContextMenu } from './ContextMenu'
|
||||
|
||||
export { DragAndScale } from './DragAndScale'
|
||||
|
||||
export { NodeEvent } from './infrastructure/LGraphNodeEventMap'
|
||||
export type { LGraphNodeEventMap } from './infrastructure/LGraphNodeEventMap'
|
||||
export {
|
||||
onAllNodeEvents,
|
||||
offAllNodeEvents
|
||||
} from './infrastructure/NodeEventEmitter'
|
||||
export type { Unsubscribe } from './infrastructure/NodeEventEmitter'
|
||||
export { Rectangle } from './infrastructure/Rectangle'
|
||||
export type { SubgraphEventMap } from './infrastructure/SubgraphEventMap'
|
||||
export type {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Size
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeEvent } from '@/lib/litegraph/src/infrastructure/LGraphNodeEventMap'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
@@ -436,6 +437,12 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
this.callback?.(this.value, canvas, node, pos, e)
|
||||
|
||||
node.onWidgetChanged?.(this.name ?? '', v, oldValue, this)
|
||||
node.emit(NodeEvent.WIDGET_CHANGED, {
|
||||
name: this.name ?? '',
|
||||
value: v,
|
||||
oldValue,
|
||||
widget: this
|
||||
})
|
||||
if (node.graph) node.graph._version++
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isComboWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import { isComboWidget, NodeEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
|
||||
@@ -137,6 +137,12 @@ function createAssetBrowserWidget(
|
||||
defaultValue,
|
||||
onValueChange: (widget, newValue, oldValue) => {
|
||||
node.onWidgetChanged?.(widget.name, newValue, oldValue, widget)
|
||||
node.emit(NodeEvent.WIDGET_CHANGED, {
|
||||
name: widget.name,
|
||||
value: newValue,
|
||||
oldValue,
|
||||
widget
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
LiteGraph,
|
||||
NodeEvent
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { snapPoint } from '@/lib/litegraph/src/measure'
|
||||
import type { Vector2 } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -738,8 +739,9 @@ export class ComfyApp {
|
||||
})
|
||||
|
||||
const node = getNodeByExecutionId(this.rootGraph, executionId)
|
||||
if (node && node.onExecuted) {
|
||||
node.onExecuted(detail.output)
|
||||
if (node) {
|
||||
node.onExecuted?.(detail.output)
|
||||
node.emit(NodeEvent.EXECUTED, { output: detail.output })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
19
src/scripts/nodeEvents.ts
Normal file
19
src/scripts/nodeEvents.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Public API for node event subscriptions.
|
||||
*
|
||||
* Exposed via window.comfyAPI.nodeEvents for custom node extensions.
|
||||
*
|
||||
* Usage:
|
||||
* const { NodeEvent, onAllNodeEvents, offAllNodeEvents } = window.comfyAPI.nodeEvents
|
||||
*
|
||||
* // In beforeRegisterNodeDef:
|
||||
* onAllNodeEvents(nodeType, NodeEvent.EXECUTED, function(detail) {
|
||||
* // `this` is the node instance
|
||||
* console.log('Node executed:', detail.output)
|
||||
* })
|
||||
*/
|
||||
export { NodeEvent } from '@/lib/litegraph/src/infrastructure/LGraphNodeEventMap'
|
||||
export {
|
||||
onAllNodeEvents,
|
||||
offAllNodeEvents
|
||||
} from '@/lib/litegraph/src/infrastructure/NodeEventEmitter'
|
||||
Reference in New Issue
Block a user