fix(widgets): isolate autocomplete text cleanup

This commit is contained in:
Will Miao
2026-04-23 20:07:11 +08:00
parent c6e5467907
commit b31fae4e51
5 changed files with 92 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ import LoraRandomizerWidget from '@/components/LoraRandomizerWidget.vue'
import LoraCyclerWidget from '@/components/LoraCyclerWidget.vue' import LoraCyclerWidget from '@/components/LoraCyclerWidget.vue'
import JsonDisplayWidget from '@/components/JsonDisplayWidget.vue' import JsonDisplayWidget from '@/components/JsonDisplayWidget.vue'
import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue' import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue'
import { createVueWidgetCleanup } from './vue-widget-cleanup'
import type { LoraPoolConfig, RandomizerConfig, CyclerConfig } from './composables/types' import type { LoraPoolConfig, RandomizerConfig, CyclerConfig } from './composables/types'
import { import {
setupModeChangeHandler, setupModeChangeHandler,
@@ -66,6 +67,12 @@ function forwardMiddleMouseToCanvas(container: HTMLElement) {
} }
const vueApps = new Map<number, VueApp>() const vueApps = new Map<number, VueApp>()
let autocompleteTextWidgetInstanceId = 0
export function createAutocompleteTextWidgetInstanceId() {
autocompleteTextWidgetInstanceId += 1
return autocompleteTextWidgetInstanceId
}
// Cache for dynamically loaded addLorasWidget module // Cache for dynamically loaded addLorasWidget module
let addLorasWidgetCache: any = null let addLorasWidgetCache: any = null
@@ -562,8 +569,9 @@ function createAutocompleteTextWidgetFactory(
inputOptions: { placeholder?: string } = {} inputOptions: { placeholder?: string } = {}
) { ) {
const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}` const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}`
const instanceId = createAutocompleteTextWidgetInstanceId()
const container = document.createElement('div') const container = document.createElement('div')
container.id = `autocomplete-text-widget-${node.id}-${widgetName}` container.id = `autocomplete-text-widget-${instanceId}`
container.style.width = '100%' container.style.width = '100%'
container.style.height = '100%' container.style.height = '100%'
container.style.display = 'flex' container.style.display = 'flex'
@@ -644,17 +652,12 @@ function createAutocompleteTextWidgetFactory(
}) })
vueApp.mount(container) vueApp.mount(container)
// Use a unique key combining node.id and widget name to avoid collisions const appKey = instanceId
const appKey = node.id * 100000 + widgetName.charCodeAt(0)
vueApps.set(appKey, vueApp) vueApps.set(appKey, vueApp)
widget.onRemove = () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
const vueApp = vueApps.get(appKey) vueApps.delete(appKey)
if (vueApp) { })
vueApp.unmount()
vueApps.delete(appKey)
}
}
return { widget } return { widget }
} }

View File

@@ -0,0 +1,15 @@
import type { App as VueApp } from 'vue'
export function createVueWidgetCleanup(vueApp: VueApp, onCleanup?: () => void) {
let didUnmount = false
return () => {
if (didUnmount) {
return
}
vueApp.unmount()
didUnmount = true
onCleanup?.()
}
}

View File

@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest'
import { createVueWidgetCleanup } from '@/vue-widget-cleanup'
describe('createVueWidgetCleanup', () => {
it('cleans up only the Vue app bound to the widget remove handler', () => {
const firstCleanup = vi.fn()
const secondCleanup = vi.fn()
const firstApp = { unmount: vi.fn() }
const secondApp = { unmount: vi.fn() }
const removeFirst = createVueWidgetCleanup(firstApp as any, firstCleanup)
const removeSecond = createVueWidgetCleanup(secondApp as any, secondCleanup)
removeFirst()
expect(firstApp.unmount).toHaveBeenCalledTimes(1)
expect(firstCleanup).toHaveBeenCalledTimes(1)
expect(secondApp.unmount).not.toHaveBeenCalled()
expect(secondCleanup).not.toHaveBeenCalled()
removeSecond()
expect(secondApp.unmount).toHaveBeenCalledTimes(1)
expect(secondCleanup).toHaveBeenCalledTimes(1)
})
it('is idempotent when ComfyUI calls the remove handler more than once', () => {
const cleanup = vi.fn()
const app = { unmount: vi.fn() }
const remove = createVueWidgetCleanup(app as any, cleanup)
remove()
remove()
expect(app.unmount).toHaveBeenCalledTimes(1)
expect(cleanup).toHaveBeenCalledTimes(1)
})
})

View File

@@ -14933,6 +14933,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-76ce0f19"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-76ce0f19"]]);
function createVueWidgetCleanup(vueApp, onCleanup) {
let didUnmount = false;
return () => {
if (didUnmount) {
return;
}
vueApp.unmount();
didUnmount = true;
onCleanup == null ? void 0 : onCleanup();
};
}
const LORA_PROVIDER_NODE_TYPES$1 = [ const LORA_PROVIDER_NODE_TYPES$1 = [
"Lora Stacker (LoraManager)", "Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)", "Lora Randomizer (LoraManager)",
@@ -15274,6 +15285,11 @@ function forwardMiddleMouseToCanvas(container) {
}); });
} }
const vueApps = /* @__PURE__ */ new Map(); const vueApps = /* @__PURE__ */ new Map();
let autocompleteTextWidgetInstanceId = 0;
function createAutocompleteTextWidgetInstanceId() {
autocompleteTextWidgetInstanceId += 1;
return autocompleteTextWidgetInstanceId;
}
let addLorasWidgetCache = null; let addLorasWidgetCache = null;
function createLoraPoolWidget(node) { function createLoraPoolWidget(node) {
const container = document.createElement("div"); const container = document.createElement("div");
@@ -15653,8 +15669,9 @@ if ((_a = app$1.ui) == null ? void 0 : _a.settings) {
function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputOptions = {}) { function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputOptions = {}) {
var _a2, _b, _c; var _a2, _b, _c;
const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}`; const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}`;
const instanceId = createAutocompleteTextWidgetInstanceId();
const container = document.createElement("div"); const container = document.createElement("div");
container.id = `autocomplete-text-widget-${node.id}-${widgetName}`; container.id = `autocomplete-text-widget-${instanceId}`;
container.style.width = "100%"; container.style.width = "100%";
container.style.height = "100%"; container.style.height = "100%";
container.style.display = "flex"; container.style.display = "flex";
@@ -15721,15 +15738,11 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
ripple: false ripple: false
}); });
vueApp.mount(container); vueApp.mount(container);
const appKey = node.id * 1e5 + widgetName.charCodeAt(0); const appKey = instanceId;
vueApps.set(appKey, vueApp); vueApps.set(appKey, vueApp);
widget.onRemove = () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
const vueApp2 = vueApps.get(appKey); vueApps.delete(appKey);
if (vueApp2) { });
vueApp2.unmount();
vueApps.delete(appKey);
}
};
return { widget }; return { widget };
} }
app$1.registerExtension({ app$1.registerExtension({
@@ -15834,4 +15847,7 @@ app$1.registerExtension({
} }
} }
}); });
export {
createAutocompleteTextWidgetInstanceId
};
//# sourceMappingURL=lora-manager-widgets.js.map //# sourceMappingURL=lora-manager-widgets.js.map

File diff suppressed because one or more lines are too long