-
Notifications
You must be signed in to change notification settings - Fork 64
Description
What's on your mind?
When a new client joins a collaborative editing session, they receive the full document state from the server (~900KB), but then immediately echo it back as a SYNC_UPDATE. This creates a feedback loop where every join triggers massive broadcasts to all other connected clients.
Current Behavior
- Client A is editing a document (~850KB of Y.js state)
- Client B joins the session
- Server sends Client B the full document state (SYNC_STEP2, ~892KB) ✅ correct
- Client B's browser sends the entire document back as SYNC_UPDATE (~852KB) ❌ bug
- Server broadcasts this to Client A
- Both clients freeze while processing the redundant 852KB update
Timeline from our logs:
13:54:32.945 - Client B receives 892KB SYNC_STEP2 from server
13:54:32.950 - Y.Doc update event fires with origin=provider (correct, not broadcast)
13:54:33.308 - Y.Doc update event fires AGAIN with origin="Object" (wrong!)
13:54:33.308 - Client B sends 852KB SYNC_UPDATE back to server
13:54:33.434 - Server broadcasts 852KB to Client A
Expected Behavior
When a client receives state from the server, it should NOT echo that state back. The sync protocol should be:
- Client → Server: SYNC_STEP1 (state vector, ~10 bytes)
- Server → Client: SYNC_STEP2 (missing state, ~900KB)
- Client → Server: SYNC_STEP2 (any state server is missing, likely ~0 bytes)
- No SYNC_UPDATE echo
Root Cause Analysis
The issue is in the interaction between y-prosemirror and y-websocket:
- Y.js receives the update from WebSocket with
origin = provider - Y.js fires change events on shared types (Y.XmlFragment, etc.)
y-prosemirror'sobserveDeephandler catches these and updates ProseMirror- ProseMirror state change triggers
y-prosemirror'sappendTransaction - This creates a NEW Y.js transaction with
origin = ySyncPluginKey(not the provider!) y-websocket's_updateHandlerseesorigin !== thisand broadcasts
The key evidence from browser console:
// First update - correctly ignored
{ updateSize: 892985, origin: "F", isLocal: false } // "F" is minified provider
// Second update - incorrectly broadcast (355ms later)
{ updateSize: 852042, origin: "Object", isLocal: false } // Different origin!Impact
- Network: Every client join broadcasts ~850KB × (N-1) clients
- Performance: All clients freeze for 300-500ms processing redundant data
- UX: Multi-user editing becomes unusable with documents >500KB
Suggested Fix
In y-prosemirror's sync plugin, the _typeChanged handler should check if the transaction origin is a network source before triggering appendTransaction. Something like:
_typeChanged(events, transaction) {
// Skip if this change came from the network (WebSocket provider)
if (transaction.origin && transaction.origin.ws) return;
// ... rest of handler
}Or alternatively, y-websocket could track recently-received update hashes and skip broadcasting duplicates.
Environment
- SuperDoc Version: Using collaboration-yjs from superdoc packages
- y-websocket: Standard npm package
- y-prosemirror: Standard npm package
- Y.js: 13.x
- Document size: ~850KB Y.js state (typical legal document)
- Browser: Chrome/Firefox (reproducible in both)
Steps to Reproduce
- Open a collaborative document with substantial content (~100+ pages)
- Have Client A connected and idle
- Open the same document in a new browser tab (Client B)
- Observe network traffic - Client B sends ~850KB SYNC_UPDATE immediately after joining
- Observe Client A's UI freezes briefly
Related Issues
- [Bug] Every keystroke triggers a large Y.Doc update with the full document #1830 discusses a similar symptom (large updates) but different cause (DOCX export on keystroke)
- This issue is specifically about the Y.js sync protocol echo on client join