Skip to content

fix: preserve Set/Map order when restoring referential equalities - #357

Open
spokodev wants to merge 1 commit into
flightcontrolhq:mainfrom
spokodev:fix/set-map-referential-equality-order
Open

fix: preserve Set/Map order when restoring referential equalities#357
spokodev wants to merge 1 commit into
flightcontrolhq:mainfrom
spokodev:fix/set-map-referential-equality-order

Conversation

@spokodev

@spokodev spokodev commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

setDeep replaced Set members and Map keys with delete + add, which moves the entry to the end of the container. When more than one referentially equal value lives in the same Set (or is used as a key in the same Map), each replacement shifts the positional indices that the remaining referential-equality annotations rely on, so getNthKey resolves the wrong entry. The result is silent data loss — entries are dropped and others duplicated — with default options, on both serialize/deserialize and stringify/parse.

const a = { tag: 'a' }, b = { tag: 'b' };
const back = SuperJSON.deserialize(SuperJSON.serialize({ set: new Set([a, b]), a, b }));
[...back.set]; // [{tag:'b'}, {tag:'b'}] — 'a' is gone

Rebuild the container in place so every entry keeps its original index. Adds regression tests for the Set and Map-key cases.

Greptile Summary

This PR fixes a silent data-loss bug where setDeep used delete+add (or set+delete) to replace a Set member or Map key, moving the entry to the end of the container and corrupting the positional indices that subsequent referential-equality annotations rely on. The fix rebuilds the Set/Map in-place, preserving original insertion order throughout the restoration walk.

  • src/accessDeep.ts: Both the Set branch and the Map-key branch now snapshot entries into a temporary array, clear the container, then re-insert everything in original order — substituting only the one entry that changed.
  • src/index.test.ts: Two new regression tests are added covering the multi-referential-equal-values-in-Set case and the multi-referential-equal-keys-in-Map case that triggered the original bug.

Confidence Score: 5/5

The change is a targeted, well-reasoned fix to a reproducible data-loss bug in Set/Map referential-equality restoration, with no regressions to other code paths.

Both the Set and Map-key branches now correctly snapshot-then-rebuild the container, preserving insertion order across multiple sequential annotations. The Map value path (which was already order-safe) is untouched. Two focused regression tests confirm the previously broken cases now work. No new error paths or behavioural changes are introduced beyond the bug fix.

No files require special attention — the change is self-contained within src/accessDeep.ts and src/index.test.ts.

Important Files Changed

Filename Overview
src/accessDeep.ts Set and Map-key branches rebuilt to preserve insertion order; logic is correct and handles incremental multi-annotation updates cleanly.
src/index.test.ts Two well-scoped regression tests added for the fixed Set and Map-key ordering cases; assertions cover both insertion order and referential equality.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[setDeep called] --> B{parent type?}
    B -- Array --> C[Direct index assignment]
    B -- PlainObject --> D[Direct key assignment]
    B -- Set --> E[oldValue = getNthKey]
    B -- Map --> F{annotation type?}

    E --> G[newValue = mapper oldValue]
    G --> H{oldValue !== newValue?}
    H -- No --> I[no-op]
    H -- Yes --> J[Snapshot to array\nReplace at index\nClear + re-add all]
    J --> K[Set order preserved]

    F -- key --> L[newKey = mapper keyToRow]
    F -- value --> M[parent.set in-place\norder unchanged]

    L --> N{newKey !== keyToRow?}
    N -- No --> O[no-op]
    N -- Yes --> P[Snapshot entries\nClear\nRe-insert substituting old key]
    P --> Q[Map order preserved]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[setDeep called] --> B{parent type?}
    B -- Array --> C[Direct index assignment]
    B -- PlainObject --> D[Direct key assignment]
    B -- Set --> E[oldValue = getNthKey]
    B -- Map --> F{annotation type?}

    E --> G[newValue = mapper oldValue]
    G --> H{oldValue !== newValue?}
    H -- No --> I[no-op]
    H -- Yes --> J[Snapshot to array\nReplace at index\nClear + re-add all]
    J --> K[Set order preserved]

    F -- key --> L[newKey = mapper keyToRow]
    F -- value --> M[parent.set in-place\norder unchanged]

    L --> N{newKey !== keyToRow?}
    N -- No --> O[no-op]
    N -- Yes --> P[Snapshot entries\nClear\nRe-insert substituting old key]
    P --> Q[Map order preserved]
Loading

Reviews (1): Last reviewed commit: "fix: preserve Set/Map order when restori..." | Re-trigger Greptile

setDeep replaced Set members and Map keys with delete + add, which moves
the entry to the end of the container. When more than one referentially
equal value lives in the same Set (or is used as a key in the same Map),
each replacement shifted the positional indices that the remaining
referential-equality annotations rely on, so getNthKey resolved the wrong
entry. The result was silent data loss: entries were dropped and others
duplicated.

Rebuild the container in place instead, keeping every entry at its
original index.
@spokodev
spokodev requested a review from Skn0tt as a code owner July 8, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant