Skip to content

fix: a drop during card migration no longer duplicates the card (MS-27523) - #243

Merged
davidnmbond merged 2 commits into
mainfrom
bug/MS-27523-carddeck-concurrent-drop
Oct 14, 2026
Merged

davidnmbond merged 2 commits into
mainfrom
bug/MS-27523-carddeck-concurrent-drop

Conversation

@davidnmbond

Copy link
Copy Markdown
Contributor

Summary

Dropping a card shortly after entering a column could terminate the Blazor Server circuit. Seen in production on a Magic Suite Kanban board (MS-27523); reproduced here on the demo kanban, where the first drop killed the circuit every time.

Cause

PDCardDeckGroup migrates cards between decks on drag-enter. RemoveSelectedCardsAsync awaits the browser part way through (cancelling animations), and the drop's InitiateTransformAsync ran without waiting for it. A drop landing in that gap ran the transformation and refreshed the destination from data that already held the card; the migration then resumed and inserted the card a second time. The next render threw More than one sibling of component 'PDCard' has the same key value, which is an unhandled circuit exception.

Separately, every deck registers its own document-level drop listener, so the transformation ran once per deck (up to eight times on the demo page).

Fix

Migration, drop and end of drag take one SemaphoreSlim. A drop waits for an in-flight migration, and later drops from the other decks find the drag already over and return.

Evidence

  • Two new bUnit tests, both red before the fix (one with the exact duplicate-key exception) and green after: Group_DropDuringTheMigration_PutsTheCardInTheDestinationOnce, Group_DropReachingEveryDeck_TransformsOnce. Full suite: 3801 passed.
  • Real browser (Playwright, Chromium, demo /pdcarddeckgroup): before, circuit terminated on the first drop in 3 of 3 runs and the server logged Unhandled exception in circuit; after, 3 x 40 drags, 0 console errors, 0 server circuit errors, cards back where they started.

Not covered

Timing on a high-latency link. The race window is the JS round trip, so it is wider in production than on localhost; the tests force the interleaving rather than rely on timing.

🤖 Generated with Claude Code

…7523)

PDCardDeckGroup migrated cards between decks on drag-enter, awaiting the browser part way
through (cancelling animations), and ran the drop's transformation and refresh without
waiting for it. A drop landing in that gap refreshed the destination from data that already
held the card, then the migration resumed and inserted it again, so the deck held the same
card twice and the render threw 'More than one sibling ... has the same key value',
terminating the Blazor Server circuit. Every deck also listens for the drop on the document,
so the transformation ran once per deck.

Migration, drop and end of drag now take one lock, so a drop waits for the migration and
later drops find the drag already over. Reproduced in a real browser on the demo kanban
(circuit killed on the first drop) and verified fixed over 3 x 40 drags.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 6 duplication

Metric Results
Complexity 0
Duplication 6

View in Codacy

🟢 Coverage 100.00% diff coverage · +0.00% coverage variation

Metric Results
Coverage variation ✅ +0.00% coverage variation
Diff coverage ✅ 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (8f35684) 16054 16031 99.86%
Head commit (dea0e15) 16056 (+2) 16033 (+2) 99.86% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#243) 2 2 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

AI Reviewer: run a review on demand. To trigger the first review automatically, go to your organization or repository integration settings. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@davidnmbond
davidnmbond merged commit f4b7801 into main Oct 14, 2026
8 checks passed
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