Skip to content

[Feature]: Scope vueNodesMap per LogicFlow instance to support nested scenarios #2377

@EralChen

Description

@EralChen

背景&目的

Motivation

Currently, vueNodesMap in registry.ts is a module-level global object, while register() accepts a per-instance lf parameter. This means lf.register() correctly scopes the Model/View to the specific LogicFlow instance, but vueNodesMap does not — leading to an inconsistency.

// registry.ts (current)
export const vueNodesMap = {} // ← global singleton

export function register (config, lf) {
  vueNodesMap[type] = { component, effect } // ← global overwrite!
  lf.register({ type, view, model }) // ← per-instance (correct)
}

When the same node type is registered for multiple LogicFlow instances (e.g., nested sub-flows), the later register() call overwrites the earlier entry in vueNodesMap.

Problem Scenario

  1. Create an outer LogicFlow instance, register node type A — its Vue component captures context A
  2. Create an inner (nested) LogicFlow instance, register the same type A — its Vue component captures context B, and overwrites vueNodesMap['A']
  3. Delete/destroy the inner LogicFlow instance — context B is now stale/empty
  4. Add a new node of type A to the outer LogicFlow
  5. Result: The outer LogicFlow creates the node using the stale component from the destroyed inner instance (context B), instead of the original component (context A)

Concrete scenario

This happens with a Loop Node that embeds a sub-flow (inner LogicFlow). The loop's sub-flow registers the same node types as the outer flow. When the loop node is deleted from the canvas, all new nodes created afterward use the orphaned inner components — breaking rendering (e.g., missing icons, empty slot data).

Expected Behavior

Each LogicFlow instance should maintain its own vueNodesMap, so nested instances don't interfere with each other.

Proposal

Option A: Scope vueNodesMap per LogicFlow instance (recommended)

const vueNodesMaps = new WeakMap<LogicFlow, Record<string, VueNodeEntry>>()

export function register (config: VueNodeConfig, lf: LogicFlow) {
  const { type, component, effect, view: CustomNodeView, model: CustomNodeModel } = config

  if (!type)
    throw new Error('You should specify type in config')

  if (!vueNodesMaps.has(lf))
    vueNodesMaps.set(lf, {})
  vueNodesMaps.get(lf)![type] = { component, effect }

  lf.register({
    type,
    view: CustomNodeView || VueNodeView,
    model: CustomNodeModel || VueNodeModel,
  })
}

export function getVueNodeConfig (type: string, lf: LogicFlow) {
  return vueNodesMaps.get(lf)?.[type]
}

Using WeakMap ensures entries are automatically garbage-collected when the LogicFlow instance is destroyed.

Option B: Guard against overwrite (minimal change)

export function register (config: VueNodeConfig, lf: LogicFlow) {
  const { type, component, effect } = config

  // Only set if not already registered (first-come wins)
  if (!vueNodesMap[type]) {
    vueNodesMap[type] = { component, effect }
  }

  lf.register({ type, view, model })
}

This is simpler but less flexible — it prevents any update to an existing registration.

Environment

  • @logicflow/vue-node-registry: ^1.0.18
  • @logicflow/core: ^2.0.16

Current Workaround

At the application level, we wrap the inner register() calls with a component that snapshots and restores vueNodesMap after mount:

const PreserveRegistrations = defineComponent({
  setup (_, { slots }) {
    const saved = { ...vueNodesMap }
    onMounted(() => {
      Object.assign(vueNodesMap, saved)
    })
    return () => slots.default?.()
  },
})

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions