Skip to content

test: reproduce duplicate-text deletion swallowing a peer's concurrent insert - #3006

Draft
christianhg wants to merge 1 commit into
mainfrom
repro-duplicate-delete-dmp-anchor
Draft

test: reproduce duplicate-text deletion swallowing a peer's concurrent insert#3006
christianhg wants to merge 1 commit into
mainfrom
repro-duplicate-delete-dmp-anchor

Conversation

@christianhg

Copy link
Copy Markdown
Member

Red by design: this is a reproduction, not a fix. During a customer's collaborative session, deleting text that exists as an exact duplicate in the same block corrupted the result. Reproduced deterministically here, and the proven variant is nastier than the report: it is convergent silent data loss.

The mechanism is structural to the wire format. Deleting one of two exact copies travels as a diffMatchPatch whose context anchors are identical at both copies. When the peer's concurrent insert reaches the store first, the delete applies against a base shifted by text the deleter never saw, anchors at the wrong copy's tail, and consumes the peer's insert. applyPatches reports success, and server and both editors converge unanimously on the corrupted value: the peer's typed "hi" is gone, no divergence ever forms, and no repair machinery downstream can see anything to repair. A 30-line kernel probe against applyPatches(parsePatch(...)) shows the same swallow with no editors involved.

Two choreographies are pinned in the two-client harness (plugin.two-client.browser.test.tsx): peer typing at the end of the block, and peer typing inside the to-be-deleted copy. Both fail on the final-text assert while settleAndAssertConvergence passes. Delivery order is load-bearing: with the delete arriving at the store first, both scenarios pass, which is why the existing convergence suite never caught this.

The fix conversation is architectural rather than local: diffMatchPatch context matching is definitionally ambiguous on repeated text, so candidates include tightening the offset hint, rejecting low-confidence fuzzy matches instead of applying them, or a different wire representation for deletions. Whichever design wins turns these tests green. Related: repeated paste under concurrent typing manufactures exactly the duplicate text this mechanism needs, so the two field reports compose.

…t insert

Red by design; this is a repro branch, not a fix. Deleting one of two
exact copies of a string travels as a `diffMatchPatch` whose context
anchors are identical at both copies. When the peer's insert reaches
the store first, the delete applies against a base shifted by text the
deleter never saw, anchors at the wrong copy's tail, and consumes the
peer's insert. `applyPatches` reports success and every party
converges on the corrupted value: no divergence, no repair, silent
loss of the peer's text.

Two choreographies pinned in the two-client harness: peer typing at
the end of the block and peer typing inside the to-be-deleted copy.
Both fail on the final-text assert while the convergence assert
passes. Delivery order is load-bearing: with the delete arriving
first, both scenarios pass, which is why the existing suite never
caught it.
@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 45901b6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-example-basic Ready Ready Preview, Comment Jul 22, 2026 7:16pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
portable-text-playground Skipped Skipped Jul 22, 2026 7:16pm

Request Review

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (5ddcc5b4)

@portabletext/editor

Metric Value vs main (5ddcc5b)
Internal (raw) 807.4 KB -
Internal (gzip) 155.1 KB -
Bundled (raw) 1.42 MB -
Bundled (gzip) 319.4 KB -
Import time 102ms +1ms, +1.0%

@portabletext/editor/behaviors

Metric Value vs main (5ddcc5b)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -2.1%

@portabletext/editor/plugins

Metric Value vs main (5ddcc5b)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms +0ms, +2.2%

@portabletext/editor/selectors

Metric Value vs main (5ddcc5b)
Internal (raw) 82.7 KB -
Internal (gzip) 15.4 KB -
Bundled (raw) 78.4 KB -
Bundled (gzip) 14.3 KB -
Import time 8ms +0ms, +2.8%

@portabletext/editor/traversal

Metric Value vs main (5ddcc5b)
Internal (raw) 28.1 KB -
Internal (gzip) 5.6 KB -
Bundled (raw) 28.1 KB -
Bundled (gzip) 5.5 KB -
Import time 6ms +0ms, +3.7%

@portabletext/editor/utils

Metric Value vs main (5ddcc5b)
Internal (raw) 30.6 KB -
Internal (gzip) 6.4 KB -
Bundled (raw) 28.2 KB -
Bundled (gzip) 6.1 KB -
Import time 6ms +0ms, +1.3%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @portabletext/markdown

Compared against main (5ddcc5b4)

Metric Value vs main (5ddcc5b)
Internal (raw) 53.8 KB -
Internal (gzip) 9.8 KB -
Bundled (raw) 348.9 KB -
Bundled (gzip) 96.3 KB -
Import time 41ms -0ms, -0.2%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

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