-
-
Notifications
You must be signed in to change notification settings - Fork 720
Description
Checklist
- Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
- Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/
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.
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:
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.

