fix: Vue mode socket map data not cleaned up on dynamic input changes (#8469)

## Summary

Fixes a bug where socket map data was not properly removed when sockets
are dynamically added/removed via DynamicCombo widgets in Vue mode
(Nodes 2.0).

## Problem

When DynamicCombo widgets (e.g., `should_remesh` on Meshy nodes) change
their selection, inputs are dynamically added/removed. The Vue `v-for`
loop in `NodeSlots.vue` was using array index as the `:key`, causing Vue
to **reuse** slot components instead of properly unmounting them.

This led to:
- Socket map entries leaking (never cleaned up)
- Socket positions becoming desynced
- Stale cached offset data

## Solution

1. **Use slot `name` as Vue key** instead of array index in
`NodeSlots.vue`
   - Slot names are unique per node (enforced by ComfyUI backend)
- When a slot is removed, Vue sees the key disappear and properly
unmounts the component
- `onUnmounted` cleanup in `useSlotElementTracking` now runs correctly

2. **Add defensive cleanup** in `useSlotElementTracking.ts`
- Before registering a new slot, check if a stale entry exists with the
same key
   - Clean up stale entry to handle any edge cases

## Related

- Fixes COM-12970
- Related to #7837 (fixed LiteGraph version of this bug, but not Vue
mode)

## Testing

- Quality checks pass (typecheck, lint, format)
- Manual testing with DynamicCombo nodes (Meshy, nodes_logic)
recommended

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8469-fix-Vue-mode-socket-map-data-not-cleaned-up-on-dynamic-input-changes-2f86d73d365081e599eadca4f15e6b6e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Christian Byrne
2026-01-29 19:47:43 -08:00
committed by GitHub
parent d784d4982b
commit ee4a205d32
2 changed files with 10 additions and 2 deletions

View File

@@ -9,7 +9,7 @@
>
<InputSlot
v-for="(input, index) in filteredInputs"
:key="`input-${index}`"
:key="`input-${input.name}`"
:slot-data="input"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
@@ -23,7 +23,7 @@
>
<OutputSlot
v-for="(output, index) in nodeData.outputs"
:key="`output-${index}`"
:key="`output-${output.name}`"
:slot-data="output"
:node-type="nodeData?.type || ''"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"

View File

@@ -183,6 +183,14 @@ export function useSlotElementTracking(options: {
// Register slot
const slotKey = getSlotKey(nodeId, index, type === 'input')
// Defensive cleanup: remove stale entry if it exists with different element
// This handles edge cases where Vue component reuse prevents proper unmount
const existingEntry = node.slots.get(slotKey)
if (existingEntry && existingEntry.el !== el) {
delete existingEntry.el.dataset.slotKey
layoutStore.deleteSlotLayout(slotKey)
}
el.dataset.slotKey = slotKey
node.slots.set(slotKey, { el, index, type })