مقالات معمّقة حول التكنولوجيا التي تشكّل المستقبل.

التعاون في تحرير النصوص أصعب مما تظن

CRDTs مقابل OT في التحرير التعاوني الفوري: لكل منهما مفاضلات لا يتحدث عنها أحد. الواقع العملي خلف النصوص متعددة المستخدمين.

يدان شبحيتان تكتبان على لوحة مفاتيح واحدة، وتتصادم بينهما تيارات من الضوء

تتعامل Google Docs مع التحرير التعاوني الفوري لملايين المستخدمين. يبدو الأمر بلا جهد: تكتب، فيتحرك مؤشرك، وتظهر تعديلات زميلك فورًا. لكن هذه البساطة خادعة. فالتحرير التعاوني، في جوهره، من أصعب المشكلات في الأنظمة الموزعة، والنهجان الرئيسيان لحلها (Operational Transformation وCRDTs) لكل منهما مفاضلات لا تتضح إلا عندما تحاول البناء بهما فعلًا.

الطرح الذي يقدمه CRDTs (Conflict-free Replicated Data Types) مغرٍ: بنى بيانات تُدمج تلقائيًا دون تعارضات، ولا تحتاج إلى خادم مركزي، ويتحقق فيها الاتساق النهائي بقوة الرياضيات. لكن الواقع، كما اكتشفت فرق عدة بعد اختيار CRDTs لمحرراتها التعاونية، أكثر فوضى. النظرية جميلة، أما الهندسة فقاسية.

المشكلة: التعديلات المتزامنة

التحدي الجوهري: مستخدمان يحرران المستند نفسه في الوقت ذاته، مع تأخير في الشبكة بينهما. يُدرج المستخدم A كلمة 'Hello' عند الموضع 5. أما المستخدم B، الذي لم يرَ تعديل A بعد، فيحذف الحرف الموجود عند الموضع 5. كيف ينبغي أن يبدو المستند النهائي؟

السؤال هنا ملتبس. هل ينبغي أن ينطبق حذف B على الحرف الذي كان في الموضع 5 قبل إدراج A، أم بعده؟ إن كان قبله، فالحذف يزيل الحرف الأصلي وتظهر 'Hello' كاملة. وإن كان بعده، فالحذف يزيل الحرف 'H' من 'Hello'. كلا التفسيرين معقول. على النظام أن يختار أحدهما وأن يضمن أن كل العملاء يصلون إلى النتيجة نفسها، فالتباعد يعني أن يرى مستخدمان مستندين مختلفين، وهذا هو الفشل الذي لا يُغتفر.

The convergence problem:
Initial document: "The quick brown fox"
^ position 10
User A (at time T):  Insert "very " at position 10
User B (at time T):  Delete character at position 10
A sees locally: "The quick very brown fox"
B sees locally: "The quick rown fox"  (deleted 'b')
Now A receives B's operation: delete at position 10
But A already inserted 5 chars at position 10...
Should B's delete now apply at position 10? (deletes 'v' from 'very')
Or at position 15? (deletes 'b' from 'brown' — the original target)
Both A and B must arrive at the same document.
This is the problem OT and CRDTs solve differently.

Operational Transformation (OT)

يحل OT، وهو النهج الأقدم (1989)، المشكلة بتحويل العمليات مقابل بعضها البعض. عندما يستقبل A عملية B 'حذف عند الموضع 10'، يتحقق نظامه من العمليات التي طبّقها A ولم يرها B بعد. ثم يعدّل عملية B لتأخذ تلك التغييرات في الحسبان: بما أن A أدرج 5 أحرف عند الموضع 10، فينبغي أن ينطبق حذف B الآن عند الموضع 15 (لأن الحرف المستهدف انزاح إلى اليمين).

النهج يعمل، وGoogle Docs تستخدمه. المشكلة في دوال التحويل. بالنسبة إلى محرر نصوص يدعم عمليتي الإدراج والحذف، عليك تعريف كيفية تحويل كل زوج من العمليات مقابل الآخر، وهذا يعني أربع حالات. أضف التنسيق والجداول والصور والقوائم والتعليقات، وسيتضخم عدد الحالات توافقيًا. يجب أن تكون كل حالة صحيحة، والأخطاء الدقيقة تسبب تباعد المستندات، وهذا شبه مستحيل إعادة إنتاجه وتصحيحه.

كذلك يتطلب OT تقليديًا خادمًا مركزيًا لفرض ترتيب كلي للعمليات. بدون خادم، قد تُحوَّل العمليات المتزامنة بترتيبات مختلفة لدى عملاء مختلفين، فتنتج نتائج متباينة. تستطيع Google تحمّل تكلفة خادم مركزي لـ Docs. أما تطبيق من نوع الند للند (peer-to-peer) فلا يستطيع استخدام OT بسهولة.

CRDTs: الوعد النظري

تتخذ CRDTs نهجًا مختلفًا جذريًا. فبدلًا من تحويل العمليات، تستخدم بنى بيانات تُنتج فيها جميع ترتيبات الدمج الممكنة النتيجة نفسها. هذا ضمان رياضي: إذا كانت بنية البيانات CRDT صالحة، فالاتساق يتحقق تلقائيًا. لا دوال تحويل، ولا خادم مركزي، ولا اعتماد على ترتيب.

في تحرير النصوص، أهم متغيرات CRDT هي RGA (Replicated Growable Array) وما يشابهها من CRDTs للتسلسل. فبدلًا من تتبع المواضع (التي تتغير مع كل تعديل)، يُسند كل حرف معرّفًا فريدًا ومرتبًا عالميًا. يُنشئ الإدراج معرفات جديدة بين المعرفات الموجودة. ويُعلَّم الحذف المعرّف بوصفه شاهد قبر (tombstone)، أي لا يُزال فعليًا، والمزيد عن ذلك لاحقًا.

CRDT sequence representation:
Document: "cat"
Internal representation (simplified):
ID: (A,1) → 'c'   (user A, logical clock 1)
ID: (A,2) → 'a'   (user A, logical clock 2)
ID: (A,3) → 't'   (user A, logical clock 3)
User B inserts 'h' between 'c' and 'a':
ID: (B,1) → 'h'   position: between (A,1) and (A,2)
Document: "chat"
User A (concurrently) inserts 'r' between 'c' and 'a':
ID: (A,4) → 'r'   position: between (A,1) and (A,2)
Both IDs go between the same characters.
The CRDT uses a deterministic tie-breaking rule
(e.g., higher user ID wins) to order them.
Final document: "chart" or "chrat"
(deterministic — all clients get the same result)

CRDTs: الواقع العملي

وعد CRDTs، أي عدم وجود تعارضات، واللامركزية، واتساق مضمون رياضيًا، وعد حقيقي. لكن التحديات الهندسية كبيرة.

شواهد القبور تتراكم. عندما تحذف حرفًا في CRDT، لا يمكن إزالته من بنية البيانات، فقد لا تكون النسخ الأخرى قد رأت الحذف بعد، وتحتاج إلى المعرّف لتضع تعديلاتها في موضعها الصحيح. لذلك يُعلَّم الحرف المحذوف بوصفه شاهد قبر: غير مرئي لكنه ما زال موجودًا. مستند عُدّل كثيرًا قد يتراكم فيه آلاف شواهد القبور. مستند من 1000 حرف قد يحتوي على 50,000 شاهد قبر من سجل التحرير. هذا يضخم الذاكرة ويبطئ العمليات.

البيانات الوصفية ضخمة. يحتاج كل حرف إلى معرّف فريد (معرّف المستخدم مع طابع زمني منطقي)، ومؤشرات إلى المعرفات المجاورة، وعلامة الحذف. قد تصل البيانات الوصفية لكل حرف إلى 50 إلى 100 بايت. لمستند بحجم 100 كيلوبايت، قد يصل تمثيل CRDT إلى 5 إلى 10 ميغابايت. وهذا مهم للمزامنة، فإرسال حالة CRDT كاملة عبر الشبكة مكلف.

الأداء يتدهور مع تراكم السجل. العمليات على CRDT التسلسلي ليست O(1)، فإدراج حرف يتطلب إيجاد الموضع الصحيح في التسلسل المرتب بالمعرّفات، وهذا يعتمد على عدد المعرّفات الكلي (بما فيها شواهد القبور). في المستندات ذات سجلات التحرير الطويلة، يصبح ذلك بطيئًا بشكل ملحوظ.

الحفاظ على النية صعب. تضمن CRDTs الاتساق، أي أن جميع النسخ تصل إلى الحالة نفسها. لكن هل تصل إلى الحالة الصحيحة؟ عندما يحرر مستخدمان الكلمة نفسها في الوقت ذاته، يدمج CRDT أحرفهما بشكل حتمي. النتيجة متسقة لكنها قد تكون غير منطقية. يمكن تصميم OT بحيث يبقى تعديل أحد المستخدمين سليمًا، ويُطبَّق تعديل الآخر حوله. أما CRDTs فتدمج بشكل آلي.

Yjs والحل الوسط العملي

Yjs هي أشهر مكتبة CRDT لـ JavaScript، وتشغّل ميزات التعاون في كثير من تطبيقات الويب. وهي مصممة بعناية، وتعالج كثيرًا من مشكلات CRDT النظرية بتحسينات عملية، فالترميز الثنائي المضغوط يقلل من البيانات الوصفية، وجمع شواهد القبور يعمل عندما يكون جميع العملاء متصلين.

لكن Yjs ليست سحرًا. الفرق التي تتبناها تكتشف أن مكتبة CRDT تحل مشكلة الاتساق لا مشكلة التعاون. ما زلت تحتاج إلى: خادم إشارات لاكتشاف النظراء، وطبقة تخزين للتغييرات دون اتصال، وحل تعارضات للعمليات عالية المستوى (ماذا يحدث عندما يعيد مستخدمان هيكلة القسم نفسه؟)، وتتبع للحضور (المؤشرات والتحديدات)، وتراجع وإعادة تنفيذ يحترم تعديلات المستخدمين الآخرين، وإدارة للصلاحيات.

بعض الفرق، بعد بناء محرر تعاوني على Yjs، خلصت إلى أن نهج CRDT يضيف تعقيدًا لا تحتاجه. إذا كان تطبيقك يملك خادمًا مركزيًا (وأغلب التطبيقات تملكه)، فقد يكون OT، أو حتى نهج أبسط كـ 'الكتابة الأخيرة تفوز' مع كشف التعارض، أكثر عملية. تتألق CRDTs في سيناريوهات الند للند الحقيقية، مثل التطبيقات التي تعمل دون اتصال، والبرمجيات المحلية أولًا، والأنظمة التي لا يمكن فيها افتراض وجود سلطة مركزية.

ما الذي ينبغي لمعظم التطبيقات فعله

إذا كنت تضيف التحرير التعاوني إلى تطبيقك، فهذا إطار القرار العملي.

  • إذا كان لديك خادم مركزي ومستنداتك صغيرة: استخدم OT. نهج Google يعمل. مكتبات مثل ShareDB تطبّق OT لـ Node.js. الخادم المركزي يبسط كل شيء: الترتيب، والتخزين، وحل التعارضات، والصلاحيات.
  • إذا كنت تحتاج دعمًا دون اتصال أو الند للند: استخدم CRDTs (Yjs أو Automerge). CRDTs هي النهج الوحيد الذي يتعامل مع التحرير دون اتصال والمزامنة بين الند والند بشكل صحيح. اقبل تكلفة البيانات الوصفية وتراكم شواهد القبور كثمن لللامركزية.
  • إذا كان المتعاونون نادرًا ما يحررون القسم نفسه في الوقت ذاته: قد لا تحتاج أيًا منهما. القفل البسيط (محرر واحد لكل قسم) أو 'الكتابة الأخيرة تفوز' مع واجهة جيدة لعرض التعارض يغطيان كثيرًا من أنماط التعاون الواقعية دون تعقيد OT أو CRDTs.
  • إذا كنت تبني منافسًا لـ Google Docs: ستحتاج إلى فريق من مهندسي الأنظمة الموزعة وسنوات من التطوير. هذا ليس مشروع عطلة نهاية أسبوع. فالبساطة السطحية للتحرير التعاوني تخفي تعقيدًا استثنائيًا.

التقييم الصريح

التحرير التعاوني من تلك المشكلات التي تكون صعوبتها غير بديهية. المسار السعيد، أي مستخدمان يجريان تعديلات غير متداخلة، ينجح مع تقريبًا أي نهج. أما الحالات الصعبة، مثل التعديلات المتزامنة على المنطقة نفسها، والتحرير دون اتصال مع مزامنة لاحقة، وبنى المستندات المعقدة (الجداول، والقوائم المتداخلة، والكائنات المضمنة)، فتتطلب فهمًا عميقًا للمفاضلات بين OT وCRDTs.

لا يوجد نهج أفضل بشكل مطلق. OT أبسط مع خادم، وأصعب بدونه. وCRDTs تعمل دون خادم، لكنها تحمل تكلفة بيانات وصفية وتراكم شواهد قبور. كلاهما يتطلب هندسة دقيقة تتجاوز الخوارزمية الأساسية، وتلك الهندسة (الحضور، والتخزين، والصلاحيات، والتراجع) غالبًا ما تكون أصعب من مشكلة الاتساق نفسها.

يتجه القطاع ببطء نحو هجين عملي: CRDTs لنموذج البيانات (ضمانات الدمج التلقائي)، مع خادم للتنسيق (الحضور، والصلاحيات، وجمع المهملات). هذا يمنحك أفضل العالمين: اتساق رياضي وبنية تحتية عملية. لكنه يمنحك أيضًا تعقيد العالمين، ولهذا يظل التحرير التعاوني مشكلة صعبة بعد 35 عامًا من البحث.