diff --git a/src/base/assert.test.ts b/src/base/assert.test.ts index 4dc00a6369..e7aa545f9d 100644 --- a/src/base/assert.test.ts +++ b/src/base/assert.test.ts @@ -1,58 +1,58 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { assert, setAssertReporter } from "@/base/assert"; +import { assert, setAssertReporter } from '@/base/assert' -describe("assert", () => { - let consoleErrorSpy: ReturnType; +describe('assert', () => { + let consoleErrorSpy: ReturnType beforeEach(() => { - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - }); + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + }) afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllEnvs(); - setAssertReporter(() => {}); - }); + vi.restoreAllMocks() + vi.unstubAllEnvs() + setAssertReporter(() => {}) + }) - it("does nothing when condition is true", () => { - expect(() => assert(true, "should not throw")).not.toThrow(); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); + it('does nothing when condition is true', () => { + expect(() => assert(true, 'should not throw')).not.toThrow() + expect(consoleErrorSpy).not.toHaveBeenCalled() + }) - it("logs console.error when condition is false", () => { - vi.stubEnv("DEV", false); - assert(false, "test message"); + it('logs console.error when condition is false', () => { + vi.stubEnv('DEV', false) + assert(false, 'test message') expect(consoleErrorSpy).toHaveBeenCalledWith( - "[Assertion failed]: test message", - ); - }); + '[Assertion failed]: test message' + ) + }) - it("throws in DEV mode when condition is false", () => { - vi.stubEnv("DEV", true); - expect(() => assert(false, "dev error")).toThrow( - "[Assertion failed]: dev error", - ); - }); + it('throws in DEV mode when condition is false', () => { + vi.stubEnv('DEV', true) + expect(() => assert(false, 'dev error')).toThrow( + '[Assertion failed]: dev error' + ) + }) - it("does not throw in non-DEV mode when condition is false", () => { - vi.stubEnv("DEV", false); - expect(() => assert(false, "non-dev error")).not.toThrow(); - }); + it('does not throw in non-DEV mode when condition is false', () => { + vi.stubEnv('DEV', false) + expect(() => assert(false, 'non-dev error')).not.toThrow() + }) - it("calls registered reporter in non-DEV mode", () => { - vi.stubEnv("DEV", false); - const reporter = vi.fn(); - setAssertReporter(reporter); - assert(false, "reporter message"); - expect(reporter).toHaveBeenCalledWith("reporter message"); - }); + it('calls registered reporter in non-DEV mode', () => { + vi.stubEnv('DEV', false) + const reporter = vi.fn() + setAssertReporter(reporter) + assert(false, 'reporter message') + expect(reporter).toHaveBeenCalledWith('reporter message') + }) - it("does not call reporter when condition is true", () => { - vi.stubEnv("DEV", false); - const reporter = vi.fn(); - setAssertReporter(reporter); - assert(true, "no call"); - expect(reporter).not.toHaveBeenCalled(); - }); -}); + it('does not call reporter when condition is true', () => { + vi.stubEnv('DEV', false) + const reporter = vi.fn() + setAssertReporter(reporter) + assert(true, 'no call') + expect(reporter).not.toHaveBeenCalled() + }) +}) diff --git a/src/base/assert.ts b/src/base/assert.ts index 6d36bc107a..858a8f5b7d 100644 --- a/src/base/assert.ts +++ b/src/base/assert.ts @@ -1,6 +1,6 @@ -type AssertReporter = (message: string) => void; +type AssertReporter = (message: string) => void -let reporter: AssertReporter | null = null; +let reporter: AssertReporter | null = null /** * Register a reporter for assertion failures in non-DEV environments. @@ -8,7 +8,7 @@ let reporter: AssertReporter | null = null; * Sentry, toast notifications, etc. */ export function setAssertReporter(fn: AssertReporter): void { - reporter = fn; + reporter = fn } /** @@ -19,13 +19,13 @@ export function setAssertReporter(fn: AssertReporter): void { * - Otherwise: delegates to registered reporter (Sentry, toast, etc.) */ export function assert(condition: boolean, message: string): void { - if (condition) return; + if (condition) return - console.error(`[Assertion failed]: ${message}`); + console.error(`[Assertion failed]: ${message}`) if (import.meta.env.DEV) { - throw new Error(`[Assertion failed]: ${message}`); + throw new Error(`[Assertion failed]: ${message}`) } - reporter?.(message); + reporter?.(message) } diff --git a/src/main.ts b/src/main.ts index 9a1456a0b5..5ab074b9d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,63 +1,63 @@ -import { definePreset } from "@primevue/themes"; -import Aura from "@primevue/themes/aura"; -import * as Sentry from "@sentry/vue"; -import { initializeApp } from "firebase/app"; -import { createPinia } from "pinia"; -import "primeicons/primeicons.css"; -import PrimeVue from "primevue/config"; -import ConfirmationService from "primevue/confirmationservice"; -import ToastService from "primevue/toastservice"; -import Tooltip from "primevue/tooltip"; -import { createApp } from "vue"; -import { VueFire, VueFireAuth } from "vuefire"; +import { definePreset } from '@primevue/themes' +import Aura from '@primevue/themes/aura' +import * as Sentry from '@sentry/vue' +import { initializeApp } from 'firebase/app' +import { createPinia } from 'pinia' +import 'primeicons/primeicons.css' +import PrimeVue from 'primevue/config' +import ConfirmationService from 'primevue/confirmationservice' +import ToastService from 'primevue/toastservice' +import Tooltip from 'primevue/tooltip' +import { createApp } from 'vue' +import { VueFire, VueFireAuth } from 'vuefire' -import { setAssertReporter } from "@/base/assert"; -import { getFirebaseConfig } from "@/config/firebase"; +import { setAssertReporter } from '@/base/assert' +import { getFirebaseConfig } from '@/config/firebase' import { configValueOrDefault, - remoteConfig, -} from "@/platform/remoteConfig/remoteConfig"; -import "@/lib/litegraph/public/css/litegraph.css"; -import router from "@/router"; -import { isDesktop, isNightly } from "@/platform/distribution/types"; -import { useToastStore } from "@/platform/updates/common/toastStore"; -import { useBootstrapStore } from "@/stores/bootstrapStore"; + remoteConfig +} from '@/platform/remoteConfig/remoteConfig' +import '@/lib/litegraph/public/css/litegraph.css' +import router from '@/router' +import { isDesktop, isNightly } from '@/platform/distribution/types' +import { useToastStore } from '@/platform/updates/common/toastStore' +import { useBootstrapStore } from '@/stores/bootstrapStore' -import App from "./App.vue"; +import App from './App.vue' // Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css) -import "./assets/css/style.css"; -import { i18n } from "./i18n"; +import './assets/css/style.css' +import { i18n } from './i18n' /** * CRITICAL: Load remote config FIRST for cloud builds to ensure * window.__CONFIG__is available for all modules during initialization */ -const isCloud = __DISTRIBUTION__ === "cloud"; +const isCloud = __DISTRIBUTION__ === 'cloud' if (isCloud) { const { refreshRemoteConfig } = - await import("@/platform/remoteConfig/refreshRemoteConfig"); - await refreshRemoteConfig({ useAuth: false }); + await import('@/platform/remoteConfig/refreshRemoteConfig') + await refreshRemoteConfig({ useAuth: false }) - const { initTelemetry } = await import("@/platform/telemetry/initTelemetry"); - await initTelemetry(); + const { initTelemetry } = await import('@/platform/telemetry/initTelemetry') + await initTelemetry() } const ComfyUIPreset = definePreset(Aura, { semantic: { // @ts-expect-error fixme ts strict error - primary: Aura["primitive"].blue, - }, -}); + primary: Aura['primitive'].blue + } +}) -const firebaseApp = initializeApp(getFirebaseConfig()); +const firebaseApp = initializeApp(getFirebaseConfig()) -const app = createApp(App); -const pinia = createPinia(); +const app = createApp(App) +const pinia = createPinia() const sentryDsn = isCloud - ? configValueOrDefault(remoteConfig.value, "sentry_dsn", __SENTRY_DSN__) - : __SENTRY_DSN__; + ? configValueOrDefault(remoteConfig.value, 'sentry_dsn', __SENTRY_DSN__) + : __SENTRY_DSN__ Sentry.init({ app, @@ -75,47 +75,47 @@ Sentry.init({ // Disable event target wrapping to reduce overhead on high-frequency // DOM events (pointermove, mousemove, wheel). Sentry still captures // errors via window.onerror and unhandledrejection. - Sentry.browserApiErrorsIntegration({ eventTarget: false }), - ], + Sentry.browserApiErrorsIntegration({ eventTarget: false }) + ] } : { integrations: [], autoSessionTracking: false, - defaultIntegrations: false, - }), -}); + defaultIntegrations: false + }) +}) setAssertReporter((message) => { if (isDesktop) { Sentry.captureMessage(`[Assertion failed]: ${message}`, { - level: "warning", - }); + level: 'warning' + }) } if (isNightly) { useToastStore().add({ - severity: "warn", - summary: "Assertion failed", - detail: message, - }); + severity: 'warn', + summary: 'Assertion failed', + detail: message + }) } -}); +}) -app.directive("tooltip", Tooltip); +app.directive('tooltip', Tooltip) app .use(router) .use(PrimeVue, { theme: { preset: ComfyUIPreset, options: { - prefix: "p", + prefix: 'p', cssLayer: { - name: "primevue", - order: "theme, base, primevue", + name: 'primevue', + order: 'theme, base, primevue' }, // This is a workaround for the issue with the dark mode selector // https://github.com/primefaces/primevue/issues/5515 - darkModeSelector: ".dark-theme, :root:has(.dark-theme)", - }, - }, + darkModeSelector: '.dark-theme, :root:has(.dark-theme)' + } + } }) .use(ConfirmationService) .use(ToastService) @@ -123,10 +123,10 @@ app .use(i18n) .use(VueFire, { firebaseApp, - modules: [VueFireAuth()], - }); + modules: [VueFireAuth()] + }) -const bootstrapStore = useBootstrapStore(pinia); -void bootstrapStore.startStoreBootstrap(); +const bootstrapStore = useBootstrapStore(pinia) +void bootstrapStore.startStoreBootstrap() -app.mount("#vue-app"); +app.mount('#vue-app') diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts index af2c2f08cb..54312ea239 100644 --- a/src/scripts/changeTracker.ts +++ b/src/scripts/changeTracker.ts @@ -1,46 +1,46 @@ -import _ from "es-toolkit/compat"; +import _ from 'es-toolkit/compat' -import { assert } from "@/base/assert"; -import type { CanvasPointerEvent } from "@/lib/litegraph/src/litegraph"; -import { LGraphCanvas, LiteGraph } from "@/lib/litegraph/src/litegraph"; -import type { ComfyWorkflow } from "@/platform/workflow/management/stores/workflowStore"; -import { useWorkflowStore } from "@/platform/workflow/management/stores/workflowStore"; -import type { ComfyWorkflowJSON } from "@/platform/workflow/validation/schemas/workflowSchema"; -import type { ExecutedWsMessage } from "@/schemas/apiSchema"; -import { useExecutionStore } from "@/stores/executionStore"; -import { useNodeOutputStore } from "@/stores/nodeOutputStore"; -import { useSubgraphNavigationStore } from "@/stores/subgraphNavigationStore"; +import { assert } from '@/base/assert' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph' +import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph' +import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' +import type { ExecutedWsMessage } from '@/schemas/apiSchema' +import { useExecutionStore } from '@/stores/executionStore' +import { useNodeOutputStore } from '@/stores/nodeOutputStore' +import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' -import { api } from "./api"; -import type { ComfyApp } from "./app"; -import { app } from "./app"; +import { api } from './api' +import type { ComfyApp } from './app' +import { app } from './app' function clone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); + return JSON.parse(JSON.stringify(obj)) } function isActiveTracker(tracker: ChangeTracker): boolean { - return useWorkflowStore().activeWorkflow?.changeTracker === tracker; + return useWorkflowStore().activeWorkflow?.changeTracker === tracker } -const reportedInactiveCalls = new Set(); +const reportedInactiveCalls = new Set() /** * Report a ChangeTracker method being called on an inactive tracker. * Deduplicates per method+workflow per session to avoid signal noise on hot paths. */ function reportInactiveTrackerCall(method: string, workflowPath: string) { - const key = `${method}:${workflowPath}`; - if (reportedInactiveCalls.has(key)) return; - reportedInactiveCalls.add(key); + const key = `${method}:${workflowPath}` + if (reportedInactiveCalls.has(key)) return + reportedInactiveCalls.add(key) assert( false, - `ChangeTracker.${method}() called on inactive tracker for: ${workflowPath}`, - ); + `ChangeTracker.${method}() called on inactive tracker for: ${workflowPath}` + ) } export class ChangeTracker { - static MAX_HISTORY = 50; + static MAX_HISTORY = 50 /** * Guard flag to prevent captureCanvasState from running during loadGraphData. * Between rootGraph.configure() and afterLoadNewGraph(), the rootGraph @@ -48,25 +48,25 @@ export class ChangeTracker { * the OLD workflow. Any captureCanvasState call in that window would * serialize the wrong graph into the old workflow's activeState, corrupting it. */ - static isLoadingGraph = false; + static isLoadingGraph = false /** * The active state of the workflow. */ - activeState: ComfyWorkflowJSON; - undoQueue: ComfyWorkflowJSON[] = []; - redoQueue: ComfyWorkflowJSON[] = []; - changeCount: number = 0; + activeState: ComfyWorkflowJSON + undoQueue: ComfyWorkflowJSON[] = [] + redoQueue: ComfyWorkflowJSON[] = [] + changeCount: number = 0 /** * Whether the redo/undo restoring is in progress. */ - _restoringState: boolean = false; + _restoringState: boolean = false - ds?: { scale: number; offset: [number, number] }; - nodeOutputs?: Record; + ds?: { scale: number; offset: [number, number] } + nodeOutputs?: Record private subgraphState?: { - navigation: string[]; - }; + navigation: string[] + } constructor( /** @@ -76,9 +76,9 @@ export class ChangeTracker { /** * The initial state of the workflow */ - public initialState: ComfyWorkflowJSON, + public initialState: ComfyWorkflowJSON ) { - this.activeState = initialState; + this.activeState = initialState } /** @@ -86,21 +86,21 @@ export class ChangeTracker { */ reset(state?: ComfyWorkflowJSON) { // Do not reset the state if we are restoring. - if (this._restoringState) return; + if (this._restoringState) return - if (state) this.activeState = clone(state); - this.initialState = clone(this.activeState); + if (state) this.activeState = clone(state) + this.initialState = clone(this.activeState) } store() { this.ds = { scale: app.canvas.ds.scale, - offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]], - }; - this.nodeOutputs = useNodeOutputStore().snapshotOutputs(); - const navigation = useSubgraphNavigationStore().exportState(); + offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]] + } + this.nodeOutputs = useNodeOutputStore().snapshotOutputs() + const navigation = useSubgraphNavigationStore().exportState() // Always store the navigation state, even if empty (root level) - this.subgraphState = { navigation }; + this.subgraphState = { navigation } } /** @@ -117,11 +117,11 @@ export class ChangeTracker { */ deactivate() { if (!isActiveTracker(this)) { - reportInactiveTrackerCall("deactivate", this.workflow.path); - return; + reportInactiveTrackerCall('deactivate', this.workflow.path) + return } - if (!this._restoringState) this.captureCanvasState(); - this.store(); + if (!this._restoringState) this.captureCanvasState() + this.store() } /** @@ -132,46 +132,46 @@ export class ChangeTracker { * @internal Not part of the public extension API. */ prepareForSave() { - if (isActiveTracker(this)) this.captureCanvasState(); + if (isActiveTracker(this)) this.captureCanvasState() } restore() { if (this.ds) { - app.canvas.ds.scale = this.ds.scale; - app.canvas.ds.offset = this.ds.offset; + app.canvas.ds.scale = this.ds.scale + app.canvas.ds.offset = this.ds.offset } if (this.nodeOutputs) { - useNodeOutputStore().restoreOutputs(this.nodeOutputs); + useNodeOutputStore().restoreOutputs(this.nodeOutputs) } if (this.subgraphState) { - const { navigation } = this.subgraphState; - useSubgraphNavigationStore().restoreState(navigation); + const { navigation } = this.subgraphState + useSubgraphNavigationStore().restoreState(navigation) - const activeId = navigation.at(-1); + const activeId = navigation.at(-1) if (activeId) { // Navigate to the saved subgraph - const subgraph = app.rootGraph.subgraphs.get(activeId); + const subgraph = app.rootGraph.subgraphs.get(activeId) if (subgraph) { - app.canvas.setGraph(subgraph); + app.canvas.setGraph(subgraph) } } else { // Empty navigation array means root level - app.canvas.setGraph(app.rootGraph); + app.canvas.setGraph(app.rootGraph) } } } updateModified() { - api.dispatchCustomEvent("graphChanged", this.activeState); + api.dispatchCustomEvent('graphChanged', this.activeState) // Get the workflow from the store as ChangeTracker is raw object, i.e. // `this.workflow` is not reactive. - const workflow = useWorkflowStore().getWorkflowByPath(this.workflow.path); + const workflow = useWorkflowStore().getWorkflowByPath(this.workflow.path) if (workflow) { workflow.isModified = !ChangeTracker.graphEqual( this.initialState, - this.activeState, - ); + this.activeState + ) } } @@ -181,302 +181,302 @@ export class ChangeTracker { * Calling this on an inactive tracker would capture the wrong graph. */ captureCanvasState() { - const isUndoRedoing = this._restoringState; - const isInsideChangeTransaction = this.changeCount > 0; + const isUndoRedoing = this._restoringState + const isInsideChangeTransaction = this.changeCount > 0 if ( !app.graph || isInsideChangeTransaction || isUndoRedoing || ChangeTracker.isLoadingGraph ) - return; + return if (!isActiveTracker(this)) { - reportInactiveTrackerCall("captureCanvasState", this.workflow.path); - return; + reportInactiveTrackerCall('captureCanvasState', this.workflow.path) + return } - const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON; + const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON if (!this.activeState) { - this.activeState = currentState; - return; + this.activeState = currentState + return } if (!ChangeTracker.graphEqual(this.activeState, currentState)) { - this.undoQueue.push(this.activeState); + this.undoQueue.push(this.activeState) if (this.undoQueue.length > ChangeTracker.MAX_HISTORY) { - this.undoQueue.shift(); + this.undoQueue.shift() } - this.activeState = currentState; - this.redoQueue.length = 0; - this.updateModified(); + this.activeState = currentState + this.redoQueue.length = 0 + this.updateModified() } } /** @deprecated Use {@link captureCanvasState} instead. */ checkState() { if (!ChangeTracker._checkStateWarned) { - ChangeTracker._checkStateWarned = true; + ChangeTracker._checkStateWarned = true console.warn( - "checkState() is deprecated — use captureCanvasState() instead.", - ); + 'checkState() is deprecated — use captureCanvasState() instead.' + ) } - this.captureCanvasState(); + this.captureCanvasState() } - private static _checkStateWarned = false; + private static _checkStateWarned = false async updateState(source: ComfyWorkflowJSON[], target: ComfyWorkflowJSON[]) { - const prevState = source.pop(); + const prevState = source.pop() if (prevState) { - target.push(this.activeState); - this._restoringState = true; + target.push(this.activeState) + this._restoringState = true try { await app.loadGraphData(prevState, false, false, this.workflow, { checkForRerouteMigration: false, - silentAssetErrors: true, - }); - this.activeState = prevState; - this.updateModified(); + silentAssetErrors: true + }) + this.activeState = prevState + this.updateModified() } finally { - this._restoringState = false; + this._restoringState = false } } } async undo() { - await this.updateState(this.undoQueue, this.redoQueue); + await this.updateState(this.undoQueue, this.redoQueue) } async redo() { - await this.updateState(this.redoQueue, this.undoQueue); + await this.updateState(this.redoQueue, this.undoQueue) } async undoRedo(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && !e.altKey) { - const key = e.key.toUpperCase(); + const key = e.key.toUpperCase() // Redo: Ctrl + Y, or Ctrl + Shift + Z - if ((key === "Y" && !e.shiftKey) || (key == "Z" && e.shiftKey)) { - await this.redo(); - return true; - } else if (key === "Z" && !e.shiftKey) { - await this.undo(); - return true; + if ((key === 'Y' && !e.shiftKey) || (key == 'Z' && e.shiftKey)) { + await this.redo() + return true + } else if (key === 'Z' && !e.shiftKey) { + await this.undo() + return true } } } beforeChange() { - this.changeCount++; + this.changeCount++ } afterChange() { if (!--this.changeCount) { - this.captureCanvasState(); + this.captureCanvasState() } } static init() { const getCurrentChangeTracker = () => - useWorkflowStore().activeWorkflow?.changeTracker; - const captureState = () => getCurrentChangeTracker()?.captureCanvasState(); + useWorkflowStore().activeWorkflow?.changeTracker + const captureState = () => getCurrentChangeTracker()?.captureCanvasState() - let keyIgnored = false; + let keyIgnored = false window.addEventListener( - "keydown", + 'keydown', (e: KeyboardEvent) => { // Do not trigger on repeat events (Holding down a key) // This can happen when user is holding down "Space" to pan the canvas. - if (e.repeat) return; + if (e.repeat) return // If the mask editor is opened, we don't want to trigger on key events - const comfyApp = app.constructor as typeof ComfyApp; - if (comfyApp.maskeditor_is_opended?.()) return; + const comfyApp = app.constructor as typeof ComfyApp + if (comfyApp.maskeditor_is_opended?.()) return - const activeEl = document.activeElement; + const activeEl = document.activeElement requestAnimationFrame(async () => { - let bindInputEl: Element | null = null; + let bindInputEl: Element | null = null // If we are auto queue in change mode then we do want to trigger on inputs - if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { + if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === 'instant') { if ( - activeEl?.tagName === "INPUT" || - (activeEl && "type" in activeEl && activeEl.type === "textarea") + activeEl?.tagName === 'INPUT' || + (activeEl && 'type' in activeEl && activeEl.type === 'textarea') ) { // Ignore events on inputs, they have their native history - return; + return } - bindInputEl = activeEl; + bindInputEl = activeEl } keyIgnored = - e.key === "Control" || - e.key === "Shift" || - e.key === "Alt" || - e.key === "Meta"; - if (keyIgnored) return; + e.key === 'Control' || + e.key === 'Shift' || + e.key === 'Alt' || + e.key === 'Meta' + if (keyIgnored) return - const changeTracker = getCurrentChangeTracker(); - if (!changeTracker) return; + const changeTracker = getCurrentChangeTracker() + if (!changeTracker) return // Check if this is a ctrl+z ctrl+y - if (await changeTracker.undoRedo(e)) return; + if (await changeTracker.undoRedo(e)) return // If our active element is some type of input then handle changes after they're done - if (ChangeTracker.bindInput(bindInputEl)) return; - changeTracker.captureCanvasState(); - }); + if (ChangeTracker.bindInput(bindInputEl)) return + changeTracker.captureCanvasState() + }) }, - true, - ); + true + ) - window.addEventListener("keyup", () => { + window.addEventListener('keyup', () => { if (keyIgnored) { - keyIgnored = false; - captureState(); + keyIgnored = false + captureState() } - }); + }) // Handle clicking DOM elements (e.g. widgets) - window.addEventListener("mouseup", () => { - captureState(); - }); + window.addEventListener('mouseup', () => { + captureState() + }) // Handle prompt queue event for dynamic widget changes - api.addEventListener("promptQueued", () => { - captureState(); - }); + api.addEventListener('promptQueued', () => { + captureState() + }) - api.addEventListener("graphCleared", () => { - captureState(); - }); + api.addEventListener('graphCleared', () => { + captureState() + }) // Handle litegraph clicks - const processMouseUp = LGraphCanvas.prototype.processMouseUp; + const processMouseUp = LGraphCanvas.prototype.processMouseUp LGraphCanvas.prototype.processMouseUp = function (e) { - const v = processMouseUp.apply(this, [e]); - captureState(); - return v; - }; + const v = processMouseUp.apply(this, [e]) + captureState() + return v + } // Handle litegraph dialog popup for number/string widgets - const prompt = LGraphCanvas.prototype.prompt; + const prompt = LGraphCanvas.prototype.prompt LGraphCanvas.prototype.prompt = function ( title: string, value: string | number, callback: (v: string) => void, - event: CanvasPointerEvent, + event: CanvasPointerEvent ) { const extendedCallback = (v: string) => { - callback(v); - captureState(); - }; - return prompt.apply(this, [title, value, extendedCallback, event]); - }; + callback(v) + captureState() + } + return prompt.apply(this, [title, value, extendedCallback, event]) + } // Handle litegraph context menu for COMBO widgets - const close = LiteGraph.ContextMenu.prototype.close; + const close = LiteGraph.ContextMenu.prototype.close LiteGraph.ContextMenu.prototype.close = function (e: MouseEvent) { - const v = close.apply(this, [e]); - captureState(); - return v; - }; + const v = close.apply(this, [e]) + captureState() + return v + } // Handle multiple commands as a single transaction - document.addEventListener("litegraph:canvas", (e: Event) => { - const detail = (e as CustomEvent).detail; - if (detail.subType === "before-change") { - getCurrentChangeTracker()?.beforeChange(); - } else if (detail.subType === "after-change") { - getCurrentChangeTracker()?.afterChange(); + document.addEventListener('litegraph:canvas', (e: Event) => { + const detail = (e as CustomEvent).detail + if (detail.subType === 'before-change') { + getCurrentChangeTracker()?.beforeChange() + } else if (detail.subType === 'after-change') { + getCurrentChangeTracker()?.afterChange() } - }); + }) // Store node outputs - api.addEventListener("executed", (e: CustomEvent) => { - const detail = e.detail; + api.addEventListener('executed', (e: CustomEvent) => { + const detail = e.detail const workflow = - useExecutionStore().queuedJobs[detail.prompt_id]?.workflow; - const changeTracker = workflow?.changeTracker; - if (!changeTracker) return; - changeTracker.nodeOutputs ??= {}; - const nodeOutputs = changeTracker.nodeOutputs; - const output = nodeOutputs[detail.node]; + useExecutionStore().queuedJobs[detail.prompt_id]?.workflow + const changeTracker = workflow?.changeTracker + if (!changeTracker) return + changeTracker.nodeOutputs ??= {} + const nodeOutputs = changeTracker.nodeOutputs + const output = nodeOutputs[detail.node] if (detail.merge && output) { for (const k in detail.output ?? {}) { - const v = output[k]; + const v = output[k] if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); + output[k] = v.concat(detail.output[k]) } else { - output[k] = detail.output[k]; + output[k] = detail.output[k] } } } else { - nodeOutputs[detail.node] = detail.output; + nodeOutputs[detail.node] = detail.output } - }); + }) } static bindInput(activeEl: Element | null): boolean { if ( !activeEl || - activeEl.tagName === "CANVAS" || - activeEl.tagName === "BODY" + activeEl.tagName === 'CANVAS' || + activeEl.tagName === 'BODY' ) { - return false; + return false } - for (const evt of ["change", "input", "blur"]) { - const htmlElement = activeEl as HTMLElement; + for (const evt of ['change', 'input', 'blur']) { + const htmlElement = activeEl as HTMLElement if (`on${evt}` in htmlElement) { const listener = () => { - useWorkflowStore().activeWorkflow?.changeTracker?.captureCanvasState?.(); - htmlElement.removeEventListener(evt, listener); - }; - htmlElement.addEventListener(evt, listener); - return true; + useWorkflowStore().activeWorkflow?.changeTracker?.captureCanvasState?.() + htmlElement.removeEventListener(evt, listener) + } + htmlElement.addEventListener(evt, listener) + return true } } - return false; + return false } static graphEqual(a: ComfyWorkflowJSON, b: ComfyWorkflowJSON) { - if (a === b) return true; + if (a === b) return true - if (typeof a == "object" && a && typeof b == "object" && b) { + if (typeof a == 'object' && a && typeof b == 'object' && b) { // Compare nodes ignoring order if ( !_.isEqualWith(a.nodes, b.nodes, (arrA, arrB) => { if (Array.isArray(arrA) && Array.isArray(arrB)) { - return _.isEqual(new Set(arrA), new Set(arrB)); + return _.isEqual(new Set(arrA), new Set(arrB)) } }) ) { - return false; + return false } // Compare extra properties ignoring ds if ( - !_.isEqual(_.omit(a.extra ?? {}, ["ds"]), _.omit(b.extra ?? {}, ["ds"])) + !_.isEqual(_.omit(a.extra ?? {}, ['ds']), _.omit(b.extra ?? {}, ['ds'])) ) - return false; + return false // Compare other properties normally for (const key of [ - "links", - "floatingLinks", - "reroutes", - "groups", - "definitions", - "subgraphs", + 'links', + 'floatingLinks', + 'reroutes', + 'groups', + 'definitions', + 'subgraphs' ]) { if (!_.isEqual(a[key], b[key])) { - return false; + return false } } - return true; + return true } - return false; + return false } }