قابلیتی که تا وقتی دو نفر استفاده‌اش نکنند ساده به‌نظر می‌رسد

وایت‌برد برای یک نفر فقط یک بوم با ذخیره/بارگذاری است. لحظه‌ای که نفر دوم همان بورد را باز می‌کند، یک مسئله‌ی سیستم‌های توزیع‌شده با لباس اپلیکیشن نقاشی دارید: کدام تغییر برنده می‌شود، سرعت دیدن تغییر توسط طرف دیگر چقدر است، و — بخشی که راحت جا می‌افتد — چطور مطمئن شوید حرکت نشانگر در وایت‌برد پروژه‌ی A به جلسه‌ی پروژه‌ی B روی همان سرور نشت نمی‌کند.

چیزی که WKFGo دوباره استفاده می‌کند به‌جای ساختن از نو

WKFGo از قبل یک هاب وب‌سوکت اجرا می‌کند که به‌روزرسانی‌های زنده‌ی بورد را به هر کلاینت متصل هُل می‌دهد. وایت‌بردها سرور وب‌سوکت جدا نمی‌گیرند؛ یک برش باریک و صریحاً محدود از همان هاب می‌گیرند. هر پیام وایت‌برد با پیشوند wb: نام‌گذاری شده، و تابع ریلی هاب فقط پیام‌های مطابق با آن پیشوند را لمس می‌کند:

// relay re-broadcasts a client-sent message to everyone else in the sender's
// project room. Only whiteboard collaboration events ("wb:*" — scene deltas,
// pointer positions) are accepted; ProjectID/Actor are stamped server-side so
// a client can't spoof another room or user.
func (h *Hub) relay(sender *Client, raw []byte) {
    if sender.projectID == 0 {
        return
    }
    var ev Event
    if err := json.Unmarshal(raw, &ev); err != nil || !strings.HasPrefix(ev.Type, "wb:") {
        return
    }
    ev.ProjectID = sender.projectID
    ev.Actor = sender.userID
    ...
}

دو جزئیات اینجا واقعاً کار امنیت را انجام می‌دهند. اول، هر پیامی که با wb: شروع نشود بی‌صدا رد می‌شود — کلاینت نمی‌تواند از این کانال ریلی برای قاچاق رویدادهای دلخواه به جلسه‌ی کلاینت‌های دیگر استفاده کند. دوم، ProjectID و Actor سمت سرور از اتصال احراز‌هویت‌شده‌ی فرستنده بازنویسی می‌شوند، نه از هرچیزی که کلاینت در بدنه‌ی پیام ادعا می‌کند. کلاینتی که یک projectId جعلی در بار داده‌اش می‌فرستد نادیده گرفته می‌شود — سرور از قبل می‌داند اتصال متعلق به کدام اتاق است، چون این در زمان اتصال تعیین شده، نه به‌ازای هر پیام.

چرا ریلی، نه ذخیره-بعد-پخش

موقعیت‌های نشانگر و دلتاهای صحنه در حال پیشرفت مستقیم به کلاینت‌های دیگر در همان اتاق پروژه ریلی می‌شوند — عمداً از همان مسیر یک به‌روزرسانی تسک عبور نمی‌کنند، که وب‌هوک‌ها و لاگ رویداد بیرونی را هم مطلع می‌کند. کامنت کد صریح است: «حرکت‌های نشانگر مادّه‌ی وب‌هوک نیستند.» یک جلسه‌ی وایت‌برد می‌تواند ده‌ها پیام موقعیت-نشانگر در ثانیه تولید کند؛ رفتار با هر کدام به‌عنوان یک رویداد دامنه‌ی قابل‌ممیزی، خط‌لوله‌ی اعلان را با نویزی پر می‌کند که هیچ‌کس برایش وب‌هوک نمی‌خواهد. ذخیره‌ی محتوای واقعی بورد جداگانه اتفاق می‌افتد، هنگام ذخیره — ریلی وب‌سوکت فقط برای لایه‌ی زنده‌ی زودگذر «همین الان همه دارند چه‌کار می‌کنند» است.

چه اتفاقی برای یک کلاینت کند می‌افتد

اگر اتصال یکی از همکاران کند شود، هاب همه‌ی بقیه را منتظر تخلیه‌ی بافر ارسال همان یک کلاینت نگه نمی‌دارد:

select {
case c.send <- data:
default:
    // Slow/stuck client: drop the message rather than block the
    // broadcaster. The client will resync on its next full fetch.
}

یک پیام موقعیت-نشانگر افتاده نامرئی است — پیام بعدی کسری از ثانیه بعد می‌رسد و هیچ‌کس شکافی در ردّ نشانگر نمی‌بیند. این فقط چون پیام‌های ریلی وایت‌برد موقعیت‌های زودگذرند کار می‌کند، نه وضعیت معتبر؛ افتادن یکی چیزی را خراب نمی‌کند، فقط یعنی نمای گیرنده از یک نشانگر سریع‌الحرکت برای یک فریم کمی کمتر روان است.

چرا Excalidraw پین شده، نه شناور

خود ویرایشگر Excalidraw است، پین‌شده روی نسخه‌ی ۰٫۱۷٫۶. این سهل‌انگاری نیست — ۰٫۱۸ داخلی‌ها را طوری تغییر داد که زیر ساخت Create React App خراب می‌شود، و دنبال‌کردن آخرین نسخه ارزش بی‌ثبات‌کردن همکاری برای یک ارتقای کتابخانه‌ی بوم که کسی نخواسته را ندارد. اگر Excalidraw را در اپ CRA خودتان یکپارچه می‌کنید، این را قبل از npm install روی جدیدترین تگ بدانید.

اشتباهات رایج

اعتماد به شناسه‌ی اتاق یا کاربر که کلاینت می‌فرستد. هر طراحی ریلی که «کدام اتاق» را از بار داده‌ی پیام بخواند به‌جای اتصال احراز‌هویت‌شده، فقط یک پیام دستکاری‌شده تا نشت بین‌پروژه‌ای فاصله دارد.

رفتار با هر پیام بلادرنگ به‌عنوان یک رویداد دامنه‌ی ذخیره‌شده. موقعیت نشانگر و خط‌های موقتی متعلق به همان خط‌لوله‌ای نیستند که چیزهایی که آدم‌ها و یکپارچه‌سازی‌ها برایشان اهمیت می‌دهند وب‌هوک راه می‌اندازد — آن خط‌لوله برای تغییرات وضعیتی است که اهمیت دارند، نه مختصات نشانگر ۶۰ بار در ثانیه.

مسدودکردن پخش‌کننده روی یک خواننده‌ی کند. یک کلاینت وب‌سوکت گیرکرده نباید بتواند به‌روزرسانی‌های زنده را برای بقیه‌ی اتاق متوقف کند؛ افتادن-و-همگام‌سازی‌مجدد بده‌بستان درستی برای داده‌ی زودگذر است.

سؤالات متداول

آیا این روی چند نمونه‌ی بک‌اند کار می‌کند؟

ریلی زنده‌ی وایت‌برد محلیِ همان نمونه‌ای می‌ماند که اتصالات وب‌سوکت را نگه می‌دارد — پل‌زدن بین‌نمونه‌ای برای رویدادهای بورد/اعلان اختیاری است، اما دلتاهای صحنه‌ی وایت‌برد می‌توانند به‌راحتی از سقف ۸ کیلوبایتیِ بار داده‌ی NOTIFY پستگرس که برای آن پل استفاده می‌شود بگذرند، پس همکاری وایت‌بردیِ بین‌نمونه‌ای فعلاً به نشست‌های چسبنده نیاز دارد (کلاینت همیشه به همان نمونه‌ی بک‌اند مسیریابی می‌شود).

اگر اتصال وسط ویرایش قطع شود چه؟

پیام‌های ریلی‌شده از دست می‌روند، اما وضعیت ذخیره‌شده‌ی بورد نه — اتصال مجدد از آخرین ذخیره همگام می‌شود، نه از جریان وب‌سوکت.

آیا کسی خارج از پروژه می‌تواند جلسه‌ی وایت‌برد را ببیند؟

نه — اتصال به یک projectID محدود است که در زمان اتصال از نشست احراز‌هویت‌شده تعیین شده، و ریلی فقط درون همان اتاق پخش می‌شود.

جمع‌بندی

همکاری بلادرنگ روی وضعیت بصری مشترک به‌خاطر نقاشی سخت نیست — به‌خاطر این سخت است که هر پیام باید به اتاق درست محدود شود، به فرستنده‌ی درست نسبت داده شود صرف‌نظر از ادعای کلاینت، و آن‌قدر ارزان باشد که وقتی خواننده‌ای عقب می‌ماند بدون عاقبت بتوان آن را رها کرد.