ما وراء المزامنة: هندسة مساحات عمل تعاونية في الوقت الفعلي باستخدام CRDTs و Expo
توقف عن محاربة ظروف السباق (race conditions) ومنطق 'Last Write Wins' في تطبيقات الجوال الخاصة بك. يستعرض هذا المقال كيف قمت بدمج CRDTs مع Expo لبناء بيئات تعاونية عالية الأداء وجاهزة للعمل دون اتصال بالإنترنت.

ما وراء المزامنة: هندسة مساحات عمل تعاونية في الوقت الفعلي باستخدام CRDTs و Expo
لسنوات، كنا نخدع أنفسنا بشأن مفهوم "الوقت الفعلي". لقد بنينا تطبيقات تعتمد على WebSockets أساسية مع الكثير من التوقعات بأن استراتيجية "Last Write Wins" (LWW) لن تدمر بيانات المستخدم عندما يقوم شخصان بتعديل حقل ما في نفس الوقت عبر اتصال 5G متقطع.
لقد قضيت الأشهر القليلة الماضية في إعادة بناء مساحة عمل تعاونية معقدة في تطبيق جوال يعتمد على Expo. لم يكن الاختراق التقني في بناء سيرفر socket أسرع، بل كان في الابتعاد عن مزامنة الحالة (state synchronization) والتوجه نحو تقارب الحالة (state convergence) باستخدام CRDTs (Conflict-free Replicated Data Types).
إليك كيف تعاملت مع الهيكلية التقنية والعقبات التي ذللتها في الطريق.
التحول الجوهري: من المزامنة إلى التقارب
في إعدادات REST أو GraphQL القياسية، تكون واجهة المستخدم (UI) مجرد انعكاس لقاعدة بيانات مركزية. في البيئات التعاونية، يخلق هذا تأخراً هائلاً بين النية (intent) والظهور (visibility). إذا كنت في قطار مع زمن وصول (latency) عالٍ، فإن الـ "optimistic update" الخاص بي سيتعارض في النهاية مع تحديثك، وسيتعين على السيرفر اختيار فائز واحد.
تغير CRDTs قواعد اللعبة؛ فهي تسمح لنسخ متعددة (أجهزة) بأن تُحدَّث بشكل مستقل ومتزامن دون الحاجة لتنسيق مسبق. تضمن بنية البيانات نفسها أنه بمجرد أن ترى جميع النسخ نفس مجموعة التحديثات، فإنها ستصل إلى نفس الحالة تماماً. لا حاجة لوسيط مركزي هنا.
التقنيات المستخدمة: Expo + Yjs
لهذا المشروع، اخترت Yjs بسبب نمطيته (modularity) وأدائه العالي. بينما يلجأ العديد من المطورين إلى Automerge، وجدت أن الـ binary encoding في Yjs أخف بكثير بالنسبة لأجهزة الجوال، حيث تعتبر قيود الذاكرة واستهلاك البطارية أمراً بالغ الأهمية.
إعداد المستند المشترك (Shared Document)
في تطبيق Expo، ستحتاج إلى جعل حالة CRDT تعيش خارج دورة رندرة (render cycle) React القياسية لمنع اختناقات الأداء. قمت بلف منطق Yjs في custom hook يدير دورة حياة Y.Doc.
تحدي الجوال: الأداء والتكامل مع React
أكبر فخ واجهته كان إعادة رندرة شاشة الجوال بالكامل في كل مرة يقوم فيها مستخدم عن بُعد بكتابة حرف واحد. في مستند يعمل عليه 10 أشخاص، ستنهار الـ FlatList الخاصة بك.
حللت ذلك باستخدام Selective Subscriptions. بدلاً من دفع كامل الـ Y.Doc إلى حالة React، استخدمت نهج الـ "Proxy". مكتبات مثل @syncedstore/core رائعة، ولكن للأداء الخام في Expo، فضلت استخدام valtio أو نمط event-emitter بسيط لتحديث المكون (component) المحدد الذي يحتاج إلى التغيير فقط.
نصيحة: تجنب حلقة "التحديث المزدوج"
عند تحديث حالة مشتركة، لا تريد إعادة تشغيل منطق التغيير من مدخلاتك الخاصة. يعالج Yjs ذلك عبر transaction.origin.
من خلال وسم (tagging) معاملاتك، يمكن للمراقبين (observers) تجاهل التحديثات التي نشأت من المستخدم المحلي، مما يمنع حدوث اضطراب (jitter) غير ضروري في واجهة المستخدم.
ميزة "أولوية العمل دون اتصال" لم تعد رفاهية بل مطلباً
جمال استخدام CRDTs في تطبيق Expo هو أن دعم العمل دون اتصال بالإنترنت (offline support) يأتي مجاناً تقريباً. عندما يفقد HocuspocusProvider (أو أي مزود Yjs) الاتصال، يستمر الـ Y.Doc المحلي في العمل.
لقد قمت بدمج IndexedDB (عبر y-indexeddb) على الويب و SQLite على نسخة Expo الأصلية (native) لحفظ سجلات التحديثات الثنائية (binary update logs) محلياً. عند إعادة فتح التطبيق، يقوم بتحميل الـ binary blob، وتطبيقه على المستند، ويقوم المزود تلقائياً بمزامنة الفوارق (diffs) فقط مع السيرفر بمجرد استعادة الاتصال.
دروس من الميدان
- Binary عبر JSON: انقل الحالة دائماً كـ
Uint8Array. معالجتها أسرع بكثير على معالجات الجوال مقارنة بأشجار JSON الضخمة. - تواجد المؤشر (Cursor Presence): لا تخزن البيانات المؤقتة مثل مواقع المؤشرات في مستند CRDT نفسه. استخدم Awareness API؛ فهي تتعالج عملية تنظيف "خروج المستخدم من التطبيق" تلقائياً دون تضخيم تاريخ المستند.
- سقف الذاكرة: يمكن لمستندات CRDT الكبيرة أن تنمو بشكل مفرط. يجب عليك دورياً عمل "compact" للمستند على السيرفر لدمج تاريخ التحديثات، وإلا فإن المزامنة الأولية لعميل جوال جديد قد تستغرق ثوانٍ.
كلمات أخيرة
هندسة مساحة عمل تعاونية في Expo لا تتعلق فقط بـ real-time sockets بعد الآن؛ بل تتعلق بالأنظمة الموزعة (distributed systems). من خلال إسناد منطق حل النزاعات إلى CRDTs، يمكننا التركيز على بناء واجهات سريعة وسلسة تشعر المستخدم بأنها تعمل محلياً حتى لو كان على بعد أميال من أقرب برج تغطية مستقر.
إذا كنت لا تزال تبني بمنطق "Last Write Wins"، فقد حان الوقت لإعادة التقييم. الأدوات أصبحت متوفرة، وعبء الأداء ضئيل جداً مقارنة بالدفعة الهائلة التي ستحصل عليها تجربة المستخدم.