- بواسطة x32x01 ||
Sync في تطبيقات Cross-Platform 🔄 ازاي تخليه شغال صح Offline-First من غير لخبطة؟
لو بتعمل Cross-Platform App (Flutter / React Native / .NET MAUI… إلخ) فـ موضوع الـ Sync ده حرفيًا من أهم الحاجات اللي لازم تتهندل صح ✅لأنك أكيد اتلغبطت قبل كده:
إيه اللي اتعمل Local ولسه محتاج يترفع للسيرفر؟ 🤯
الهدف الأساسي من الـ Sync إن الابلكيشن يبقى Offline-First:
تستخدمه من غير نت عادي… ولما النت يرجع كل حاجة تتزامن تلقائي 😎📱
يعني إيه Offline-First وليه مهم؟ 🌍
Offline-First معناه ببساطة:- اليوزر يقدر يضيف/يعدل/يمسح وهو مفيش نت
- التغييرات تتحفظ Local
- أول ما النت يشتغل، يحصل Sync تلقائي أو بزرار Push/Pull 🔄
- تجربة استخدام أحسن بكتير ✨
- مفيش فقدان بيانات
- مفيش Errors بسبب الشبكة كل شوية
الفكرة اللي ماشية بيها معظم apps المحترمة: Local Queue ✅
خلينا نقولها بصراحة:أحسن Approach منتشر حاليًا هو إنك تعمل Queue محلية (Op Queue) لكل اللي اليوزر بيعمله.
يعني أي Action زي:
- Add
- Update
- Delete
- حتى View (لو بتتبع analytics محلي)
- status = pending
- ومعاه localTimestamp
Flow بسيط للـ Sync (Push/Pull) 🔁
تعالى نمشيها خطوة خطوة، بشكل مرتب ومفهوم:1) اليوزر يعمل تغيير → يتسجل Local 🗃️
أي تغيير يتسجل في Local DB كـ op/change item:- type: create / update / delete
- status: pending
- localTimestamp: وقت التنفيذ على الجهاز
- tempId أو sequence
عشان تتابع الأحداث بسهولة وتربط العمليات ببعض.
2) كل العمليات تروح على Op Queue 📥
كل اللي بيحصل من أي منصة (موبايل/ويب/ديسكتوب) بيتجمع في queue محلية.دي اللي هتستخدمها بعدين في Push للسيرفر.
3) لازم العمليات تبقى Idempotent 🔐
دي نقطة ذهبية ⭐أي عملية في الـ Queue لازم تكون Idempotent (يعني لو اتبعتت مرتين بالغلط متعملش كارثة).
✅ الحل: تستخدم
idempotencyKeyمهم جدًا لو بتتعامل مع: Payments 💳 - Booking - أي خدمة خارجية
4) أول ما النت يبقى متاح → Push على دفعات 🚀
بدل ما تبعت كل حاجة مرة واحدة، ابعت Batch:مثلاً:
- 50 ops
- أو X KB
POST /sync/push5) السيرفر يعمل Upsert ويرد بحالة كل Op 🧠
السيرفر يستقبل الـ batch ويعمل:- upsert لكل op
- باستخدام idempotencyKey أو unique constraint
- accepted ✅
- conflict ⚠️
- rejected ❌
6) السيرفر يرجّع IDs وتواريخ “Authoritative” ⏱️
بعد ما السيرفر يقبل العملية، لازم يرجّع:- serverId
- serverTimestamp
وساعتها الابلكيشن يعمل:
- status = synced
- syncedAt = serverTimestamp
- تحديث الـ local record بالـ serverId
Pull بالدلتا باستخدام lastSyncAt 📌
بدل ما تسحب الداتا كلها كل مرة… اعملها صح:- خزن lastSyncAt
- واعمل Pull بالداتا اللي اتغيرت بعده بس
GET /sync/pull?since=lastSyncAtده بيخليك:
- أسرع ⚡
- أقل استهلاك داتا
- أقل Load على السيرفر
Retries + Backoff عشان الشبكة مش دايمًا جميلة 😅
حصل فشل في Push؟ عادي… بس متعملش Spam للسيرفر.استخدم Exponential Backoff:
- حاول بعد 1 ثانية
- بعد 2
- بعد 4
- بعد 8…
JavaScript:
let retry = 0;
async function syncWithBackoff(fn) {
while (retry < 5) {
try {
return await fn();
} catch (e) {
const delay = Math.pow(2, retry) * 1000;
await new Promise(r => setTimeout(r, delay));
retry++;
}
}
throw new Error("Sync failed after retries");
} التعامل مع الـ Conflicts من غير صداع 🤝
الـ Conflict بيحصل لما:- نفس العنصر اتعدل Local
- واتعدل على السيرفر قبل ما تعمل Push
- Last write wins (سهل بس مش دايمًا صح)
- Merge (أفضل بس محتاج تصميم)
- Prompt user (لو بيانات حساسة)
ملخص سريع تمشي عليه في أي App ✅
🔹 خزن كل حاجة في Queue محلية🔹 اعمل Push على دفعات
🔹 استقبل mappings + timestamps من السيرفر
🔹 حدث الـ Local records لـ synced
🔹 اعمل Pull بالدلتا من lastSyncAt
🔹 خلي ops idempotent + retries ب backoff
🔹 حل الـ merge خصوصًا وقت signup:
ارفع اللي عندك Local الأول… وبعدها اسحب اللي على السيرفر 🔄