Skip to content

fix(platform-links): prevent duplicate displayOrder via unique constraint and retry#2

Closed
Ridanshi wants to merge 3 commits into
Harxhit:mainfrom
Ridanshi:fix/platform-link-display-order-race
Closed

fix(platform-links): prevent duplicate displayOrder via unique constraint and retry#2
Ridanshi wants to merge 3 commits into
Harxhit:mainfrom
Ridanshi:fix/platform-link-display-order-race

Conversation

@Ridanshi

@Ridanshi Ridanshi commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Fixes Dev-Card#485 — concurrent platform-link creation corrupts link ordering by assigning duplicate displayOrder values.

Root cause: createPlatformLink read max(displayOrder) and inserted in two separate operations with no transaction and no DB constraint. Two concurrent requests reading the same max would both attempt to insert with the same displayOrder.

Changes:

  • schema.prisma: add @@unique([userId, displayOrder]) to PlatformLink; migration in 20260612000000_platform_link_unique_display_order/
  • profileService.createPlatformLink: retry loop (max 5 attempts) re-reads max and retries on P2002 unique constraint violations — the DB constraint catches races the application cannot prevent at READ COMMITTED isolation
  • profileService.reorderLinks: switch from batch array form to interactive $transaction callback with two-phase updates (temp offset → final values) so swapping adjacent positions doesn't trigger the new unique constraint mid-transaction
  • profile-cache.test.ts: update $transaction mock to handle both array and callback forms
  • platform-link-ordering.test.ts: new test file covering display order assignment, P2002 retry (single, exhausted, non-P2002), concurrent request simulation, two-phase reorder verification, ordering integrity (sequential, delete-then-create), and regression

Test Plan

  • npx vitest run src/__tests__/platform-link-ordering.test.ts — 27 tests pass
  • npx vitest run src/__tests__/profile-cache.test.ts — 18 tests pass (all existing cache tests preserved)
  • npx vitest run src/__tests__/profiles.test.ts — 8 tests pass
  • npx eslint src/services/profileService.ts src/__tests__/platform-link-ordering.test.ts — no errors
  • npx tsc --noEmit — no type errors

Summary by cubic

Fixes duplicate platform-link displayOrder under concurrency and keeps public profiles fresh. Adds GitHub autodiscovery with cached suggestions for quick link setup.

  • Bug Fixes

    • Add unique constraint on (userId, displayOrder) and retry createPlatformLink on P2002 (up to 5 attempts).
    • Rework reorder to a two-phase $transaction (temp offset → final) to avoid unique conflicts.
    • Invalidate the public profile Redis cache after create, update, delete, and reorder.
  • Migration

    • Apply DB migration adding the unique index on (user_id, display_order).
    • Ensure no existing duplicates per user before migrating, or resolve them first.

Written for commit b9591d0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Introduced profile caching for improved performance and faster data retrieval.
    • Automatic display order management for platform links with built-in duplicate prevention.
  • Bug Fixes

    • Improved handling of concurrent platform link creation operations.
    • Enhanced data consistency through automatic cache invalidation after profile and link modifications.

…ard#506)

* feat: add GitHub platform autodiscovery

* fix: resolve lint and type issues in autodiscovery

* fix: restore reply parameter in github autodiscovery route

* fix: resolve unused reply lint issue
@github-actions

Copy link
Copy Markdown

Hi @Ridanshi,

Thanks for opening this pull request.

This PR has been automatically classified based on the files modified.

Applied Labels

  • backend

Primary Review Area

  • backend

Reviewer

@Harxhit has been identified as the primary reviewer for this pull request.

If you have any questions regarding the affected area or implementation details, feel free to reach out to the assigned reviewer.

Thank you for your contribution!

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements per-user display-order uniqueness for platform links, adds retry-based conflict resolution and a two-phase reordering algorithm, integrates Redis cache invalidation across profile mutations, and provides comprehensive test coverage for both display-ordering and cache behaviors.

Changes

Platform Link Display-Order Enforcement and Cache Invalidation

Layer / File(s) Summary
Database Schema Constraint
apps/backend/prisma/migrations/20260612000000_platform_link_unique_display_order/migration.sql, apps/backend/prisma/schema.prisma
Unique index on platform_links(user_id, display_order) added at both schema and migration levels to prevent multiple links per user from sharing the same display order.
Display Order Allocation with Retry
apps/backend/src/services/profileService.ts
createPlatformLink now allocates displayOrder as 0 for the first link and max+1 for subsequent links, with a retry loop (max 5 attempts) to handle P2002 unique-constraint conflicts, and invalidates profile cache after successful creation.
Two-Phase Reordering Algorithm
apps/backend/src/services/profileService.ts
reorderLinks replaces single-phase transaction with a two-phase updateMany using a large temporary offset to prevent conflicts during concurrent swaps, then applies final positions and invalidates cache.
Profile Cache Invalidation
apps/backend/src/services/profileService.ts
Introduces profileCacheKey(username) helper and invalidateProfileCacheForUser(app, userId) function, integrating cache clearing into getOwnProfile typing, updateProfile, updatePlatformLink, and deletePlatformLink.
Type Safety and Route Handlers
apps/backend/src/services/cardService.ts, apps/backend/src/routes/cards.ts
Tightens internal card/link types to use concrete PlatformLink instead of unknown, and updates DELETE handler to manage outcomes via thrown exceptions rather than return-value checking.
Display Order Integration Tests
apps/backend/src/__tests__/platform-link-ordering.test.ts
Comprehensive test suite covering display-order assignment, P2002 retry behavior (single success, exhaustion, non-P2002 pass-through), concurrency resolution, two-phase reorder algorithm with temporary offsets, ordering integrity through sequential operations, and regression assertions.
Profile Cache Integration Tests
apps/backend/src/__tests__/profile-cache.test.ts
Full test suite verifying cache hits/misses with header assertions, cache invalidation on link mutations (with failure scenarios), cache repopulation, multi-mutation consistency, cache-key format matching, and non-fatal Redis error handling.
Test Setup Updates
apps/backend/src/__tests__/cards.test.ts
Fastify setup simplified by removing FastifyRequest import, loosening authenticate request type to any, and casting mockPrisma as PrismaClient.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

A rabbit hops through ordered links so neat,
With cache that blinks when profiles meet,
Two phases swirl where conflicts hide,
And tests ensure no order's denied. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately captures the main fix: preventing duplicate displayOrder values via a unique constraint and retry mechanism, which is the primary focus of the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering summary, root cause analysis, specific changes, and a test plan with passing results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

CI — Checks Failed

Backend — FAIL

Check Result
Lint FAIL
Test FAIL
Typecheck PASS

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Check -
Build -

Last updated: Fri, 12 Jun 2026 13:24:21 GMT

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/backend/src/routes/cards.ts (1)

1-3: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix import order to comply with ESLint import-x/order rule.

Services imports must come before utils imports. Line 3 (cardService) should be moved before Line 1 (error.util).

This is a blocking CI failure per the pipeline log.

📦 Proposed fix for import order
-import { handleDbError } from '../utils/error.util.js';
-import { createCardSchema, updateCardSchema } from '../utils/validators.js';
 import * as cardService from '../services/cardService'
+import { handleDbError } from '../utils/error.util.js';
+import { createCardSchema, updateCardSchema } from '../utils/validators.js';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/cards.ts` around lines 1 - 3, The import order
violates ESLint import-x/order: move the service import to come before utility
imports; specifically, place "import * as cardService from
'../services/cardService'" above the imports for "handleDbError" and
"createCardSchema, updateCardSchema" so cardService is imported before
error.util.js and validators.js, preserving existing specifiers and paths.

Source: Pipeline failures

🧹 Nitpick comments (3)
apps/backend/src/services/profileService.ts (1)

131-131: 💤 Low value

Dead code: this line is unreachable.

The loop always exits via return on success (line 123) or throw on error (line 128). On the final attempt, if a P2002 occurs, attempt < MAX_RETRIES - 1 is false, so the original error is thrown rather than reaching this line.

♻️ Remove dead code
       throw err;
     }
   }
-  throw new Error('Failed to allocate display order after max retries');
+  // Unreachable: loop always exits via return or throw
+  throw new Error('Unreachable');
 }

Or simply remove the line since TypeScript will infer the return type correctly without it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/services/profileService.ts` at line 131, Remove the
unreachable final throw statement that reads "Failed to allocate display order
after max retries" in the display-order allocation loop (the throw at the end of
the function that contains the P2002 retry logic, e.g., in the
allocateDisplayOrder or equivalent method in profileService.ts); the loop
already returns on success and rethrows on the last retry, so delete that dead
throw, ensure the function signature/type still compiles, and run TypeScript
build/tests to confirm no regressions.
apps/backend/src/__tests__/platform-link-ordering.test.ts (2)

196-207: 💤 Low value

Consider making the aggregate mock stateful for more realistic concurrency simulation.

The test mocks aggregate to always return max=2, so both concurrent requests attempt displayOrder=3, and after retries both succeed with the same value. In reality, after the first successful insert, the second retry would read an updated max and insert at a different displayOrder.

While the test successfully verifies that the retry mechanism works and both requests complete, a more realistic simulation would update the aggregate result after successful inserts to match actual database behavior.

♻️ Optional enhancement for test realism
 describe('createPlatformLink — concurrent request simulation', () => {
   it('two simultaneous creates both succeed via retry when first conflicts', async () => {
     const p2002 = Object.assign(new Error('Unique constraint failed on (user_id, display_order)'), {
       code: 'P2002',
     });
 
     let createCallCount = 0;
+    let currentMax = 2;
     (mockPrisma.platformLink.aggregate as any).mockImplementation(() => {
-      // Both reads see max=2 before either inserts
-      return Promise.resolve({ _max: { displayOrder: 2 } });
+      return Promise.resolve({ _max: { displayOrder: currentMax } });
     });
     (mockPrisma.platformLink.create as any).mockImplementation(({ data }: any) => {
       createCallCount++;
       // Simulate: first two calls (both at order=3) conflict; retries succeed
       if (createCallCount <= 2) {
         return Promise.reject(p2002);
       }
+      currentMax = Math.max(currentMax, data.displayOrder);
       return Promise.resolve(baseLink(`link-${createCallCount}`, data.displayOrder));
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/__tests__/platform-link-ordering.test.ts` around lines 196 -
207, The aggregate mock always returning {_max: {displayOrder: 2}} makes the
test unrealistic; make it stateful by introducing a closure variable (e.g.,
currentMax = 2) and have mockPrisma.platformLink.aggregate return {_max:
{displayOrder: currentMax}}; in the mockPrisma.platformLink.create
implementation (which uses createCallCount, p2002 and baseLink), increment
currentMax when a create resolves successfully so subsequent aggregate calls
reflect the new max, and keep the initial rejects (<=2) to simulate
conflict/retry.

227-233: 💤 Low value

Consider making the aggregate mock stateful for more realistic concurrency simulation.

Similar to the two-create test, this test uses a static aggregate result. While it successfully verifies that retries resolve conflicts, updating the mock to reflect successful inserts would more accurately simulate database behavior during concurrent operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/__tests__/platform-link-ordering.test.ts` around lines 227 -
233, The aggregate mock is static but should reflect successful inserts so
concurrency is simulated; update the test to make
mockPrisma.platformLink.aggregate stateful by tying its returned count to the
same counter used by (mockPrisma.platformLink.create as any).mockImplementation:
keep callCount and p2002/baseLink logic, but when create resolves (i.e., not
rejecting), increment a createdLinks counter (or derive from callCount) and have
mockPrisma.platformLink.aggregate read that counter and return { _max: {
displayOrder: ... } } / { _count: { id: createdLinks } } as appropriate so
aggregate reflects successful inserts during retries; ensure the aggregate mock
and create mock share the same closure/state so aggregate updates after each
successful create.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/__tests__/profile-cache.test.ts`:
- Line 87: The two app builder functions are missing explicit TypeScript return
types; add explicit return type annotations for buildProfileApp and the other
app builder function referenced at the other location (line ~101) to satisfy
`@typescript-eslint/explicit-function-return-type`—e.g., annotate them as async
functions returning Promise<express.Application> (or the precise app type your
helpers return) so the signature reads like: async function
buildProfileApp(withRedis = true): Promise<express.Application> { ... } (apply
the same style to the second builder).

In `@apps/backend/src/routes/cards.ts`:
- Around line 118-119: The two single-line if statements checking error?.code
('NOT_FOUND' and 'LAST_CARD') in the cards route handler must be converted to
block form to satisfy the ESLint curly rule; update the if (error?.code ===
'NOT_FOUND') and if (error?.code === 'LAST_CARD') branches to use braces and
place the existing reply.status(...).send(...) calls inside their respective { }
blocks so behavior is unchanged but style complies.

---

Outside diff comments:
In `@apps/backend/src/routes/cards.ts`:
- Around line 1-3: The import order violates ESLint import-x/order: move the
service import to come before utility imports; specifically, place "import * as
cardService from '../services/cardService'" above the imports for
"handleDbError" and "createCardSchema, updateCardSchema" so cardService is
imported before error.util.js and validators.js, preserving existing specifiers
and paths.

---

Nitpick comments:
In `@apps/backend/src/__tests__/platform-link-ordering.test.ts`:
- Around line 196-207: The aggregate mock always returning {_max: {displayOrder:
2}} makes the test unrealistic; make it stateful by introducing a closure
variable (e.g., currentMax = 2) and have mockPrisma.platformLink.aggregate
return {_max: {displayOrder: currentMax}}; in the mockPrisma.platformLink.create
implementation (which uses createCallCount, p2002 and baseLink), increment
currentMax when a create resolves successfully so subsequent aggregate calls
reflect the new max, and keep the initial rejects (<=2) to simulate
conflict/retry.
- Around line 227-233: The aggregate mock is static but should reflect
successful inserts so concurrency is simulated; update the test to make
mockPrisma.platformLink.aggregate stateful by tying its returned count to the
same counter used by (mockPrisma.platformLink.create as any).mockImplementation:
keep callCount and p2002/baseLink logic, but when create resolves (i.e., not
rejecting), increment a createdLinks counter (or derive from callCount) and have
mockPrisma.platformLink.aggregate read that counter and return { _max: {
displayOrder: ... } } / { _count: { id: createdLinks } } as appropriate so
aggregate reflects successful inserts during retries; ensure the aggregate mock
and create mock share the same closure/state so aggregate updates after each
successful create.

In `@apps/backend/src/services/profileService.ts`:
- Line 131: Remove the unreachable final throw statement that reads "Failed to
allocate display order after max retries" in the display-order allocation loop
(the throw at the end of the function that contains the P2002 retry logic, e.g.,
in the allocateDisplayOrder or equivalent method in profileService.ts); the loop
already returns on success and rethrows on the last retry, so delete that dead
throw, ensure the function signature/type still compiles, and run TypeScript
build/tests to confirm no regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4ea01be-2f1b-49a7-841d-678aadde4408

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9430a and 7f4104b.

📒 Files selected for processing (8)
  • apps/backend/prisma/migrations/20260612000000_platform_link_unique_display_order/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/__tests__/cards.test.ts
  • apps/backend/src/__tests__/platform-link-ordering.test.ts
  • apps/backend/src/__tests__/profile-cache.test.ts
  • apps/backend/src/routes/cards.ts
  • apps/backend/src/services/cardService.ts
  • apps/backend/src/services/profileService.ts


// ── App builders ──────────────────────────────────────────────────────────────

async function buildProfileApp(withRedis = true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit return type annotations.

The ESLint rule @typescript-eslint/explicit-function-return-type requires explicit return types on functions. Both app builder functions are missing return type annotations.

📝 Proposed fix
-async function buildProfileApp(withRedis = true) {
+async function buildProfileApp(withRedis = true): Promise<FastifyInstance> {
   const app = Fastify({ logger: false });
-async function buildPublicApp(withRedis = true) {
+async function buildPublicApp(withRedis = true): Promise<FastifyInstance> {
   const app = Fastify({ logger: false });

Also applies to: 101-101

🧰 Tools
🪛 GitHub Actions: CI / 1_backend-ci.txt

[warning] 87-87: @typescript-eslint/explicit-function-return-type: Missing return type on function

🪛 GitHub Actions: CI / backend-ci

[warning] 87-87: ESLint warning: Missing return type on function @typescript-eslint/explicit-function-return-type

🪛 GitHub Check: backend-ci

[warning] 87-87:
Missing return type on function

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/__tests__/profile-cache.test.ts` at line 87, The two app
builder functions are missing explicit TypeScript return types; add explicit
return type annotations for buildProfileApp and the other app builder function
referenced at the other location (line ~101) to satisfy
`@typescript-eslint/explicit-function-return-type`—e.g., annotate them as async
functions returning Promise<express.Application> (or the precise app type your
helpers return) so the signature reads like: async function
buildProfileApp(withRedis = true): Promise<express.Application> { ... } (apply
the same style to the second builder).

Comment thread apps/backend/src/routes/cards.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/prisma/schema.prisma">

<violation number="1" location="apps/backend/prisma/schema.prisma:54">
P1: Adding `@@unique([userId, displayOrder])` without first deduplicating existing duplicate rows in the migration will cause PostgreSQL to reject `CREATE UNIQUE INDEX` if duplicates exist. The migration must include a pre-step to remediate historical duplicates (e.g., assign sequential display orders per user) before creating the unique constraint.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
cardLinks CardLink[]

@@unique([userId, displayOrder])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Adding @@unique([userId, displayOrder]) without first deduplicating existing duplicate rows in the migration will cause PostgreSQL to reject CREATE UNIQUE INDEX if duplicates exist. The migration must include a pre-step to remediate historical duplicates (e.g., assign sequential display orders per user) before creating the unique constraint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/prisma/schema.prisma, line 54:

<comment>Adding `@@unique([userId, displayOrder])` without first deduplicating existing duplicate rows in the migration will cause PostgreSQL to reject `CREATE UNIQUE INDEX` if duplicates exist. The migration must include a pre-step to remediate historical duplicates (e.g., assign sequential display orders per user) before creating the unique constraint.</comment>

<file context>
@@ -51,6 +51,7 @@ model PlatformLink {
   user      User       @relation(fields: [userId], references: [id], onDelete: Cascade)
   cardLinks CardLink[]
 
+  @@unique([userId, displayOrder])
   @@map("platform_links")
 }
</file context>

Ridanshi added 2 commits June 12, 2026 18:52
…ions

Root cause: createPlatformLink, updatePlatformLink, deletePlatformLink, and
reorderLinks all mutated the database but never called redis.del on the
profile:<username> cache key, leaving stale data served to viewers until
the 5-minute TTL expired naturally.

Fix: Add a private invalidateProfileCacheForUser helper that resolves the
username via a lightweight SELECT then calls redis.del. All four mutation
functions now await this helper after a successful DB write so the cache
is cleared immediately. Cache invalidation is skipped when Redis is absent
and errors are caught and logged non-fatally so a Redis blip never fails
a mutation request.

Also fix the DELETE /api/cards/:id route handler which checked error codes
as return values; the service throws errors, so the handler now catches them.
Fix cards.test.ts duplicate buildApp declaration, and apply the PlatformLink
type fix to cardService.ts (upstream/main has not yet merged that PR).

Tests: 21 new tests in profile-cache.test.ts cover cache hit/miss lifecycle,
all four mutation paths, failed mutations, non-existent links, Redis-absent
mode, consecutive mutations, cache repopulation, and non-fatal Redis errors.
…aint and retry

Concurrent createPlatformLink calls both read the same max(displayOrder)
and insert the same value, corrupting link ordering for the user.

- Add @@unique([userId, displayOrder]) to PlatformLink schema with migration
- Wrap createPlatformLink in a retry loop (max 5 attempts) that re-reads
  max and retries on P2002 unique constraint violations
- Reorder uses two-phase transaction (temp offset then final values) to
  avoid constraint conflicts when adjacent positions swap
- Add platform-link-ordering.test.ts covering concurrency, retry, two-phase
  reorder, ordering integrity, and regression scenarios

Closes Dev-Card#485
@Ridanshi Ridanshi force-pushed the fix/platform-link-display-order-race branch from 7f4104b to b9591d0 Compare June 12, 2026 13:23
@Ridanshi Ridanshi closed this Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent platform-link creation can assign duplicate display orders and corrupt link ordering

2 participants