8000 Y.Doc items integrated in different order after sync from server, possible corruption from UndoManager · Issue #699 · yjs/yjs · GitHub
[go: up one dir, main page]

Skip to content

Y.Doc items integrated in different order after sync from server, possible corruption from UndoManager #699

@james-atticus

Description

@james-atticus

Checklist

Describe the bug

We're using Yjs to sync data for our Lexical-based editor, using their lexical-yjs package. I've hit an issue where the Yjs document loaded from the server has text content in a different order from what I would expect (before it reaches Lexical). Specifically, the server and client have Items integrated in a different order. More details below, but I think UndoManager may have somehow created an Item that has semantically conflicting origin/rightOrigin.

To Reproduce

The file in question (zipped so Github will let me upload it): yjs-2025-03-18-sync-issue.zip

Use the following script which mimics syncing the doc using y-protocols:

import * as fs from 'fs';
import * as decoding from 'lib0/decoding';
import * as encoding from 'lib0/encoding';
import { readSyncMessage, writeSyncStep1 } from 'y-protocols/sync';
import * as Y from 'yjs';

const f = fs.readFileSync('/path/to/yjs-2025-03-18-sync-issue');

// Load the YDoc on the server.
const serverYDoc = new Y.Doc();
Y.applyUpdate(serverYDoc, f, { isRemote: true });

// Init empty doc on client.
const clientYDoc = new Y.Doc();

const performSync = (initiatingDoc, remoteDoc) => {
    // Send sync step 1
    const sync1Encoder = encoding.createEncoder();
    writeSyncStep1(sync1Encoder, initiatingDoc);
    const sync1Uint8Arr = encoding.toUint8Array(sync1Encoder);

    // Read sync step 1, reply with sync step 2
    const sync2Encoder = encoding.createEncoder();
    readSyncMessage(decoding.createDecoder(sync1Uint8Arr), sync2Encoder, remoteDoc, { remote: true });
    const sync2Uint8Arr = encoding.toUint8Array(sync2Encoder);

    // Read sync step 2
    readSyncMessage(decoding.createDecoder(sync2Uint8Arr), encoding.createEncoder(), initiatingDoc, { remote: true });
};

performSync(clientYDoc, serverYDoc);
// Note: having the server also initiate a sync with the client didn't change the final result.
// performSync(serverYDoc, clientYDoc);

// Compare the paragraphs
const printParagraph = (doc) => console.log(doc.get('root', Y.XmlText).toDelta()[0].insert.toDelta()[1].insert);
printParagraph(serverYDoc);
printParagraph(clientYDoc);

Observe that the text is different:

. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu grruat nulla pariaturExcepteur officia dessint occaecat cupidatat non proident, sunt in culpa qui erunt mollit anim id est labi
. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu rugrat nulla pariaturExcepteur sint occaecat cupidatat non proident, sunt in culpa qui erunt mollit anim id officia desest labi

Expected behavior

Items should be integrated in the same order.

Screenshots

Below is what the Items look like on the "client" Y.Doc, following each item using .next.

Image

Note that the Item containing officia des appears later. What's weird is that its origin is [x389, 4721], which I think(?) means it was inserted after sint occaecat [...], however its rightOrigin is [x389, 4666] which puts it before that same Item.

By comparison, here's what it looks like on the "server" Y.Doc:

Image

Environment Information

  • Client: y-protocols
    • y-protocols 1.0.6
    • yjs 13.6.15
    • lexical 0.27.0
    • @hocuspocus/provider 2.13.0
  • Server
    • y-protocols 1.0.6
    • yjs 13.6.24
    • lexical 0.27.1
    • @hocuspocus/server 2.15.2

Additional context

We're using Yjs' UndoManager to handle undo/redo. I'm 95% sure the document got into this state when I changed some text from a paragraph to a heading, then reverted with undo. I did a few undo/redo operations in quick succession though so I can't say for sure.

Lexical uses Y.XmlText to represent block elements (root, paragraph), embeds (Y.Map) to mark text formatting, and string content for the text itself. Eg:

XmlText (root)
- XmlText (paragraph)
- - Map (formatting marker: bold, colour, etc)
- - ContentString (the actual text content)

Changing from paragraph to heading would have deleted the paragraph's XmlText, and created a new XmlText with new children for the heading.

  • I'm a sponsor 💖
  • This issue is a blocker for my project.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

    0