refactor: migrate ES private fields to TypeScript private for Vue Proxy compatibility (#8440)

## Summary

Migrates ECMAScript private fields (`#`) to TypeScript private
(`private`) across LiteGraph to fix Vue Proxy reactivity
incompatibility.

## Problem

ES private fields (`#field`) are incompatible with Vue's Proxy-based
reactivity system - accessing `#field` through a Proxy throws
`TypeError: Cannot read private member from an object whose class did
not declare it`.

## Solution

- Converted all `#field` to `private _field` across 10 phases
- Added `toJSON()` methods to `LGraph`, `NodeSlot`, `NodeInputSlot`, and
`NodeOutputSlot` to prevent circular reference errors during
serialization (TypeScript private fields are visible to `JSON.stringify`
unlike true ES private fields)
- Made `DragAndScale.element.data` non-enumerable to break canvas
circular reference chain

## Testing

- All 4027 unit tests pass
- Added 9 new serialization tests to catch future circular reference
issues
- Browser tests (undo/redo, save workflows) verified working

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8440-refactor-migrate-ES-private-fields-to-TypeScript-private-for-Vue-Proxy-compatibility-2f76d73d365081a3bd82d429a3e0fcb7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-29 18:18:58 -08:00
committed by GitHub
parent 82bacb82a7
commit 067d80c4ed
28 changed files with 653 additions and 705 deletions

View File

@@ -54,7 +54,7 @@ class ConversionContext {
/** Reroutes that has at least a valid link pass through it */
validReroutes: Set<Reroute>
#rerouteIdCounter = 0
private _rerouteIdCounter = 0
constructor(public workflow: WorkflowJSON04) {
this.nodeById = _.keyBy(workflow.nodes.map(_.cloneDeep), 'id')
@@ -76,7 +76,7 @@ class ConversionContext {
pos: getNodeCenter(node),
linkIds: []
}))
this.#rerouteIdCounter = reroutes.length + 1
this._rerouteIdCounter = reroutes.length + 1
this.rerouteByNodeId = _.keyBy(reroutes, 'nodeId')
this.rerouteById = _.keyBy(reroutes, 'id')
@@ -88,7 +88,7 @@ class ConversionContext {
/**
* Gets the chain of reroute nodes leading to the given node
*/
#getRerouteChain(node: RerouteNode): RerouteNode[] {
private _getRerouteChain(node: RerouteNode): RerouteNode[] {
const nodes: RerouteNode[] = []
let currentNode: RerouteNode = node
while (currentNode?.type === 'Reroute') {
@@ -106,7 +106,7 @@ class ConversionContext {
return nodes
}
#connectRerouteChain(rerouteNodes: RerouteNode[]): Reroute[] {
private _connectRerouteChain(rerouteNodes: RerouteNode[]): Reroute[] {
const reroutes = rerouteNodes.map((node) => this.rerouteByNodeId[node.id])
for (const reroute of reroutes) {
this.validReroutes.add(reroute)
@@ -121,7 +121,7 @@ class ConversionContext {
return reroutes
}
#createNewLink(
private _createNewLink(
startingLink: ComfyLinkObject,
endingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
@@ -136,7 +136,7 @@ class ConversionContext {
parentId: reroute.id
})
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
reroute.linkIds ??= []
reroute.linkIds.push(endingLink.id)
@@ -153,11 +153,11 @@ class ConversionContext {
}
}
#createNewInputFloatingLink(
private _createNewInputFloatingLink(
endingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
): ComfyLinkObject {
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
if (!reroute.linkIds?.length) {
reroute.floating = {
@@ -166,7 +166,7 @@ class ConversionContext {
}
}
return {
id: this.#rerouteIdCounter++,
id: this._rerouteIdCounter++,
origin_id: -1,
origin_slot: -1,
target_id: endingLink.target_id,
@@ -176,11 +176,11 @@ class ConversionContext {
}
}
#createNewOutputFloatingLink(
private _createNewOutputFloatingLink(
startingLink: ComfyLinkObject,
rerouteNodes: RerouteNode[]
): ComfyLinkObject {
const reroutes = this.#connectRerouteChain(rerouteNodes)
const reroutes = this._connectRerouteChain(rerouteNodes)
for (const reroute of reroutes) {
if (!reroute.linkIds?.length) {
reroute.floating = {
@@ -190,7 +190,7 @@ class ConversionContext {
}
return {
id: this.#rerouteIdCounter++,
id: this._rerouteIdCounter++,
origin_id: startingLink.origin_id,
origin_slot: startingLink.origin_slot,
target_id: -1,
@@ -200,7 +200,7 @@ class ConversionContext {
}
}
#reconnectLinks(nodes: ComfyNode[], links: ComfyLinkObject[]): void {
private _reconnectLinks(nodes: ComfyNode[], links: ComfyLinkObject[]): void {
// Remove all existing links on sockets
for (const node of nodes) {
for (const input of node.inputs ?? []) {
@@ -245,18 +245,18 @@ class ConversionContext {
const endingRerouteNode = this.nodeById[
endingLink.origin_id
] as RerouteNode
const rerouteNodes = this.#getRerouteChain(endingRerouteNode)
const rerouteNodes = this._getRerouteChain(endingRerouteNode)
const startingLink =
this.linkById[
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
]
if (startingLink) {
// Valid link found, create a new link
links.push(this.#createNewLink(startingLink, endingLink, rerouteNodes))
links.push(this._createNewLink(startingLink, endingLink, rerouteNodes))
} else {
// Floating link found, create a new floating link
floatingLinks.push(
this.#createNewInputFloatingLink(endingLink, rerouteNodes)
this._createNewInputFloatingLink(endingLink, rerouteNodes)
)
}
}
@@ -270,14 +270,14 @@ class ConversionContext {
})
for (const rerouteNode of floatingEndingRerouteNodes) {
const rerouteNodes = this.#getRerouteChain(rerouteNode)
const rerouteNodes = this._getRerouteChain(rerouteNode)
const startingLink =
this.linkById[
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
]
if (startingLink) {
floatingLinks.push(
this.#createNewOutputFloatingLink(startingLink, rerouteNodes)
this._createNewOutputFloatingLink(startingLink, rerouteNodes)
)
}
}
@@ -285,7 +285,7 @@ class ConversionContext {
const nodes = Object.values(this.nodeById).filter(
(node) => node.type !== 'Reroute'
)
this.#reconnectLinks(nodes, links)
this._reconnectLinks(nodes, links)
return {
...this.workflow,