همگامسازی (آفلاین)
این صفحه قرارداد کامل همگامسازی بین کلاینتهای تقویمم (وب و اندر وید) و API است. سیستم آفلایناول است: کاربر هر تغییری را ابتدا در پایگاه دادهٔ محلی خود ذخیره میکند و کلاینت بعداً آن را به سرور میفرستد. بنابراین «ذخیره شد» در رابط کاربری هیچوقت به معنی «سرور پذیرفت» نیست.
سه مفهوم پایه وجود دارد که بقیهٔ صفحه بر آنها بنا میشود:
- نیت (intent): هر تغییر محلی که هنوز از سرور پاسخ نگرفته، یک نیت است. نیتها در صفی به نام
outboxنگه داشته میشوند تا ارسال شوند. - canonical: آخرین وضعیت تأییدشدهٔ سرور از یک ردیف. هر چه سرور در پاسخ push برمیگرداند یا pull میفرستد canonical است؛ پیشنویس محلی کاربر میتواند با آن فرق داشته باشد و تا وقتی push نشده، فقط محلی است.
- رسید (receipt): سرور نتیجهٔ هر عملیات را بهصورت ماندگار نگه میدارد تا اگر پاسخ گم شد، تکرار همان عملیات نتیجهٔ اصلی را بدون نوشتن دوباره برگرداند. جزئیات در بخش رسید.
نکته مهم: یک نیت را فقط زمانی از
outboxحذف کنید که نتیجهٔ همانmutationIdرا در پاسخ push دریافت کردهاید. پاسخpullهیچوقت تأییدکنندهٔ عملیاتهای push نیست، وHTTP 200هم به معنی موفقیت همهٔ آیتمهای داخل batch نیست — هر آیتم ن تیجهٔ مستقل خودش را دارد.
همهٔ مسیرهای این صفحه به Bearer token حساب نیاز دارند (به احراز هویت مراجعه کنید).
| مسیر | کار |
|---|---|
POST /api/sync/push | ارسال تغییرات محلی؛ سرصفحهٔ X-Sync-Protocol: 2 اجباری است |
GET /api/sync/changes?limit=500 | دریافت تغییرات سرور؛ صفحهٔ بعدی با cursor موجود در پاسخ |
GET /api/sync/conflicts | فهرست تعارضهای تاریخی نسل قبل — نه تعارضهای پروتکل ۲ |
POST /api/sync/conflicts/:id/resolve | تصمیمگیری دربارهٔ یک تعارض تاریخی |
شناسهها و مفاهیم پایه
uid: هویت پایدار یک ردیف که کلاینت هنگام ساخت میسازد (UUIDv7) و در همهٔ دستگاهها یکی میماند. ارجاعهای بین ردیفها (مثلcalendarUidیک رویداد) هم با uid انجام میشود، نه با شناسهٔ عددی داخلی دیتابیس.mutationId: شناسهٔ یکتای هر عملیات در محدودهٔ یک حساب. هر تغییر محلی جدید باید mutationId تازه بگیرد؛ تکرار همین شناسه بعداً همان نقش «تلاش مجدد همان عملیات» را دارد.version: شمارهٔ نسخهٔ ردیف در سرور، بهصورت رشتهٔ دهدهی (مثلاً"23"). هر نوشتن سرور نسخه را از یک دنبالهٔ سراسری بالا میبرد. هرگز آن را عدد JSON یا timestamp فرض نکنید.baseVersion: نسخهای از ردیف که کلاینت تغییر خود را روی آن بنا کرده است. سرور با مقایسهٔbaseVersionباversionفعلی تشخیص میدهد که آیا تغییر قابل اعمال است یا دیر رسیده (stale).deleted: زمان حذف نرم بهصورت ISO، یاnullبرای ردیف زنده. ردیفِ حذفشدهٔ نرم «tombstone» (نشان حذف) نامیده میشود و همچنان با pull برگردانده میشود.- cursor: رشتهٔ مبهم و امضاشده که پیشرفت pull را برای هر نوع موجودیت نگه میدارد. محتوایش را parse یا ساختنش تلاش نکنید؛ فقط همان چیزی که سرور داده را ذخیره و پس بفرستید.
ساعت دستگاه هیچوقت معیار تازگی داده نیست؛ فقط version (برای ردیف) و cursor (برای پیشرفت pull) مبنای سروریاند.
Push
درخواست push با Content-Type: application/json، Bearer token و سرصفحهٔ دقیق X-Sync-Protocol: 2 ارسال میشود. بدون این سرصفحه (یا با مقدار دیگری) سرور پیش از هر اعتبارسنجی و هر نوشتن، با 426 جواب میدهد.
نمونهٔ زیر یک patch روی وظیفهٔ موجود با نسخهٔ ۲۳ است — فقط title عوض میشود و بقیهٔ فیلدها دستنخورده میمانند:
{
"tasks": [
{
"mutationId": "019a3ec0-0000-7000-8000-000000000001",
"uid": "019a3ec0-0000-7000-8000-000000000002",
"op": "upsert",
"baseVersion": "23",
"title": "خرید نان"
}
]
}
معنای فیلدهای patch این است:
- فیلدی که در JSON نیامده، مقدار فعلی سرور را حفظ میکند.
nullفقط برای فیلدهای nullable معنا دارد و مقدار آن فیلد را پاک میکند. پس غیبتِ فیلد باnullفرق دارد.createdAtاختیاری است و فقط هنگام ساخت ردیف جدید اثر دارد؛ در ویرایش نادیده گرفته میشود.updatedAtوdeletedفیلدهای ورودی نیستند؛ سرور خودش آنها را مدیریت میکند.- سه عمل ممکن است:
upsert(ساخت یا ویرایش)،delete(حذف نرم) وrestore(احیای صریح یک tombstone).
ترتیب پردازش و استقلال آیتمها
سرور همیشه آرایهها را به ترتیب ثابت calendars → events → tasks → reminders پردازش میکند و داخل هر آرایه، ترتیب ورودی کلاینت را حفظ میکند. هر آیتم در تراکنش مستقل خودش اعمال میشود؛ نتیجهٔ یک آیتم، سرنوشت آیتم بعدی را تعیین نمیکند.
نتیجهٔ عملی برای کلاینت:
- ساخت: والد باید قبل از فرزند ارسال شود — از جمله والدِ یک زیروظیفه. چون تقویمها قبل از رویدادها و رویدادها قبل از وظیفهها پردازش میشوند، میتوانید والد و فر زند را در همان یک batch بفرستید.
- حذف: برعکس — فرزند باید قبل از والد حذف شود. چون ترتیب آرایهها در سرور ثابت است، معمولاً درستترین کار این است که فرزندان را در push قبلی حذف و تأییدشان را بگیرید، بعد حذف والد را بفرستید.
- شکست والد (مثلاً بهدلیل
unknown_parent) میتواند آیتمهای وابستهٔ بعدی را هم به خطا بکشاند.
اعتبارسنجی مقادیر
تاریخها باید قابل parse باشند. order وظیفه عدد صحیح signed Int32 است، value یادآور عدد صحیح مثبت Int32، و providerId عدد صحیح در بازهٔ امن JSON (±9007199254740991). مقدار نامعتبر قبل از ذخیره به نتیجهٔ error همان آیتم تبدیل میشود — نه خطای 500 سرور و نه رد خودکار کل batch. در مقابل، خرابی خود پاکت درخواست (envelope) همچنان کل درخواست را با 400 رد میکند؛ تفکیک این دو در بخش خطاها آمده است.
پاسخ push
پاسخ push برای هر آیتم ارسالی دقیقاً یک نتیجه دارد. نتیجهها را با mutationId تطبیق دهید، نه فقط با uid — چون در برخی حالتها (مثل ادغام تقویم پیشفرض) uid مؤثر با uid درخواستی فرق میکند.
نمونهٔ کامل یک نتیجهٔ stale، برای وقتی که وظیفهٔ نمونهٔ بالا دیگر در سرور وجود ندارد:
{
"success": true,
"data": {
"calendars": [],
"events": [],
"tasks": [
{
"mutationId": "019a3ec0-0000-7000-8000-000000000001",
"requestedUid": "019a3ec0-0000-7000-8000-000000000002",
"effectiveUid": "019a3ec0-0000-7000-8000-000000000002",
"status": "stale",
"canonical": null
}
],
"reminders": [],
"conflicts": []
}
}
فیلد canonical در نتیجه:
- در
applied: ردیفی که واقعاً نوشته شده. - در
stale: وضعیت فعلی سرور که با نیت شما واگرا شده («واگرا» یعنی سرور از نسخهای که شما مبنا گذاشتهاید جلوتر رفته). - در
error: ممکن است مقدار داشته باشد یاnullباشد؛ به تهیبودن آن تکیه نکنید.
| وضعیت | معنی | کاری که کلاینت باید انجام دهد |
|---|---|---|
applied | سرور عملیات را پذیرفت و نوشت | نتیجه را با mutationId به نیت متناظر در outbox وصل کنید، canonical را در دیتابیس محلی ذخیره کنید و فقط همان یک نیت را از outbox حذف کنید. اگر بعد از آن نیت، ویرایش جدیدتری روی همان ردیف در outbox هست، آنها را حذف نکنید — مال همانها هنوز پاسخ نگرفتهاند. |
stale | پیششرط نسخه برقرار نبود و چیزی نوشته نشد | نیت را نگه دارید (حذفش نکنید)، آن را با canonical برگشتی مقایسه کنید و بر اساس تصمیم کاربر یا سیاست برنامه، عملیات جدیدی با baseVersion و mutationId تازه بسازید. |
error | سرور آیتم را رد کرد؛ فیلد reason علت را میگوید | علت را برطرف کنید و برای تلاش جدید mutationId تازه بسازید — تکرار همان شناسه، نتیجهٔ قدیمی را تکرار میکند (بخش رسید). |
پاسخهای applied/stale/error همگی داخل HTTP 200 برمیگردند؛ موفقیت سطح HTTP با موفقیت آیتم یکی نیست.
نسخه و tombstone
جدول زیر نتیجهٔ ترکیب وضعیت فعلی ردیف در سرور با درخواست کلاینت را نشان میدهد. «base برابر» یعنی baseVersion دقیقاً version فعلی سرور باشد:
| وضعیت سرور | در خواست | نتیجه |
|---|---|---|
| غایب | upsert با baseVersion=null | ساخت ردیف جدید (applied) |
| غایب | upsert با base غیرتهی، یا delete/restore | stale، بدون canonical |
| زنده | upsert یا delete با base برابر | اعمال با رعایت قیود وابستگی |
| موجود | base تهی یا نابرابر | stale، بدون نوشتن |
| tombstone | upsert با هر base | زنده نمیشود؛ stale |
| tombstone | restore با base برابر نسخهٔ حذف | احیای صریح همراه patch؛ والد باید زنده باشد |
| tombstone | delete با base برابر | applied بیاثر (حذفِ حذفشده) |
| زنده | restore | stale |
هشدار: صرفاً جایگزینی
baseVersionبا نسخهٔ جدید و ارسال دوباره، یعنی تصمیم به بازنویسی دادهای که دستگاه دیگری (یا سرور) نوشته است. قبل از چنین کاری، نیت محلی وcanonicalرا مقایسه کنید و در رابط کاربر، تصمیم آگاهانه بگیرید. همچنین احیای یک رکورد حذفشده همیشهrestoreصریح میخواهد؛upsertروی tombstone ردیف را زنده نمیکند.