Description:
I am implementing collaborative editing functionality using Yjs for a script editor in my React application. The collaborative editing works well, but I'm encountering an issue when multiple users type at the same time. For each key press, the entire typed content inside the Yjs document gets duplicated.
Here's a simplified overview of my setup:
Socket Handling (socket.mjs):
// socket.mjs
import { createServer } from 'http';
import { parse } from 'url';
import next from 'next';
import { Server as SocketIOServer } from 'socket.io';
import * as Y from 'yjs';
import dotenv from 'dotenv';
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
dotenv.config({ path: envFile });
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const ydoc = new Y.Doc();
const yxmlFragment = ydoc.getXmlFragment('shared');
const ydocMap = new Map();
const activeUsers = {};
app.prepare().then(() => {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
});
const io = new SocketIOServer(server, {
cors: {
origin: process.env.BASE_URL || "http://localhost:3000", // Use environment variable or default
methods: ["GET", "POST"],
allowedHeaders: ["*"],
credentials: true
}
});
io.on('connection', (socket) => {
// When a user connects, send the current document state as a binary update
console.log('Initiating')
socket.emit('init', Y.encodeStateAsUpdate(ydoc));
socket.on('updateScene', ({ sceneId, userId, update }) => {
console.log(sceneId);
const ydocForScene = ydocMap.get(sceneId);
Y.applyUpdate(ydocForScene, update);
socket.to(sceneId).emit(`update:${sceneId}`, { update: update, userId: userId });
});
});
const PORT = process.env.SOCKET_PORT || 4000;
server.listen(PORT, () => {
console.log(`> Ready on http://localhost:${PORT}`);
});
});
Script Editor Component (ScriptEditor.tsx):
// ScriptEditor.tsx
import * as yup from 'yup'
import io from 'socket.io-client';
import * as Y from 'yjs';
const ScriptEditor = (props: any) => {
// Initialize Yjs document and WebsocketProvider
const ydoc = new Y.Doc();
const wsProvider = new WebsocketProvider('ws://localhost:3001', 'my-room', ydoc);
initializeSocket(ydoc);
useEffect(() => {
// Initialize once and store in refs
if (!ydocRef.current) {
ydocRef.current = new Y.Doc();
}
if (!yxmlFragmentRef.current) {
yxmlFragmentRef.current = ydocRef.current.getXmlFragment('shared');
}
if (!socketRef.current) {
socketRef.current = io(process.env.WEB_SOCKET_URL);
socketRef.current.on('init', (update) => {
if (update) {
const updatedArray = new Uint8Array(update);
Y.applyUpdate(ydocRef.current, updatedArray);
}
});
}
const ydoc = ydocRef.current;
const yxmlFragment = yxmlFragmentRef.current;
const socket = socketRef.current;
socket.on(`update:${sceneId}`, (data) => {
if (data.userId != userData.user.id) {
try {
// Convert the received ArrayBuffer to a Uint8Array
const update = new Uint8Array(data.update);
// Apply the update to the new document
Y.applyUpdate(ydoc, update);
} catch (e) {
console.error('Error applying update:', e);
}
}
});
}
return (
<div>
{/* Your script editor UI and contenteditable area */}
<div
id="script-container"
ref={editorRef}
style={{ outline: 'none' }}
contentEditable={!(project?.isDemoProject || !allowEdit)}
dangerouslySetInnerHTML={{ __html: scriptText }}
onInput={valueChange}
onMouseUp={handleSelection}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
>
</div>
);
};
export default ScriptEditor;
Issue:
Whenever multiple users are simultaneously typing in the collaborative script editor, each key press results in the entire content getting duplicated in the Yjs document. This leads to unexpected behavior and makes the collaborative editing feature impractical.
Question:
- What could be causing this duplication issue when multiple users are typing simultaneously in a Yjs collaborative editing setup?
- How can I ensure that only the changes made by the user are reflected in the Yjs document without unnecessary duplications?
Additionally, I want to mention that we are using a custom contenteditable div
for script editing, and we are not using any built-in plugins like Quill. Any insights, suggestions, or code improvements would be greatly appreciated. Thank you!
I would need to see your input handling code (e.g. valueChange). But I would guess that you are updating
yXmlFragment
by overwriting its state completely. Yjs interprets that as deleting all existing text and inserting all new text; if two users do so concurrenty, they both insert the complete new text, duplicating it.Instead, you need to figure out the precise changes (e.g. char 'x' inserted at ...) and call corresponding Yjs methods. E.g. see y-prosemirror's
updateYText
method, which takes a diff and sends the precise changes to aY.Text
node within the XML tree: https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js