Skip to content

Adding a component from the rail can mint a duplicate id, and the next reload silently discards the design #46

Description

@yashksaini-coder

What

Dragging a component in from the rail can mint an id a node in the design already has. Nothing
notices at the time, the design keeps working for the rest of the session, and then the next
reload silently replaces it with the Single Server preset.

It is the autosave that breaks, so there is no error and nothing to undo. You come back to what
looks like a fresh app and the work is gone.

Reproducing it without a reload

makeNode and the paste path disagree about ids inside a single session:

Ctrl+D mints      : db-1
palette add mints : db-1
same id?          : true

topology ids       : client,api,db,db-1,db-1
isTopology accepts : false

Ctrl+D goes through freshId, which scans the topology, so it correctly picks db-1. A palette
drag goes through makeNode, which does not scan, and the module counter is independently
sitting at 0, so it picks db-1 as well.

Reproducing it in the app

Default preset, palette drag each time:

1. fresh boot         : client,api,db
2. after palette add  : client,api,db,cache-1   saved: client,api,db,cache-1
3. after reload       : client,api,db,cache-1
4. add cache again    : client,api,db,cache-1,cache-1   duplicate: true
   saved to storage   : client,api,db,cache-1,cache-1
5. after 2nd reload   : client,api,db

Step 5 is the bug. Both caches are gone and nothing was said. React logs
Encountered two children with the same key at step 4, which is the only outward sign.

Cause

makeNode (src/sim/presets.ts:653) mints from a module-global counter:

let nodeCounter = 0;
export function makeNode(kind, x, y, label?) {
  nodeCounter += 1;
  return { id: `${kind}-${nodeCounter}`, ... };
}

The counter resets to 0 on every page load and never looks at the topology.
handleAddNode (src/App.tsx:1479) appends the result unconditionally. Every add path that
routes through makeNode has this shape; the rail is where I hit it.

The other two id paths already defend against exactly this, and their comments say why:

  • freshId (src/clipboard.ts:189): "Scanned against the live topology rather than a
    counter, because ids on the clipboard may come from another tab whose counter this session
    never saw, and a collision would silently merge two different nodes."
  • freshAnnId (src/App.tsx:1215): "scanned against the live list because a restored
    session's ids predate this tab's counters."

Nodes added from the rail are the one path that skips the scan those comments exist to justify.

Why it costs the whole design rather than one node

isTopology rejects the entire topology on any duplicate id
(src/clipboard.ts, if (ids.has(n.id)) return false), and loadSession
(src/App.tsx:507) turns any rejection into a silent fall back to PRESETS[0].

That check is also the door for every other way a design leaves the app, so once a design has
picked up a duplicate id all four are unreadable:

  • the saved session, src/App.tsx:507
  • named saved designs, src/savedDesigns.ts:77, which drops the row
  • share links, src/share.ts:369, which returns BAD_LINK
  • exported .breakscale files, src/designFile.ts:185

So this is not only "a reload loses it". Save, share or export after hitting it and those
artefacts are already invalid.

Fix

freshId in src/clipboard.ts is already the right function and already takes a used set. It
is module-private today, but App.tsx:42 imports three other things from that module and
clipboard.ts exports no components, so exporting it costs nothing.

handleAddNode then mints against the live topology the way freshAnnId does, and every add
path that shares it gets the fix for free. makeNode's counter can stay for the presets, which
build a topology from nothing and cannot collide.

I have the approach ready currently testing and implementing it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions