مستوى المقال: متوسط — مطلوب معرفة JavaScript أساسي، فهم بسيط للـ events في DOM، وخبرة استدعاء مكتبات من npm أو CDN. مفيش حاجة من backend مطلوبة.
لو محتاج تبني محرّر نصوص يكتب فيه أكتر من مستخدم في نفس اللحظة، أول حل بيخطر في بالك سيرفر WebSocket بيـ broadcast التعديلات لكل المستخدمين. الحل ده شغّال لما تكون 3 ناس وإنترنت مستقر، لكنه بيتكسر لما حد يقطع لحظة، أو يدخل user رقم 6، أو ترسل operation مرتين. CRDTs بتحل المشكلة دي رياضياً، و مكتبة Yjs بتطبّق الحل في 80 سطر بدون Backend مخصّص.
Collaborative Editor زي Google Docs في 80 سطر JavaScript
المشكلة باختصار
لما اتنين بيكتبوا في نفس السطر في نفس اللحظة، السيرفر التقليدي بيستخدم Operational Transformation أو ما يُعرف بـ OT. الطريقة دي بتحتاج سيرفر مركزي يحسب كل عملية بترتيب صارم، ولو فُقدت رسالة واحدة، النص بيتفسد ولازم تبدأ من نسخة احتياطية. Google Docs اشتغلت 12 سنة وكتبت 50,000 سطر كود علشان توصل لـ OT engine موثوق. ده مش مسار واقعي لو إنت بتبني feature في sprint.
CRDTs (Conflict-Free Replicated Data Types) بتعكس المعادلة. بدل ما يكون فيه central authority بيحسم التعارض، كل تعديل بيحمل معلومات كافية علشان يدمج نفسه مع أي تعديل تاني، بأي ترتيب وصل، حتى لو وصل أكتر من مرة، والنتيجة النهائية بتطلع نفسها على كل المستخدمين. يعني السيرفر بقى pipe بسيط ينقل bytes، مش brain يحسم خلافات.
المفهوم بمثال بسيط للمبتدئ
تخيّل اتنين أصحاب بيدوّنوا ملاحظات في كرّاسة ورق، وكل واحد عنده نسخة. اتفقوا على قاعدة: كل سطر يكتبه أي حد يحط جنبه ID من جزئين: الساعة بالميلي ثانية + اسم الكاتب. لما يتقابلوا ويدمجوا الورقتين، بيرتّبوا الأسطر بالـ ID. لو الأول كتب "اشتري خبز" والتاني كتب "اشتري لبن" في نفس اللحظة، الاتنين بيظهروا في النسخة المجمّعة، بدون فقد، بدون "مين قبل مين". الترتيب حتمي ومحسوم رياضياً.
القاعدة دي حتى لو الورقة وقعت ودوّر عليها كلب، ولما يجمعوها مع نسخة صاحبك، النتيجة هتطلع نفسها. لو فقدوا نسخة كاملة، ساعتها بس بيخسروا تعديلات. غير كده، كل تعديل ضامن إنه هيلحق المستند.
التعريف العلمي: CRDT حسب ورقة Shapiro et al. 2011 هو هيكل بيانات بيضمن "Strong Eventual Consistency"، يعني لو وصلت كل التعديلات لكل العقد (بأي ترتيب)، النتيجة النهائية بتكون متطابقة بدون coordination. Yjs تحديداً بتطبّق YATA algorithm من ورقة Petso et al. 2016، وهو نوع متطوّر من CRDT مُحسَّن للنصوص الطويلة وعمليات الحذف المتداخلة.
الكود الكامل — 80 سطر شغّالة
الـ stack: Yjs 13.6 + y-webrtc 10.3 للنقل P2P بين المتصفحات عبر signaling server مجاني تقدّمه Yjs. مفيش backend مطلوب على لابتوبك أو على أي VPS.
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<title>Collaborative Editor</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; }
#editor { width: 100%; height: 400px; font-size: 18px; padding: 12px;
border: 2px solid #ddd; border-radius: 8px; line-height: 1.6; }
.status { padding: 10px; background: #f5f5f5; margin-bottom: 12px;
border-radius: 6px; font-size: 14px; }
.online { color: #16a34a; }
</style>
</head>
<body>
<div class="status">الحالة: <span id="status">جارٍ الاتصال...</span></div>
<textarea id="editor" placeholder="ابدأ تكتب، وافتح نفس الصفحة في تاب تاني..."></textarea>
<script type="module">
import * as Y from 'https://esm.sh/yjs@13.6.10';
import { WebrtcProvider } from 'https://esm.sh/y-webrtc@10.3.0';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('shared-doc');
const provider = new WebrtcProvider('haies-demo-room-2026-may', ydoc, {
signaling: ['wss://signaling.yjs.dev'],
maxConns: 20
});
const editor = document.getElementById('editor');
const status = document.getElementById('status');
ytext.observe(() => {
const remoteText = ytext.toString();
if (editor.value !== remoteText) {
const cursorPos = editor.selectionStart;
editor.value = remoteText;
editor.setSelectionRange(cursorPos, cursorPos);
}
});
editor.addEventListener('input', () => {
ydoc.transact(() => {
ytext.delete(0, ytext.length);
ytext.insert(0, editor.value);
});
});
provider.on('peers', ({ webrtcPeers }) => {
const count = webrtcPeers.length;
status.innerHTML = count > 0
? `<span class="online">متصل مع ${count} مستخدم</span>`
: 'في انتظار مستخدمين آخرين...';
});
provider.on('synced', () => {
console.log('Document synced with peers');
});
</script>
</body>
</html>