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

@@ -241,17 +241,17 @@ function dragElement(dragEl): () => void {
}
class ComfyList {
#type
#text
#reverse
private _type
private _text
private _reverse
element: HTMLDivElement
button?: HTMLButtonElement
// @ts-expect-error fixme ts strict error
constructor(text, type?, reverse?) {
this.#text = text
this.#type = type || text.toLowerCase()
this.#reverse = reverse || false
this._text = text
this._type = type || text.toLowerCase()
this._reverse = reverse || false
this.element = $el('div.comfy-list') as HTMLDivElement
this.element.style.display = 'none'
}
@@ -261,7 +261,7 @@ class ComfyList {
}
async load() {
const items = await api.getItems(this.#type)
const items = await api.getItems(this._type)
this.element.replaceChildren(
...Object.keys(items).flatMap((section) => [
$el('h4', {
@@ -269,12 +269,12 @@ class ComfyList {
}),
$el('div.comfy-list-items', [
// @ts-expect-error fixme ts strict error
...(this.#reverse ? items[section].reverse() : items[section]).map(
...(this._reverse ? items[section].reverse() : items[section]).map(
(item: LegacyQueueItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction = item.remove ?? {
name: 'Delete',
cb: () => api.deleteItem(this.#type, item.prompt[1])
cb: () => api.deleteItem(this._type, item.prompt[1])
}
return $el('div', { textContent: item.prompt[0] + ': ' }, [
$el('button', {
@@ -311,9 +311,9 @@ class ComfyList {
]),
$el('div.comfy-list-actions', [
$el('button', {
textContent: 'Clear ' + this.#text,
textContent: 'Clear ' + this._text,
onclick: async () => {
await api.clearItems(this.#type)
await api.clearItems(this._type)
await this.load()
}
}),
@@ -339,7 +339,7 @@ class ComfyList {
hide() {
this.element.style.display = 'none'
// @ts-expect-error fixme ts strict error
this.button.textContent = 'View ' + this.#text
this.button.textContent = 'View ' + this._text
}
toggle() {