diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index e3b3de5425..e2f210a29e 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -3,6 +3,8 @@ */ import type { Locator, Page } from '@playwright/test' +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' + export class VueNodeHelpers { constructor(private page: Page) {} @@ -39,7 +41,7 @@ export class VueNodeHelpers { /** * Get all Vue node IDs currently in the DOM */ - async getNodeIds(): Promise { + async getNodeIds(): Promise { return await this.nodes.evaluateAll((nodes) => nodes .map((n) => n.getAttribute('data-node-id')) @@ -50,14 +52,14 @@ export class VueNodeHelpers { /** * Select a specific Vue node by ID */ - async selectNode(nodeId: string): Promise { + async selectNode(nodeId: nodeId): Promise { await this.page.locator(`[data-node-id="${nodeId}"]`).click() } /** * Select multiple Vue nodes by IDs using Ctrl+click */ - async selectNodes(nodeIds: string[]): Promise { + async selectNodes(nodeIds: NodeId[]): Promise { if (nodeIds.length === 0) return // Select first node normally diff --git a/browser_tests/tests/chatHistory.spec.ts b/browser_tests/tests/chatHistory.spec.ts index 7d1bf6c105..8f08055d04 100644 --- a/browser_tests/tests/chatHistory.spec.ts +++ b/browser_tests/tests/chatHistory.spec.ts @@ -1,6 +1,8 @@ import type { Page } from '@playwright/test' import { expect } from '@playwright/test' +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' + import { comfyPageFixture as test } from '../fixtures/ComfyPage' interface ChatHistoryEntry { @@ -33,7 +35,7 @@ async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) { } test.describe('Chat History Widget', () => { - let nodeId: string + let nodeId: NodeId test.beforeEach(async ({ comfyPage }) => { nodeId = await renderChatHistory(comfyPage.page, [ diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 139919c60b..75a4a706d9 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -574,7 +574,7 @@ export class LGraph const S: LGraphNode[] = [] const M: Dictionary = {} // to avoid repeating links - const visited_links: Record = {} + const visited_links: Record = {} const remaining_links: Record = {} // search for the nodes without inputs (starting nodes) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 83ce476602..751cf7c5d9 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -6168,14 +6168,14 @@ export class LGraphCanvas case 'Delete': { // segment can be a Reroute object, in which case segment.id is the reroute id - const linkId = - segment instanceof Reroute - ? segment.linkIds.values().next().value - : segment.id - if (linkId !== undefined) { - graph.removeLink(linkId) - // Clean up layout store - layoutStore.deleteLinkLayout(linkId) + const linkIds = + segment instanceof Reroute ? segment.linkIds : [segment.id] + for (const linkId of linkIds) { + if (linkId !== undefined) { + graph.removeLink(linkId) + // Clean up layout store + layoutStore.deleteLinkLayout(linkId) + } } break } diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 20e26e211d..88af48f02d 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -40,7 +40,8 @@ import type { ReadOnlyPoint, ReadOnlyRect, Rect, - Size + Size, + UniqueId } from './interfaces' import { type LGraphNodeConstructor, @@ -90,7 +91,7 @@ import { type WidgetTypeMap, toConcreteWidget } from './widgets/widgetMap' // #region Types -export type NodeId = number | string +export type NodeId = UniqueId export type NodeProperty = string | number | boolean | object diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts index 71b41f23d0..ebcd023510 100644 --- a/src/lib/litegraph/src/LLink.ts +++ b/src/lib/litegraph/src/LLink.ts @@ -9,12 +9,14 @@ import type { LGraphNode, NodeId } from './LGraphNode' import type { Reroute, RerouteId } from './Reroute' import type { CanvasColour, + ILinkSegment, INodeInputSlot, INodeOutputSlot, ISlotType, LinkNetwork, LinkSegment, - ReadonlyLinkNetwork + ReadonlyLinkNetwork, + UniqueId } from './interfaces' import type { Serialisable, @@ -24,7 +26,7 @@ import type { const layoutMutations = useLayoutMutations() -export type LinkId = number +export type LinkId = UniqueId export type SerialisedLLinkArray = [ id: LinkId, @@ -90,7 +92,7 @@ type BasicReadonlyNetwork = Pick< > // this is the class in charge of storing link information -export class LLink implements LinkSegment, Serialisable { +export class LLink implements ILinkSegment, Serialisable { static _drawDebug = false /** Link ID */ diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index 4ac6825994..9bf7ffee09 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -6,21 +6,22 @@ import type { LGraphNode, NodeId } from './LGraphNode' import { LLink, type LinkId } from './LLink' import type { CanvasColour, + ILinkSegment, INodeInputSlot, INodeOutputSlot, LinkNetwork, - LinkSegment, Point, Positionable, ReadOnlyRect, - ReadonlyLinkNetwork + ReadonlyLinkNetwork, + UniqueId } from './interfaces' import { distance, isPointInRect } from './measure' import type { Serialisable, SerialisableReroute } from './types/serialisation' const layoutMutations = useLayoutMutations() -export type RerouteId = number +export type RerouteId = UniqueId /** The input or output slot that an incomplete reroute link is connected to. */ export interface FloatingRerouteSlot { @@ -36,7 +37,7 @@ export interface FloatingRerouteSlot { * and a `WeakRef` to a {@link LinkNetwork} to resolve them. */ export class Reroute - implements Positionable, LinkSegment, Serialisable + implements Positionable, ILinkSegment, Serialisable { static radius: number = 10 /** Maximum distance from reroutes to their bezier curve control points. */ diff --git a/src/lib/litegraph/src/interfaces.ts b/src/lib/litegraph/src/interfaces.ts index 359ccfd5fd..39066c6b9e 100644 --- a/src/lib/litegraph/src/interfaces.ts +++ b/src/lib/litegraph/src/interfaces.ts @@ -11,6 +11,9 @@ import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' import type { LinkDirection, RenderShape } from './types/globalEnums' import type { IBaseWidget } from './types/widgets' +declare const __brand: unique symbol +export type UniqueId = T & { [__brand]?: B } + export type Dictionary = { [key: string]: T } /** Allows all properties to be null. The same as `Partial`, but adds null instead of undefined. */ @@ -183,8 +186,10 @@ export interface ItemLocator { ): SubgraphInputNode | SubgraphOutputNode | undefined } +export type LinkSegment = Reroute | LLink + /** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */ -export interface LinkSegment { +export interface ILinkSegment { /** Link / reroute ID */ readonly id: LinkId | RerouteId /** The {@link id} of the reroute that this segment starts from (output side), otherwise `undefined`. */ diff --git a/src/platform/workflow/management/stores/workflowStore.ts b/src/platform/workflow/management/stores/workflowStore.ts index 02b48c55bb..16c05df6cb 100644 --- a/src/platform/workflow/management/stores/workflowStore.ts +++ b/src/platform/workflow/management/stores/workflowStore.ts @@ -182,14 +182,14 @@ interface WorkflowStore { activeSubgraph: Subgraph | undefined /** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */ updateActiveGraph: () => void - executionIdToCurrentId: (id: string) => any + executionIdToCurrentId: (id: NodeExecutionId) => any nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId nodeExecutionIdToNodeLocatorId: ( - nodeExecutionId: NodeExecutionId | string + nodeExecutionId: NodeExecutionId ) => NodeLocatorId | null - nodeLocatorIdToNodeId: (locatorId: NodeLocatorId | string) => NodeId | null + nodeLocatorIdToNodeId: (locatorId: NodeLocatorId) => NodeId | null nodeLocatorIdToNodeExecutionId: ( - locatorId: NodeLocatorId | string, + locatorId: NodeLocatorId, targetSubgraph?: Subgraph ) => NodeExecutionId | null } @@ -518,14 +518,14 @@ export const useWorkflowStore = defineStore('workflow', () => { isSubgraphActive.value = isSubgraph(subgraph) } - const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => { + const subgraphNodeIdToSubgraph = (id: NodeId, graph: LGraph | Subgraph) => { const node = graph.getNodeById(id) if (node?.isSubgraphNode()) return node.subgraph } const getSubgraphsFromInstanceIds = ( currentGraph: LGraph | Subgraph, - subgraphNodeIds: string[], + subgraphNodeIds: NodeId[], subgraphs: Subgraph[] = [] ): Subgraph[] => { const currentPart = subgraphNodeIds.shift() @@ -538,7 +538,7 @@ export const useWorkflowStore = defineStore('workflow', () => { return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs) } - const executionIdToCurrentId = (id: string) => { + const executionIdToCurrentId = (id: NodeExecutionId) => { const subgraph = activeSubgraph.value // Short-circuit: ID belongs to the parent workflow / no active subgraph @@ -588,11 +588,11 @@ export const useWorkflowStore = defineStore('workflow', () => { * @returns The NodeLocatorId or null if conversion fails */ const nodeExecutionIdToNodeLocatorId = ( - nodeExecutionId: NodeExecutionId | string + nodeExecutionId: NodeExecutionId ): NodeLocatorId | null => { // Handle simple node IDs (root graph - no colons) if (!nodeExecutionId.includes(':')) { - return nodeExecutionId + return nodeExecutionId as NodeLocatorId } const parts = parseNodeExecutionId(nodeExecutionId) @@ -623,9 +623,7 @@ export const useWorkflowStore = defineStore('workflow', () => { * @param locatorId The NodeLocatorId * @returns The local node ID or null if invalid */ - const nodeLocatorIdToNodeId = ( - locatorId: NodeLocatorId | string - ): NodeId | null => { + const nodeLocatorIdToNodeId = (locatorId: NodeLocatorId): NodeId | null => { const parsed = parseNodeLocatorId(locatorId) return parsed?.localNodeId ?? null } @@ -637,7 +635,7 @@ export const useWorkflowStore = defineStore('workflow', () => { * @returns The execution ID or null if the node is not accessible from the target context */ const nodeLocatorIdToNodeExecutionId = ( - locatorId: NodeLocatorId | string, + locatorId: NodeLocatorId, targetSubgraph?: Subgraph ): NodeExecutionId | null => { const parsed = parseNodeLocatorId(locatorId) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 2149bb3787..d4d50653ee 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -151,8 +151,9 @@ export const useLitegraphService = () => { */ #setupStrokeStyles() { this.strokeStyles['running'] = function (this: LGraphNode) { - const nodeId = String(this.id) - const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId) + const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId( + this.id + ) const state = useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state if (state === 'running') { @@ -376,7 +377,7 @@ export const useLitegraphService = () => { node.title = nodeDef.display_name || nodeDef.name } - async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) { + async function registerNodeDef(type: string, nodeDefV1: ComfyNodeDefV1) { const node = class ComfyNode extends LGraphNode { static comfyClass: string static override title: string @@ -416,8 +417,9 @@ export const useLitegraphService = () => { */ #setupStrokeStyles() { this.strokeStyles['running'] = function (this: LGraphNode) { - const nodeId = String(this.id) - const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId) + const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId( + this.id + ) const state = useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state if (state === 'running') { @@ -649,7 +651,7 @@ export const useLitegraphService = () => { const nodeDef = new ComfyNodeDefImpl(nodeDefV1) node.nodeData = nodeDef - LiteGraph.registerNodeType(nodeId, node) + LiteGraph.registerNodeType(type, node) // Note: Do not following assignments before `LiteGraph.registerNodeType` // because `registerNodeType` will overwrite the assignments. node.category = nodeDef.category diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index 8791ab4e14..d858f0cd5e 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -28,7 +28,7 @@ import type { import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { useNodeOutputStore } from '@/stores/imagePreviewStore' -import type { NodeLocatorId } from '@/types/nodeIdentification' +import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification' import { createNodeLocatorId } from '@/types/nodeIdentification' interface QueuedPrompt { @@ -78,8 +78,10 @@ function getSubgraphsFromInstanceIds( * @param nodeId The node ID from execution context (could be execution ID) * @returns The NodeLocatorId */ -function executionIdToNodeLocatorId(nodeId: string | number): NodeLocatorId { - const nodeIdStr = String(nodeId) +function executionIdToNodeLocatorId( + executionId: NodeExecutionId +): NodeLocatorId { + const nodeIdStr = String(executionId) if (!nodeIdStr.includes(':')) { // It's a top-level node ID diff --git a/src/types/nodeIdentification.ts b/src/types/nodeIdentification.ts index d9a299c398..10d7956f12 100644 --- a/src/types/nodeIdentification.ts +++ b/src/types/nodeIdentification.ts @@ -1,3 +1,4 @@ +import type { UniqueId } from '@/lib/litegraph/src/interfaces' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' /** @@ -15,7 +16,7 @@ import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSche * Unlike execution IDs which change based on the instance path, * NodeLocatorId remains the same for all instances of a particular node. */ -export type NodeLocatorId = string +export type NodeLocatorId = UniqueId /** * An execution identifier representing a node's position in nested subgraphs. @@ -24,7 +25,8 @@ export type NodeLocatorId = string * Format: Colon-separated path of node IDs * Example: "123:456:789" (node 789 in subgraph 456 in subgraph 123) */ -export type NodeExecutionId = string +declare const __executionIdBrand: unique symbol +export type NodeExecutionId = UniqueId /** * Type guard to check if a value is a NodeLocatorId @@ -105,7 +107,7 @@ export function createNodeLocatorId( * @param id The NodeExecutionId to parse * @returns Array of node IDs from root to target, or null if not an execution ID */ -export function parseNodeExecutionId(id: string): NodeId[] | null { +export function parseNodeExecutionId(id: NodeExecutionId): NodeId[] | null { if (!isNodeExecutionId(id)) return null return id diff --git a/src/utils/graphTraversalUtil.ts b/src/utils/graphTraversalUtil.ts index 4a573ed24c..9e30acfce5 100644 --- a/src/utils/graphTraversalUtil.ts +++ b/src/utils/graphTraversalUtil.ts @@ -1,3 +1,4 @@ +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import type { LGraph, LGraphNode, @@ -9,7 +10,7 @@ import { parseNodeLocatorId } from '@/types/nodeIdentification' import { isSubgraphIoNode } from './typeGuardUtil' interface NodeWithId { - id: string | number + id: NodeId subgraphId?: string | null } @@ -19,7 +20,7 @@ interface NodeWithId { * @param nodeData - Node data containing id and optional subgraphId * @returns The locator ID string */ -export function getLocatorIdFromNodeData(nodeData: NodeWithId): string { +export function getLocatorIdFromNodeData(nodeData: NodeWithId): NodeLocatorId { return nodeData.subgraphId ? `${nodeData.subgraphId}:${String(nodeData.id)}` : String(nodeData.id) @@ -31,7 +32,9 @@ export function getLocatorIdFromNodeData(nodeData: NodeWithId): string { * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns Array of node IDs in the path, or null if invalid */ -export function parseExecutionId(executionId: string): string[] | null { +export function parseExecutionId( + executionId: NodeExecutionId +): string[] | null { if (!executionId || typeof executionId !== 'string') return null return executionId.split(':').filter((part) => part.length > 0) } @@ -55,7 +58,9 @@ export function getLocalNodeIdFromExecutionId( * @param executionId - The execution ID (e.g., "123:456:789" or "789") * @returns Array of subgraph node IDs (excluding the final node ID), or empty array */ -export function getSubgraphPathFromExecutionId(executionId: string): string[] { +export function getSubgraphPathFromExecutionId( + executionId: NodeExecutionId +): string[] { const parts = parseExecutionId(executionId) return parts ? parts.slice(0, -1) : [] } @@ -196,7 +201,7 @@ export function collectAllNodes( */ export function findNodeInHierarchy( graph: LGraph | Subgraph, - nodeId: string | number + nodeId: NodeId ): LGraphNode | null { // Check current graph const node = graph.getNodeById(nodeId) @@ -284,7 +289,7 @@ export function findSubgraphPathById( */ export function getNodeByExecutionId( rootGraph: LGraph, - executionId: string + executionId: NodeExecutionId ): LGraphNode | null { if (!rootGraph) return null @@ -316,7 +321,7 @@ export function getNodeByExecutionId( */ export function getNodeByLocatorId( rootGraph: LGraph, - locatorId: NodeLocatorId | string + locatorId: NodeLocatorId ): LGraphNode | null { if (!rootGraph) return null diff --git a/src/utils/linkFixer.ts b/src/utils/linkFixer.ts index 1c26afb298..f259888d2e 100644 --- a/src/utils/linkFixer.ts +++ b/src/utils/linkFixer.ts @@ -25,7 +25,7 @@ * SOFTWARE. */ import type { NodeId } from '@/lib/litegraph/src/LGraphNode' -import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' +import type { LinkId, SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' import type { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph' import type { ISerialisedGraph, @@ -105,7 +105,7 @@ export function fixBadLinks( const data: { patchedNodes: Array - deletedLinks: number[] + deletedLinks: LinkId[] } = { patchedNodes: [], deletedLinks: [] @@ -419,7 +419,7 @@ export function fixBadLinks( for (let i = data.deletedLinks.length - 1; i >= 0; i--) { logger.log(`Deleting link #${data.deletedLinks[i]}.`) if ((graph as LGraph).getNodeById) { - delete graph.links[data.deletedLinks[i]!] + delete (graph as LGraph).links[data.deletedLinks[i]!] } else { graph = graph as ISerialisedGraph // Sometimes we got objects for links if passed after ComfyUI's loadGraphData modifies the diff --git a/tests-ui/tests/store/executionStore.test.ts b/tests-ui/tests/store/executionStore.test.ts index 498148e577..077bdada54 100644 --- a/tests-ui/tests/store/executionStore.test.ts +++ b/tests-ui/tests/store/executionStore.test.ts @@ -16,7 +16,6 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ // Remove any previous global types declare global { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface Window {} } @@ -148,7 +147,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { }) it('should handle numeric node IDs', () => { - const result = store.executionIdToNodeLocatorId(123) + const result = store.executionIdToNodeLocatorId('123') // For numeric IDs, it should convert to string and return as-is expect(result).toBe('123') diff --git a/tests-ui/tests/utils/graphTraversalUtil.test.ts b/tests-ui/tests/utils/graphTraversalUtil.test.ts index f6cb748042..a434429e98 100644 --- a/tests-ui/tests/utils/graphTraversalUtil.test.ts +++ b/tests-ui/tests/utils/graphTraversalUtil.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import type { LGraph, LGraphNode, @@ -61,7 +62,7 @@ function createMockSubgraph(id: string, nodes: LGraphNode[]): Subgraph { id, _nodes: nodes, nodes: nodes, - getNodeById: (nodeId: string | number) => + getNodeById: (nodeId: NodeId) => nodes.find((n) => String(n.id) === String(nodeId)) || null } as unknown as Subgraph }