ما وراء الـ WebSocket: توسيع نطاق التعاون الفوري في Expo باستخدام الـ CRDTs
توقف عن الاعتماد على منطق "البقاء للأحدث" (Last Write Wins) وأحداث الـ Socket الهشة. أستعرض هنا كيف تمكن الـ CRDTs من بناء تجارب قوية تدعم العمل المحلي أولاً (local-first) ومتعددة المستخدمين في React Native و Expo.

لسنوات طويلة، كان خياري المفضل للتعاون الفوري بسيطاً: تشغيل خادم Socket.io، وإرسال حدث (event) عندما يكتب المستخدم، والدعاء بأن تنتهي عملية قاعدة البيانات قبل أن يضغط المستخدم التالي على 'حفظ'.
ولكن مع بدئنا في بناء لوحات تحكم ومحررات أكثر تعقيداً في Expo، أصبحت الفجوات في نموذج "الحقيقة المركزية" (Centralized Truth) من المستحيل تجاهلها. حالات السباق (Race conditions)، وفقدان البيانات المزعج بسبب منطق "البقاء للأحدث" (Last Write Wins)، وكابوس التعامل مع المزامنة بدون اتصال (offline sync)، جعلتني أدرك أننا كنا نسلك المسار الخاطئ.
إذا كنت تبني برمجيات تعاونية اليوم، فأنت بحاجة للذهاب إلى ما وراء الـ WebSocket. أنت بحاجة إلى CRDTs (Conflict-free Replicated Data Types).
المشكلة في نهج "الـ Socket فقط"
عندما تعتمد كلياً على WebSockets لبث الحالة (state)، فأنت في الواقع تبني غلافاً رقيقاً فوق نظام متزامن. إذا قام المستخدم (أ) والمستخدم (ب) بتعديل نفس الفقرة في آن واحد، يتعين على الخادم أن يلعب دور الحَكم.
في بيئة تطبيقات الجوال مثل Expo، يكون هذا الأمر أكثر خطورة. زمن الانتقال العالي في شبكات الـ 5G وفقدان الإشارة المتقطع يعني أن المستخدمين يكونون "خارج التغطية" لمدة ثانيتين أو ثلاث في المرة الواحدة. نهج الـ WebSocket التقليدي يفشل هنا لأنه يفترض وجود اتصال دائم. عندما يعاود المستخدم الاتصال، كيف تدمج تغييراته التي تعود لـ 5 ثوانٍ مضت مع الحالة الحالية دون تدمير عمل شخص آخر؟
دخول الـ CRDTs: تصميم يعتمد على "المحلي أولاً"
الـ CRDTs هي هياكل بيانات يمكن تحديثها بشكل مستقل وبالتزامن دون الحاجة لمنسق مركزي. وهي تضمن أنه إذا تلقى جهازان نفس المجموعة من التحديثات (حتى بترتيب مختلف)، فسيصلان إلى الحالة نفسها.
من خلال تجربتي، الانتقال إلى بنية تعتمد على الـ CRDT غير عقليتنا من "إرسال التحديثات إلى خادم" إلى "مزامنة قاعدة بيانات محلية". هذا هو جوهر البرمجيات التي تعتمد على المبدأ المحلي أولاً (Local-First).
المزيج التقني: Expo + Yjs + Hocuspocus
في مشروع حديث، قمنا بدمج Yjs (مكتبة CRDT عالية الأداء) في تطبيق Expo. إليك البنية التي اعتمدناها:
- الواجهة الأمامية (Expo/React Native): يتولى Yjs إدارة حالة المستند المشترك.
- الحفظ (Persistence): استخدام
expo-sqliteلتخزين "كتل التحديث" (update blocks) الثنائية محلياً ليعمل التطبيق فوراً عند التشغيل. - المزامنة (Sync): مزود WebSocket (استخدمنا Hocuspocus) الذي يقوم ببساطة بنقل التحديثات الثنائية بين الأجهزة.
تنفيذ Collaborative Hook
إليك نسخة مبسطة لكيفية هيكلة الـ shared state hook. بدلاً من useState التقليدي، نستخدم الـ CRDT كمصدر للحقيقة.
طفرة الأداء: التعامل مع التحديثات الثنائية في Expo
أحد التحديات التي واجهتها كان الأداء. الـ CRDTs مثل Yjs تتواصل عبر تحديثات ثنائية بصيغة Uint8Array. استخدام JSON التقليدي عبر الـ WebSocket يعتبر ثقيلاً جداً لهذا الغرض. باستخدام مزود Hocuspocus، تمكنا من الحفاظ على حجم البيانات صغيراً جداً، وهو أمر بالغ الأهمية لعمر بطارية الهاتف واستهلاك البيانات.
علاوة على ذلك، استخدمنا y-protocols/awareness للتعامل مع حالات "من يتواجد في المستند الآن" (المؤشرات، التواجد) دون لمس قاعدة البيانات على الإطلاق. هذه "الحالة العابرة" (ephemeral state) تعيش فقط في الذاكرة وتتزامن بين الأجهزة تلقائياً.
لماذا يهم هذا لمسيرتك المهنية
بصفتنا مهندسين كبار، لا ينبغي لنا فقط بناء تطبيقات تعمل؛ بل يجب أن نبني تطبيقات مرنة (resilient). الانتقال إلى CRDTs في تطبيقات Expo الخاصة بنا سمح لنا بـ:
- إلغاء أزرار "الحفظ". كل شيء تفاعلي وتعاوني بطبيعته.
- دعم أصيل للعمل بدون اتصال (Offline Support). يمكن للمستخدمين التعديل على متن طائرة، وعند الهبوط، يتم الدمج رياضياً دون الحاجة لنوافذ حل النزاعات (conflict dialogs).
- تقليل الضغط على الخادم. يصبح الخادم مجرد "مُرحّل" (dumb relay) بدلاً من كونه محرك حوسبة مكلف لتسوية الحالات.
الخاتمة
الـ WebSockets هي طبقة نقل (transport layer)، وليست استراتيجية للتزامن. إذا كنت تبني تطبيقاً مثل Figma للجوال أو مدير مهام تعاوني في Expo، توقف عن خوض معركة "البقاء للأحدث". تبنَّ منطق الـ CRDTs، واستفد من أداء Yjs، وامنح مستخدميك تجربة تبدو أصيلة وفورية حقاً.
في مقالي القادم، سأتعمق في تحسين expo-sqlite لحفظ الـ CRDTs — لأن إبقاء تلك البيانات على الجهاز هو القطعة الأخيرة في أحجية الـ local-first.