پشت صحنه سینک ریلتایم در Plain
خلاصهٔ کاملتر
نویسنده با یه مثال آشنا شروع میکنه: یه Pull Request رو تو GitHub باز میکنی و دستت ناخودآگاه میره سراغ رفرش، چون از بیرون نمیتونی بفهمی کدوم بخش صفحه زندهست و کدوم یه عکس لحظهٔ رندره. به گفتهٔ نویسنده این مشکل GitHub نیست، نتیجهٔ اینه که ریلتایم تیکهتیکه و سطحبهسطح روی یه اپ قدیمیِ request/response چسبونده شده؛ یه جا websocket، یه جا poll، یه جا بنر «این صفحه بهروز شده، رفرش کن».
ایدهٔ اصلی Plain اینه که بهجای صدا زدن endpoint، به state اشتراک (subscribe) میشی. لیست issueهای روی صفحهت در واقع یه کوئری روی یه استور محلیه که سرور براش بهروز نگهش میداره؛ هر ردیفی هرجا عوض شه، قبل از اینکه فکر رفرش به سرت بزنه تو استور تو هم هست. چون هر ویو بهصورت ذاتی یه کوئری زندهست، اصلاً نسخهٔ «بیات» و «زنده» نداریم که با هم فرق کنن.
نکتهٔ کلیدی مقاله اینه که دو نوع state واقعاً متفاوت داریم. اولی ردیفهاست: issue، PR، اجرای CI، پیام، ریاکشن. اینا یه منبع حقیقت واحد دارن (Postgres) و منطق last-write-wins؛ یعنی نوشتهٔ بعدی برندهست و همه به همون میرسن. دومی متن سندهاست: بدنهٔ داک یا توضیح issue، جایی که چند نفر همزمان تو یه پاراگراف تایپ میکنن و last-write-wins یعنی جملهٔ یکی، جملهٔ اون یکی رو میخوره.
برای ردیفها Plain از ElectricSQL استفاده میکنه که write-ahead log پستگرس رو با logical replication میخونه و «Shape»ها (زیرمجموعهٔ فیلترشدهٔ یه جدول) رو روی HTTP سرو میکنه. نکتهٔ امنیتی مهم اینه که کلاینت فیلتر نمیفرسته؛ فقط اسم جدول و org رو میده و سرور خودش بعد از احراز هویت و چک عضویت، شرط where رو میسازه و همیشه به organization_id قفلش میکنه:
let where = `organization_id = '${orgId}'`;
if (table === "ci_step") where += ` AND run_id = '${runId}'`;سمت کلاینت هر Shape به یه collection از TanStack DB وصله و React با یه idiom یکدست، یعنی live query، اونو میخونه و هر تغییری که Electric استریم کنه دوباره رندر میشه؛ بدون invalidate دستی و بدون refetch. برای اینکه عملِ خودِ کاربر هم آنی حس شه، نویسنده ترفند optimistic رو توضیح میده: تغییر فوری بهصورت محلی روی UI اعمال میشه، بعد سرور همون تغییر رو مینویسه و txid اون تراکنش رو برمیگردونه؛ وقتی Electric ردیف معتبر رو با همون txid برگردونه، overlay خوشبینانه یا تأیید میشه یا rollback میشه، پس هیچوقت state بیات گیر نمیکنه.
برای متن سندها مسیر فرق داره چون نویسندهٔ معتبر واحدی ندارن و CRDT هستن. ادیتور Plate با یه binding برای Yjs کار میکنه و هر داک باز یه Y.Doc میشه؛ یه سرویس جدا به اسم apps/collab با Hocuspocus (یه بکاند websocket برای Yjs) اجرا میشه. هر کلید که میزنی یه آپدیت Yjs میشه، رو websocket میره، تو کپی سرور مرج میشه و برای بقیه پخش میشه؛ چون CRDT هست ترتیب رسیدن آپدیتها مهم نیست و ادیتهای همزمان به هم میرسن. ذخیرهسازی هم debounce شده و سرور تو یه نوشتن، هم snapshot باینری CRDT و هم نسخهٔ markdownش رو کنار هم نگه میداره تا هیچوقت از هم جدا نشن.
نکات کلیدی:
- بهجای subscribe به endpoint، به state اشتراک میشی؛ هر ویو یه کوئری زندهست و رفلکس رفرش حذف میشه
- دو نوع state: ردیفها با last-write-wins روی ElectricSQL، متن سندها با CRDT روی Yjs
- کلاینت فیلتر نمیسازه؛ سرور بعد از چک عضویت شرط where رو میسازه و به organization_id قفل میکنه
- آنیبودن با overlay خوشبینانه و تطبیق txid کار میکنه: یا تأیید، یا rollback
- سرور snapshot باینری Yjs و رندر markdown رو همزمان مینویسه تا از هم درنرن




