کلاینت وب
کلاینت وب یک اپ React در apps/calendar-web/ است — نه خروجی Wasm آزمایشیِ KMP. مثل اندروید، آفلایناول کار میکند: هر چهار موجودیت ابتدا در IndexedDB (با Dexie) خوانده و نوشته میشوند و بعداً با API همگام میشوند. محدودیت فعلی رابط کاربری وب: ساخت یادآور و زیروظیفه وجود ندارد؛ آن دو در اندروید ساخته میشوند.
نقشهٔ کد
مسیرها نسبت به apps/calendar-web/src/:
| مسیر | مسئولیت |
|---|---|
lib/db/schema.ts | schema فعلی ۵: چهار موجودیت، outbox حسابمحور، cursor، canonical و مالک DB |
features/*/local.ts | ثبت ردیف و queueLocalEdit در یک تراکنش؛ سپس kick موتور sync |
lib/sync/syncEngine.ts | push/pull، بازیابی ارسال نیمهکاره، بازکلید تقویم پیشفرض |
lib/sync/syncApi.ts و leaderElection.ts | پیادهسازی پروتکل ۲ و سرصفحهٔ X-Sync-Protocol: 2؛ انتخاب تب رهبر با Web Locks |
features/auth/AuthContext.tsx و ProtectedRoute.tsx | بررسی نشست، مالکیت داده و تصمیم خروج/تعویض حساب |
features/sync/ | بنر مشکلات sync و ConflictList/hooks.ts برای حل تعارضها |
lib/apiClient.ts و tokenStore.ts | Bearer، refresh تکپروازی، نسل احراز هویت و توکنهای localStorage |
DTOها از @taghvimam/contracts میآیند و VITE_API_URL مبدأ API است. فرمانهای اجرا و تست در تستها.
تغییر و ایمنی حساب
چرخهٔ sync در وب همان قرارداد همگامسازی است؛ نکات پیادهسازی:
- حساب بدون cursor اول pull کامل میگیرد؛ دور عادی
push → pull → refreshConflictsاست. - تب رهبر (با Web Locks انتخاب میشود) دورها را با این triggerها اجرا میکند: آنلاینشدن، visible شدن تب، تایمر ۶۰ ثانیه و kick ویرایش کاربر.
- پاسخی که به آیتمی نمیخورد صف را مصرف نمیکند؛ اگر پاسخ push گم شود، همان درخواست با همان شناسه/محتوا نگه داشته و بعداً تکرار میشود تا رسیدِ سرور جواب بدهد.
- pull صفحه و cursor را اتمیک ذخیره میکند؛ پیشنویس باز کاربر با
cursorResetپاک نمیشود.
بارگذاری و نشست:
- بارگذاری اولیه،
/meو مالک DB را تأیید میکند و فقط بعد از آن داده نمایش یا sync میشود. - قطعی موقت فقط «تلاش مجدد» است؛ ابطال نشست یعنی ورود دوباره — بدون پاککردن DB محلی.
- تعویض حساب A→B بدون تصمیم صریح (خروجی یا دورریزِ کار A) انجام نمیشود؛ دادهٔ A به B نسبت داده نمیشود. fence نسل/مالک، پاسخ دیررس حساب قبلی را رد میکند.
- خروج وقتی کار محلی هست، سه راه دارد: ارسال کامل، خروجی JSON و بعد دورریز، یا دورریز با تأیید دوم. اگر کار جدیدی بعد از آن دیالوگ انجام شود، تصمیم تازه لازم است. بدون کار محلی، خروج مستقیم است.
- تب قدیمیِ بدون سرصفحهٔ پروتکل،
426میگیرد و outboxش محفوظ میماند؛ راهحل، بارگذاری مجدد صفحه است، نه حذف IndexedDB.
مشکل همگامسازی را چطور حل کنم؟
بنر مشکلات، خطاهای outbox و تعارضهای تاریخی را در یک دیالوگ فهرست میکند؛ بستن پنجره هیچ تصمیمی نمیگیرد و چیزی پاک نمیکند.
| مشکل | انتخاب موجود |
|---|---|
| اختلاف نسخه (stale) | نسخهٔ سرور، یا نسخهٔ خودم با base و شناسهٔ تازه |
| حذف در سرور | بازگردانی صریح با تأیید دوم؛ رکورد خودکار زنده نمیشود |
invalid_item | اصلاح فیلدهای پیشنهادی و ارسال دوباره |
unknown_parent / parent_deleted | انتخاب تقویم زنده و انتقال رکورد |
has_live_dependents | دیدن وابستگان و تعیین تکلیف آنها اول |
identity_mismatch | شناسهٔ تازه برای ارسال همان تغییر |
| تعارض تاریخی | رد یا بازگردانی از تاریخچه؛ UI حداکثر ۵۰ مورد اول را نشان میدهد |
مهاجرت schema قدیمی، outbox و cursor را حسابمحور میکند؛ دادهٔ بدون مالک تا تأیید مالکیت ارسال نمیشود.
تستهای راهنمای همین رفتارها: lib/sync/*.test.ts، lib/db/schema.upgrade.test.ts و features/auth/AuthContext.test.tsx.