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

@@ -294,7 +294,7 @@ export class PromptExecutionError extends Error {
}
export class ComfyApi extends EventTarget {
#registered = new Set()
private _registered = new Set()
api_host: string
api_base: string
/**
@@ -451,7 +451,7 @@ export class ComfyApi extends EventTarget {
) {
// Type assertion: strictFunctionTypes. So long as we emit events in a type-safe fashion, this is safe.
super.addEventListener(type, callback as EventListener, options)
this.#registered.add(type)
this._registered.add(type)
}
override removeEventListener<TEvent extends keyof ApiEvents>(
@@ -492,7 +492,7 @@ export class ComfyApi extends EventTarget {
/**
* Poll status for colab and other things that don't support websockets.
*/
#pollQueue() {
private _pollQueue() {
setInterval(async () => {
try {
const resp = await this.fetchApi('/prompt')
@@ -568,7 +568,7 @@ export class ComfyApi extends EventTarget {
this.socket.addEventListener('error', () => {
if (this.socket) this.socket.close()
if (!isReconnect && !opened) {
this.#pollQueue()
this._pollQueue()
}
})
@@ -691,7 +691,7 @@ export class ComfyApi extends EventTarget {
)
break
default:
if (this.#registered.has(msg.type)) {
if (this._registered.has(msg.type)) {
// Fallback for custom types - calls super direct.
super.dispatchEvent(
new CustomEvent(msg.type, { detail: msg.data })
@@ -956,7 +956,7 @@ export class ComfyApi extends EventTarget {
* @param {*} type The endpoint to post to
* @param {*} body Optional POST data
*/
async #postItem(type: string, body?: Record<string, unknown>) {
private async _postItem(type: string, body?: Record<string, unknown>) {
try {
await this.fetchApi('/' + type, {
method: 'POST',
@@ -976,7 +976,7 @@ export class ComfyApi extends EventTarget {
* @param {number} id The id of the item to delete
*/
async deleteItem(type: string, id: string) {
await this.#postItem(type, { delete: [id] })
await this._postItem(type, { delete: [id] })
}
/**
@@ -984,7 +984,7 @@ export class ComfyApi extends EventTarget {
* @param {string} type The type of list to clear, queue or history
*/
async clearItems(type: string) {
await this.#postItem(type, { clear: true })
await this._postItem(type, { clear: true })
}
/**
@@ -993,7 +993,7 @@ export class ComfyApi extends EventTarget {
* @param {string | null} [runningPromptId] Optional Running Prompt ID to interrupt
*/
async interrupt(runningPromptId: string | null) {
await this.#postItem(
await this._postItem(
'interrupt',
runningPromptId ? { prompt_id: runningPromptId } : undefined
)