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

View File

@@ -34,6 +34,7 @@ import type {
IColorable,
IContextMenuValue,
IFoundSlot,
ILinkRouting,
INodeFlags,
INodeInputSlot,
INodeOutputSlot,
@@ -1153,10 +1154,11 @@ export class LGraphNode
}
/**
* Returns the link info in the connection of an input slot
* @returns object or null
* Returns the link info in the connection of an input slot.
* 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 (slot < this.inputs.length) {

View File

@@ -11,6 +11,7 @@ import type { LGraphNode, NodeId } from './LGraphNode'
import type { Reroute, RerouteId } from './Reroute'
import type {
CanvasColour,
ILinkRouting,
INodeInputSlot,
INodeOutputSlot,
ISlotType,
@@ -89,7 +90,9 @@ type BasicReadonlyNetwork = Pick<
>
// 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
/** Link ID */

View File

@@ -184,6 +184,21 @@ export interface ItemLocator {
): 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. */
export interface LinkSegment {
/** Link / reroute ID */

View File

@@ -9,7 +9,11 @@ import type {
CallbackReturn,
ISlotType
} 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 { SubgraphNode } from './SubgraphNode'
@@ -289,7 +293,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
if (node.isVirtualNode) {
const virtualLink = this.node.getInputLink(slot)
if (virtualLink) {
if (virtualLink instanceof LLink) {
const { inputNode } = virtualLink.resolve(this.graph)
if (!inputNode)
throw new InvalidLinkError(