اگر در حال ساخت اپلیکیشنی هستید که چندین کاربر بهصورت آفلاین روی یک پوشه یا فیلد متنی کار میکنند، احتمالاً با باگ «حذف خاموش دادهها» دستوپنجه نرم کردهاید. در ۹ ژوئن ۲۰۲۶، Loro راهکاری به نام کانتینرهای ادغامپذیر (Mergeable Containers) را برای حل یک تضاد کلاسیک در انواع دادههای تکثیرشده بدون تضاد (CRDT) — که شبیه به یک دفترچه یادداشت مشترک است که همه میتوانند همزمان بدون نیاز به مدیر مرکزی در آن بنویسند — منتشر کرد.
در CRDTهای سنتی، اگر دو کاربر همزمان محتوایی را به یک یادداشت خالی اضافه کنند، آنها اغلب دو کانتینر فرزند متفاوت ایجاد میکنند. هنگام همگامسازی، قانون حل تضاد سیستم یکی را برای نمایش انتخاب میکند و باعث میشود ویرایشات کاربر دیگر ناپدید شود. اگرچه دادهها در تاریخچه باقی میمانند، اما دیگر در وضعیت فعلی سند قابل دسترسی نیستند، که از دید کاربر نهایی شبیه به از دست رفتن داده است. این یک مشکل کلاسیک در CRDTهای شبیه به JSON است، جایی که سیستم تنها میتواند یک کانتینر متنی (Text container) را در یک کلید خاص بازگرداند.
همانطور که در تحلیلهای پیشین ما دربارهی ساختارهای داده در سیستمهای توزیعشده اشاره کردیم، این مشکل تنها مختص Loro نیست. به نقل از تحلیل فنی زیکسوآن چن، مشکلات مشابهی در جوامع Yjs و Automerge نیز دیده شده است. در Yjs، فرزندِ بازنده اغلب بهطور کامل بازنویسی و حذف میشود. در Automerge، فرزند بازنده حفظ شده و میتوان آن را از طریق فراخوانیهای API خاص بازیابی کرد، اما در وضعیت استاندارد سند، نامرئی باقی میماند. در هر دو حالت، از دیدگاه اپلیکیشن، این اتفاق به عنوان حذف داده ظاهر میشود.
ریشه تلاقی شناسهها
در حالت استاندارد، شناسهی کانتینرهای فرزند به عملیات خاصی که آنها را ایجاد کرده گره خورده است. اگر کاربر A و کاربر B همزمان یک کانتینر «متن» در کلید «توضیحات» بسازند، دو شناسهی متفاوت تولید میشود چون شناسهها شامل نام کاربر ایجادکننده هستند. در نتیجه، قانون تضاد Map تصمیم میگیرد کدام شناسه پیروز شود. مشکل این نیست که ادغام لیستها ممکن نیست؛ زیرا وقتی کاربران روی یک لیست یکسان ویرایش میکنند، تغییرات بهطور طبیعی ادغام میشوند. مسئله این است که دو کاربر، دو لیست متفاوت را در یک کلید یکسان از Map ایجاد کردهاند.
پیش از این، تنها راه حل «مقداردهی اولیه عجولانه» (eager initialization) بود؛ یعنی توسعهدهندگان باید تمام کانتینرهای احتمالی فرزند را در همان لحظه ایجاد والد میساختند. برای مثال، اگر هر یادداشت همیشه به یک فیلد متنی نیاز دارد، ایجاد همزمان آن با یادداشت، رقابت در اولین ایجاد را از بین میبرد. اما این روش برای اپلیکیشنهای پویا غیرممکن است، مانند:
- اپلیکیشنهای تقویم که کانتینرهای فرزند را بر اساس تاریخ ایجاد میکنند.
- مهاجرتهای طرحواره (Schema Migrations) که کانتینرهای جدید را بهصورت تنبل (Lazy) به اسناد موجود اضافه میکنند.
- اندیسهای پویا که برای هر کلید تعریفشده توسط کاربر، یک کانتینر فرزند میسازند.
در این موارد، ایجاد بر اساس نیاز (on-demand) طبیعی است و اجتناب از ایجاد همزمان اولین نسخه دشوار است.
سازوکار کانتینرهای ادغامپذیر
Loro این مشکل را با تغییر تعریف هویت کانتینر حل کرده است. اکنون هویت یک فرزند از «موقعیت منطقی» آن در سند میآید، نه از «شناسهی عملیات». این رویکرد مشابه کانتینرهای ریشه در Loro و Yjs است که در آنها نام، همان هویت پایدار است. تا زمانی که چندین کاربر به یک نام ریشه دسترسی داشته باشند، بهطور طبیعی به یک کانتینر منطقی یکسان اشاره میکنند، فارغ از اینکه چه کسی آن را ایجاد کرده است.
این سیستم برای حفظ پایداری از یک نمایش دو لایه استفاده میکند:
۱. شناسهی کانتینر مصنوعی (Synthetic CID)
- شناسهی کانتینر (CID) از شناسهی کانتینر والد، کلید و نوع استخراج میشود.
- از یک شناسهی مصنوعی تحت یک فضای نام داخلی استفاده میکند تا از تداخل با نامهای ریشه که توسط کاربر ساخته شدهاند جلوگیری کند.
- محتوای شناسه از Map والد و کلید مشتق میشود، در حالی که نوع کانتینر در CID باقی میماند.
- این روش به تمام کاربران اجازه میدهد بدون استفاده از عملیات ایجاد، به یک شناسهی فرزند یکسان برسند.
- برای جلوگیری از رشد بازگشتی در نقشههای ادغامپذیر با تودرتویی زیاد، کدگذاری بهگونهای است که شناسههای تودرتو در طول مسیر منطقی بهصورت خطی باقی بمانند.
۲. نشانگر جایگاه Map (Map Slot Marker)
- از آنجا که یک کلید ممکن است انواع مختلفی (مثلاً یک Map در مقابل یک کانتینر Text) را نگه دارد، Loro یک نشانگر فعالساز باینری کوچک در جایگاه والد ذخیره میکند.
- این نشانگر دیدهشدن را کنترل میکند: وقتی کلاینت جایگاه را میخواند، از این نشانگر برای استخراج CID قطعی و ادغامپذیر استفاده میکند.
- این نشانگر به والد، کلید و نوع دقیق خود متصل است تا اگر مقدار باینری به جای دیگری کپی شد، فعالسازی تصادفی رخ ندهد.
- هنگام حذف یک کلید، فقط این نشانگر حذف میشود. وضعیت فرزند ادغامپذیر بلافاصله نابود نمیشود زیرا جایگاه والد دیدهشدن را کنترل میکند، نه تاریخچه ذخیرهشدهی فرزند را. این یعنی فراخوانی مجدد API میتواند همان کانتینر متنی قطعی را دوباره ظاهر کند.
پیادهسازی API و محدودیتها
توسعهدهندگان اکنون میتوانند از APIهای صریحی برای اطمینان از ادغامپذیر بودن فرزند استفاده کنند. در زبان Rust، این کار از طریق متدهای snake-case مانند get_mergeable_text و get_mergeable_map انجام میشود. استفاده از کلمه «get» تعمدی است؛ زیرا این متد فرزند را بازمیگرداند و در صورت نیاز، نشانگری را مینویسد که آن را در آن کلید نمایان کند. این فراخوانیها «همتوان» (Idempotent) هستند.
اگر یک کلید حاوی یک مقدار ساده (scalar) یا یک کانتینر استاندارد باشد، API ادغامپذیر بهجای بازنویسی داده، خطا میدهد. یک مورد ظریف مربوط به تغییر نوع است: اگر یک کاربر درخواست متن ادغامپذیر در یک کلید بدهد و کاربر دیگر درخواست Map ادغامپذیر، قانون تضاد معمولی Map تصمیم میگیرد کدام نوع نمایان باشد. با این حال، وضعیت فرزند ادغامپذیر غیرنمایان همچنان تحت شناسهی قطعیاش حفظ میشود، بنابراین بازگشت به آن نوع میتواند دوباره آن را ظاهر کند.
این رویکرد هزینهای در متادیتا دارد. چون CIDها حاوی اطلاعات مسیر منطقی هستند، مسیرهای عمیقتر و کلیدهای طولانیتر، شناسههای بزرگتری تولید میکنند. Loro در PR #1002 کدگذاری خود را بهروزرسانی کرد تا اطمینان حاصل کند شناسههای Mapهای ادغامپذیر تودرتو بهجای رشد بازگشتی، بهصورت خطی رشد کنند، هرچند باز هم باید از زنجیرههای بسیار عمیق Mapهای ادغامپذیر پرهیز کرد.
سازگاری و امنیت
بر اساس مستندات Loro، این بهروزرسانی محافظهکارانه است تا پروژههای موجود خراب نشوند. رفتار استاندارد / تغییری نکرده و اسناد قدیمی با نسخههای جدید بهدرستی خوانده میشوند. کانتینرهای ادغامپذیر از طریق APIهای جدید معرفی شدهاند بدون اینکه امضای متدهای موجود تغییر کند.
کلاینتهای قدیمی که از کانتینرهای ادغامپذیر پشتیبانی نمیکنند، نشانگر فعالساز را بهعنوان یک مقدار باینری معمولی میبینند، نه به عنوان یک لبهی کانتینر فرزند مصنوعی. آنها میتوانند دادهها را حفظ و همگام کنند، اما فرزند ادغامپذیر را با معنای جدید نمایش نمیدهند. برای جلوگیری از تداخل، اعتبارسنج نام ریشه در Loro اکنون هر نام ریشهای را که با پیشوند داخلی مورد استفاده برای CIDهای ادغامپذیر شروع شود، رد میکند.
این تغییر، فرض بنیادین درباره هویت اشیا در نقشههای همکارانه را عوض میکند: بهجای «چه کسی اول ساخت»، منطق بر این است که «این شیء در کجای ساختار قرار دارد». این امر به توسعهدهندگان اجازه میدهد تا کلیدهای پویا را از همان ابتدا به عنوان منابع مشترک در نظر بگیرند.
چه زمانی از کانتینرهای ادغامپذیر استفاده کنیم؟
اگر در حال مدیریت یک وضعیت سند پیچیده هستید، باید ارزیابی کنید که آیا کانتینرهای فرزند شما قرار است موجودیتهای منحصربهفرد باشند یا جایگاههای منطقی مشترک. از کانتینرهای ادغامپذیر در موارد زیر استفاده کنید:
- زمانی که کلید پویا است یا بهصورت تنبل (lazy) ایجاد میشود.
- زمانی که کاربران مختلف ممکن است یک فرزند یکسان را در حالت آفلاین مقداردهی اولیه کنند.
- زمانی که فرزند باید برای همه به عنوان یک Text، List، Map، Tree یا Counter مشترک عمل کند.
- زمانی که حذف کلید باید باعث پنهان شدن فرزند شود، بدون اینکه تاریخچه داخلی آن بلافاصله نابود گردد.
در موارد زیر از رفتار استاندارد / استفاده کنید:
- زمانی که هر ایجاد باید یک شیء فرزند متمایز تولید کند.
- زمانی که جایگاه والد باید دقیقاً به کانتینری اشاره کند که توسط آن عملیات خاص ساخته شده است.
- زمانی که شما در حال مدلسازی «جایگزینی» هستید و نه «مقداردهی اولیه مشترک».
برای مشاهده عملی این سازوکار، توسعهدهندگان میتوانند اسکریپتهای بازتولید موجود در مستندات Loro (تست شده روی Node 18+ با ESM) را اجرا کنند تا رفتار کانتینرهای ادغامپذیر را با رفتار پیشفرض Yjs و Automerge مقایسه نمایند. این پیادهسازی از طریق بحثهای طراحی گسترده و تلاشهای اجرایی الکسیس ویلیامز از Synapdeck میسر شد.
گام بعدی شما
- اگر از CRDTها برای مدیریت وضعیت اسناد پیچیده استفاده میکنید، بررسی کنید آیا کانتینرهای شما باید موجودیتهای منحصربهفرد باشند یا جایگاههای منطقی مشترک.
- برای تست تفاوت رفتار Loro با Yjs و Automerge، اسکریپتهای بازتولید موجود در مستندات Loro را روی Node 18+ اجرا کنید.
- در طراحی ساختار داده، از زنجیرههای بسیار عمیق Mapهای ادغامپذیر پرهیز کنید تا حجم متادیتا کنترل شود.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو