fix(contacts): make concurrent first-contact upserts safe - #469
Open
vladbisceanu wants to merge 3 commits into
Open
fix(contacts): make concurrent first-contact upserts safe#469vladbisceanu wants to merge 3 commits into
vladbisceanu wants to merge 3 commits into
Conversation
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Concurrent first-seen tracking requests can all observe a missing contact, then race on
ContactService.upsert. The unique(projectId, email)constraint correctly selects one row, but every losingcontact.createcurrently surfaces Prisma P2002 and turns an otherwise valid track request into a 500.This change treats P2002 as the election result: losers read and continue with the elected contact. They never replay their stale data over the winning row. An explicit concurrent unsubscribe is preserved with a compare-and-set update, and subscription events are emitted only by the request that actually changes the stored state.
The service regression suite forces eight first-seen upserts through the same missing-row observation, covers both winner orderings, and verifies that stale concurrent unsubscribe observers emit only one state-change event. Existing normalization and cross-project isolation coverage remains in the same suite.
Verification:
yarn vitest run apps/api/src/services/__tests__/ContactService.test.ts— 63 passedyarn eslint --config apps/api/eslint.config.mjs apps/api/src/services/ContactService.ts apps/api/src/services/__tests__/ContactService.test.tsyarn workspace api buildThis is the portable counterpart to vladbisceanu#10.