refactor(groupNode): improve type safety and reduce casts

Amp-Thread-ID: https://ampcode.com/threads/T-019baa1c-6aa2-769b-a5f9-a705b5ef2b2b
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-01-10 18:19:31 -08:00
parent d23736b310
commit 56f9e9cd40
6 changed files with 67 additions and 38 deletions

View File

@@ -4,10 +4,11 @@ import type { GroupNodeWorkflowData } from '@/lib/litegraph/src/LGraph'
import type { import type {
GroupNodeInputConfig, GroupNodeInputConfig,
GroupNodeInputsSpec, GroupNodeInputsSpec,
GroupNodeInternalLink,
GroupNodeOutputType, GroupNodeOutputType,
PartialLinkInfo PartialLinkInfo
} from './groupNodeTypes' } from './groupNodeTypes'
import { LLink, type SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' import { LLink } from '@/lib/litegraph/src/LLink'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { import {
@@ -39,7 +40,7 @@ import { app } from '../../scripts/app'
import { ManageGroupDialog } from './groupNodeManage' import { ManageGroupDialog } from './groupNodeManage'
import { mergeIfValid } from './widgetInputs' import { mergeIfValid } from './widgetInputs'
type GroupNodeLink = SerialisedLLinkArray type GroupNodeLink = GroupNodeInternalLink
type LinksFromMap = Record<number, Record<number, GroupNodeLink[]>> type LinksFromMap = Record<number, Record<number, GroupNodeLink[]>>
type LinksToMap = Record<number, Record<number, GroupNodeLink>> type LinksToMap = Record<number, Record<number, GroupNodeLink>>
type ExternalFromMap = Record<number, Record<number, string | number>> type ExternalFromMap = Record<number, Record<number, string | number>>
@@ -1005,7 +1006,7 @@ export class GroupNodeHandler {
return inputNode return inputNode
} }
innerNode.getInputLink = ((slot: number): PartialLinkInfo | null => { innerNode.getInputLink = (slot: number): PartialLinkInfo | null => {
const nodeIdx = innerNode.index ?? 0 const nodeIdx = innerNode.index ?? 0
const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot] const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot]
if (externalSlot != null) { if (externalSlot != null) {
@@ -1025,14 +1026,15 @@ export class GroupNodeHandler {
const innerLink = this.groupData.linksTo[nodeIdx]?.[slot] const innerLink = this.groupData.linksTo[nodeIdx]?.[slot]
if (!innerLink) return null if (!innerLink) return null
const linkSrcIdx = innerLink[0] const linkSrcIdx = innerLink[0]
if (linkSrcIdx == null) return null const linkSrcSlot = innerLink[1]
if (linkSrcIdx == null || linkSrcSlot == null) return null
return { return {
origin_id: innerNodes[Number(linkSrcIdx)].id, origin_id: innerNodes[Number(linkSrcIdx)].id,
origin_slot: innerLink[1], origin_slot: linkSrcSlot,
target_id: innerNode.id, target_id: innerNode.id,
target_slot: +slot target_slot: +slot
} }
}) as typeof innerNode.getInputLink }
} }
} }
@@ -1042,7 +1044,7 @@ export class GroupNodeHandler {
if (!output || !this.innerNodes) return null if (!output || !this.innerNodes) return null
const nodeIdx = output.node.index ?? 0 const nodeIdx = output.node.index ?? 0
let innerNode: LGraphNode | null = this.innerNodes[nodeIdx] let innerNode: LGraphNode | null = this.innerNodes[nodeIdx]
let l let l = innerNode?.getInputLink(0)
while (innerNode?.type === 'Reroute') { while (innerNode?.type === 'Reroute') {
l = innerNode.getInputLink(0) l = innerNode.getInputLink(0)
innerNode = innerNode.getInputNode(0) innerNode = innerNode.getInputNode(0)
@@ -1053,7 +1055,7 @@ export class GroupNodeHandler {
} }
if ( if (
l && l instanceof LLink &&
GroupNodeHandler.isGroupNode(innerNode) && GroupNodeHandler.isGroupNode(innerNode) &&
innerNode.updateLink innerNode.updateLink
) { ) {
@@ -1098,10 +1100,9 @@ export class GroupNodeHandler {
const subgraphInstanceIdPath = [...subgraphNodePath, this.node.id] const subgraphInstanceIdPath = [...subgraphNodePath, this.node.id]
// Assertion: Deprecated, does not matter. // Get the parent subgraph node if we're inside a subgraph
const subgraphNode = (this.node.graph?.getNodeById( const parentNode = this.node.graph?.getNodeById(subgraphNodePath.at(-1))
subgraphNodePath.at(-1) const subgraphNode = parentNode?.isSubgraphNode() ? parentNode : undefined
) ?? undefined) as SubgraphNode | undefined
for (const node of this.innerNodes ?? []) { for (const node of this.innerNodes ?? []) {
node.graph ??= this.node.graph node.graph ??= this.node.graph
@@ -1437,13 +1438,9 @@ export class GroupNodeHandler {
type EventDetail = { display_node?: string; node?: string } | string type EventDetail = { display_node?: string; node?: string } | string
const handleEvent = ( const handleEvent = (
type: string, type: 'executing' | 'executed',
getId: (detail: EventDetail) => string | undefined, getId: (detail: EventDetail) => string | undefined,
getEvent: ( getEvent: (detail: EventDetail, id: string, node: LGraphNode) => unknown
detail: EventDetail,
id: string,
node: LGraphNode
) => EventDetail
) => { ) => {
const handler = ({ detail }: CustomEvent<EventDetail>) => { const handler = ({ detail }: CustomEvent<EventDetail>) => {
const id = getId(detail) const id = getId(detail)
@@ -1457,16 +1454,14 @@ export class GroupNodeHandler {
;( ;(
this.node as LGraphNode & { runningInternalNodeId?: number } this.node as LGraphNode & { runningInternalNodeId?: number }
).runningInternalNodeId = innerNodeIndex ).runningInternalNodeId = innerNodeIndex
// Cast needed: dispatching synthetic events for inner nodes with transformed payloads
api.dispatchCustomEvent( api.dispatchCustomEvent(
type as 'executing', type,
getEvent(detail, `${this.node.id}`, this.node) as string getEvent(detail, `${this.node.id}`, this.node) as string
) )
} }
} }
api.addEventListener( api.addEventListener(type, handler as EventListener)
type as 'executing' | 'executed',
handler as EventListener
)
return handler return handler
} }

View File

@@ -1,6 +1,21 @@
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' import type { ILinkRouting } from '@/lib/litegraph/src/interfaces'
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation' import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
/**
* Group node internal link format.
* This differs from standard SerialisedLLinkArray - indices represent node/slot positions within the group.
* Format: [sourceNodeIndex, sourceSlot, targetNodeIndex, targetSlot, ...optionalData]
* The type (ISlotType) may be at index 5 if present.
*/
export type GroupNodeInternalLink = [
sourceNodeIndex: number | null,
sourceSlot: number | null,
targetNodeIndex: number | null,
targetSlot: number | null,
...rest: (number | string | ISlotType | null | undefined)[]
]
/** Serialized node data within a group node workflow, with group-specific index */ /** Serialized node data within a group node workflow, with group-specific index */
export interface GroupNodeSerializedNode extends Partial<ISerialisedNode> { export interface GroupNodeSerializedNode extends Partial<ISerialisedNode> {
/** Position of this node within the group */ /** Position of this node within the group */
@@ -9,7 +24,7 @@ export interface GroupNodeSerializedNode extends Partial<ISerialisedNode> {
export interface GroupNodeWorkflowData { export interface GroupNodeWorkflowData {
external: (number | string)[][] external: (number | string)[][]
links: SerialisedLLinkArray[] links: GroupNodeInternalLink[]
nodes: GroupNodeSerializedNode[] nodes: GroupNodeSerializedNode[]
config?: Record<number, unknown> config?: Record<number, unknown>
} }
@@ -37,11 +52,6 @@ export type GroupNodeOutputType = string | (string | number)[]
/** /**
* Partial link info used internally by group node getInputLink override. * Partial link info used internally by group node getInputLink override.
* Contains only the properties needed for group node execution context. * Extends ILinkRouting to be compatible with the base getInputLink return type.
*/ */
export interface PartialLinkInfo { export interface PartialLinkInfo extends ILinkRouting {}
origin_id: string | number
origin_slot: number | string
target_id: string | number
target_slot: number
}

View File

@@ -34,6 +34,7 @@ import type {
IColorable, IColorable,
IContextMenuValue, IContextMenuValue,
IFoundSlot, IFoundSlot,
ILinkRouting,
INodeFlags, INodeFlags,
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
@@ -1153,10 +1154,11 @@ export class LGraphNode
} }
/** /**
* Returns the link info in the connection of an input slot * Returns the link info in the connection of an input slot.
* @returns object or null * Group nodes may override this to return ILinkRouting for internal routing.
* @returns LLink, ILinkRouting, or null
*/ */
getInputLink(slot: number): LLink | null { getInputLink(slot: number): LLink | ILinkRouting | null {
if (!this.inputs) return null if (!this.inputs) return null
if (slot < this.inputs.length) { if (slot < this.inputs.length) {

View File

@@ -11,6 +11,7 @@ import type { LGraphNode, NodeId } from './LGraphNode'
import type { Reroute, RerouteId } from './Reroute' import type { Reroute, RerouteId } from './Reroute'
import type { import type {
CanvasColour, CanvasColour,
ILinkRouting,
INodeInputSlot, INodeInputSlot,
INodeOutputSlot, INodeOutputSlot,
ISlotType, ISlotType,
@@ -89,7 +90,9 @@ type BasicReadonlyNetwork = Pick<
> >
// this is the class in charge of storing link information // this is the class in charge of storing link information
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> { export class LLink
implements LinkSegment, Serialisable<SerialisableLLink>, ILinkRouting
{
static _drawDebug = false static _drawDebug = false
/** Link ID */ /** Link ID */

View File

@@ -184,6 +184,21 @@ export interface ItemLocator {
): SubgraphInputNode | SubgraphOutputNode | undefined ): SubgraphInputNode | SubgraphOutputNode | undefined
} }
/**
* Minimal link routing information used for input resolution.
* Both LLink and partial link info objects satisfy this interface.
*/
export interface ILinkRouting {
/** Output node ID */
readonly origin_id: NodeId
/** Output slot index */
readonly origin_slot: number
/** Input node ID */
readonly target_id: NodeId
/** Input slot index */
readonly target_slot: number
}
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */ /** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
export interface LinkSegment { export interface LinkSegment {
/** Link / reroute ID */ /** Link / reroute ID */

View File

@@ -9,7 +9,11 @@ import type {
CallbackReturn, CallbackReturn,
ISlotType ISlotType
} from '@/lib/litegraph/src/interfaces' } from '@/lib/litegraph/src/interfaces'
import { LGraphEventMode, LiteGraph } from '@/lib/litegraph/src/litegraph' import {
LGraphEventMode,
LiteGraph,
LLink
} from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from './Subgraph' import type { Subgraph } from './Subgraph'
import type { SubgraphNode } from './SubgraphNode' import type { SubgraphNode } from './SubgraphNode'
@@ -289,7 +293,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
if (node.isVirtualNode) { if (node.isVirtualNode) {
const virtualLink = this.node.getInputLink(slot) const virtualLink = this.node.getInputLink(slot)
if (virtualLink) { if (virtualLink instanceof LLink) {
const { inputNode } = virtualLink.resolve(this.graph) const { inputNode } = virtualLink.resolve(this.graph)
if (!inputNode) if (!inputNode)
throw new InvalidLinkError( throw new InvalidLinkError(