Skip to content

fix(collab): prevent stale view when remote Y.js changes bypass sdBlockRev increment#2099

Merged
harbournick merged 4 commits intomainfrom
tadeu/sd-1852-blocker-editor-intermittent-stale-view
Feb 20, 2026
Merged

fix(collab): prevent stale view when remote Y.js changes bypass sdBlockRev increment#2099
harbournick merged 4 commits intomainfrom
tadeu/sd-1852-blocker-editor-intermittent-stale-view

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Feb 18, 2026

Summary

  • Root cause: FlowBlockCache uses sdBlockRev for O(1) invalidation, but the collab plugin skips incrementing sdBlockRev for Y.js-origin transactions (to prevent an infinite sync loop). This created a window where remote content changes arrive but the cache returns stale blocks because the rev appears unchanged — only reproducible in production with real network latency.
  • Fix: Add setHasExternalChanges() to FlowBlockCache. When PresentationEditor detects a Y.js-origin transaction, it flags the cache to fall back to JSON comparison for that render cycle instead of trusting the matching rev. The flag auto-clears on commit() so normal local editing stays O(1).
  • Also fixed: File upload in collab mode was destroying and recreating the editor instead of using replaceFile(), causing a race condition where the empty Y.js room state could overwrite the new document content.

Changes

File Change
pm-adapter/src/cache.ts Add setHasExternalChanges(flag) — forces JSON fallback when Y.js changes bypass rev increment
pm-adapter/src/cache.d.ts Update type declaration
pm-adapter/src/cache.test.ts 4 new tests including regression test proving the original bug
PresentationEditor.ts Detect Y.js-origin transactions via ySyncPluginKey meta and signal cache
SuperdocDev.vue Use replaceFile() on existing editor in collab mode instead of full reinit

Test plan

  • All 1543 cache unit tests pass
  • Two-tab collab: edits in Tab A appear in Tab B (typing, suggesting mode, accept/reject tracked changes)
  • File upload in collab mode replaces content without race condition
  • Local editing performance unaffected (O(1) cache path unchanged for non-Y.js transactions)

Closes SD-1852

Copilot AI review requested due to automatic review settings February 18, 2026 16:21
@linear
Copy link

linear bot commented Feb 18, 2026

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a subtle collaboration bug where remote Y.js changes could result in stale cached content being displayed. The root cause stems from the FlowBlockCache's O(1) fast path relying on sdBlockRev for cache invalidation, while the blockNodePlugin intentionally skips incrementing sdBlockRev for Y.js-origin transactions to prevent infinite sync loops. The fix adds a setHasExternalChanges() flag to the cache that forces JSON comparison when external changes occur, ensuring correctness while preserving O(1) performance for local edits.

Changes:

  • Added setHasExternalChanges() mechanism to FlowBlockCache with dual-strategy cache validation (fast revision check for local edits, JSON comparison when Y.js changes detected)
  • Wired PresentationEditor to detect Y.js-origin transactions and signal the cache accordingly
  • Fixed file upload race condition in collaboration mode by using replaceFile() instead of destroying and recreating the editor

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/layout-engine/pm-adapter/src/cache.ts Adds #hasExternalChanges flag and dual-strategy validation logic that falls through to JSON comparison when Y.js changes bypass rev increment
packages/layout-engine/pm-adapter/src/cache.d.ts Updates type declaration with new setHasExternalChanges() method signature
packages/layout-engine/pm-adapter/src/cache.test.ts Adds 10 comprehensive tests including 4 specifically for the externalChanges feature and a regression test demonstrating the original bug
packages/super-editor/src/core/presentation-editor/PresentationEditor.ts Detects Y.js-origin transactions via ySyncPluginKey metadata and signals cache to use JSON comparison for that render cycle
packages/superdoc/src/dev/components/SuperdocDev.vue Switches to replaceFile() in collab mode to avoid Y.js race condition where empty room state could overwrite new document content

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ckRev increment

FlowBlockCache uses sdBlockRev for O(1) cache invalidation, but the
collaboration plugin skips incrementing sdBlockRev for Y.js-origin
transactions to avoid an infinite sync loop. This created a window
where remote content changes arrive but the cache returns stale blocks
because the rev appears unchanged.

Fix: add setHasExternalChanges() to FlowBlockCache. When a Y.js-origin
transaction is detected in PresentationEditor, the cache falls back to
JSON comparison for that cycle instead of trusting the matching rev.
The flag auto-clears on commit() so normal editing remains O(1).

Also fix file upload in collab mode: use replaceFile() on the existing
editor rather than destroying and recreating SuperDoc, which caused a
race condition where the empty Y.js room state overwrote the new
document content.
@tupizz tupizz force-pushed the tadeu/sd-1852-blocker-editor-intermittent-stale-view branch from 0bc463f to aa9cb89 Compare February 18, 2026 16:29
Copy link
Collaborator

@harbournick harbournick left a comment

Choose a reason for hiding this comment

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

LGTM

@harbournick harbournick merged commit 0895a93 into main Feb 20, 2026
3 checks passed
@harbournick harbournick deleted the tadeu/sd-1852-blocker-editor-intermittent-stale-view branch February 20, 2026 00:07
harbournick pushed a commit that referenced this pull request Feb 20, 2026
# [1.15.0](v1.14.0...v1.15.0) (2026-02-20)

### Bug Fixes

* **ai-actions:** preserve html/markdown insertion and prevent repeated formatted replacement ([#2117](#2117)) ([9f685e9](9f685e9))
* **ai:** support headless mode in EditorAdapter.applyPatch ([#1859](#1859)) ([cf9275d](cf9275d))
* **collaboration:** memory leaks, Vue stack overflow, and Liveblocks stability (SD-1924) ([#2030](#2030)) ([a6827fd](a6827fd)), closes [#prepareDocumentForExport](https://github.com/superdoc-dev/superdoc/issues/prepareDocumentForExport)
* **collab:** prevent stale view when remote Y.js changes bypass sdBlockRev increment ([#2099](#2099)) ([0895a93](0895a93))
* **converter:** handle null list lvlText and always clear numbering cache ([#2113](#2113)) ([336958c](336958c))
* **document-api:** remove search match cap and validate moveComment bounds ([6d3de67](6d3de67))
* export docx blobs with docx mime type ([#1849](#1849)) ([1bc466d](1bc466d))
* **export:** prevent DOCX corruption from entity encoding and orphaned delInstrText (SD-1943) ([#2102](#2102)) ([56e917f](56e917f)), closes [#replaceSpecialCharacters](https://github.com/superdoc-dev/superdoc/issues/replaceSpecialCharacters) [#1988](#1988)
* **layout-bridge:** correct cell selection for tables with rowspan ([#1839](#1839)) ([0b782be](0b782be))
* **layout,converter:** text box rendering and page-relative anchor positioning (SD-1331, SD-1838) ([#2034](#2034)) ([3947f39](3947f39))
* **layout:** route list text-start calculations through resolveListTextStartPx ([02b14b8](02b14b8))
* **painter-dom:** use absolute page Y for page-relative anchors in header/footer decorations ([0b9bc72](0b9bc72))
* preserve selection highlight when opening toolbar dropdowns ([#2097](#2097)) ([a33568e](a33568e))
* structured content renders correct on hover and select ([#1843](#1843)) ([dab3f04](dab3f04))
* **super-editor:** add unsupported-content reporting across HTML/Markdown import paths ([#2115](#2115)) ([84880b7](84880b7))
* **super-editor:** handle partial comment file-sets and clean up stale parts on export ([#2123](#2123)) ([f63ae0a](f63ae0a))
* **super-editor:** restore <hr> contentBlock parsing and harden VML HR export fallback ([#2118](#2118)) ([da51b1f](da51b1f))
* table headers are incorrectly imported from html ([#2112](#2112)) ([e8d1480](e8d1480))
* table resizing regression ([#2091](#2091)) ([20ed24e](20ed24e))
* table resizing regression ([#2091](#2091)) ([9a07f1c](9a07f1c))
* **tables:** align tableHeader attrs with tableCell to fix oversized DOCX export widths ([#2114](#2114)) ([38f0430](38f0430))
* **tables:** fix autofit column scaling, cell width overflow, and page break splitting ([#1987](#1987)) ([61a3f6f](61a3f6f))
* **tables:** prevent tblInd double-shrink when using tblGrid widths (SD-1494) ([8750ece](8750ece))
* track changes comment text for formatting changes ([#2013](#2013)) ([b2a43ff](b2a43ff))
* wire DocumentApi to Editor.doc with lifecycle-safe caching ([57326ea](57326ea))

### Features

* cropped images ([#1940](#1940)) ([3767a49](3767a49))
* extend document-api with format, examples, create.heading ([#2092](#2092)) ([fdf8c7c](fdf8c7c))
* **lists:** support hidden list indicators via w:vanish ([#2069](#2069)) ([#2080](#2080)) ([0bed0fd](0bed0fd))
* the document API limited alpha ([#2087](#2087)) ([091c24c](091c24c))
@harbournick
Copy link
Collaborator

🎉 This PR is included in superdoc v1.15.0

The release is available on GitHub release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants