7 คะแนน โดย GN⁺ 2023-10-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Reflect เป็นเฟรมเวิร์กสำหรับสร้างแอปทำงานร่วมกันแบบ Figma, Notion, Google Sheets ได้อย่างรวดเร็ว โดยเปิดตัวพร้อมเพิ่ม เซิร์ฟเวอร์แบบจัดการให้ทั้งหมด บนเอนจินซิงก์แบบเกมของ Replicache
  • UI สำหรับการทำงานร่วมกันต้องแสดงการเปลี่ยนแปลงในเครื่องทันทีโดยไม่รอการตอบกลับจากเซิร์ฟเวอร์ ดังนั้น วิธีจัดการความขัดแย้ง เมื่อมีการแก้ไขข้อมูลเดียวกันพร้อมกันจึงเป็นตัวกำหนดประสบการณ์ของผลิตภัณฑ์
  • Reflect เลือกใช้ Transactional Conflict Resolution แทน CRDT โดยให้ไคลเอนต์และเซิร์ฟเวอร์รันประวัติการเรียก mutator ชุดเดียวกัน และให้เซิร์ฟเวอร์สร้างสถานะที่มีอำนาจตามลำดับที่มาถึง
  • sequence CRDT อย่าง Yjs แข็งแกร่งกับข้อความ ลิสต์ และแมป แต่ในกรณีที่ต้องมีกฎการ merge แยกต่างหากอย่างตัวนับ ค่าที่เพิ่มอาจหายไปได้ ขณะที่ Reflect จัดการเลขคณิต การปรับแต่งลิสต์ และ invariant ระดับสูงด้วยการรัน mutator ซ้ำ
  • เนื่องจากเซิร์ฟเวอร์รับเพียงชื่อ mutation และอาร์กิวเมนต์แล้วคำนวณผลลัพธ์ใหม่ จึงไม่ต้องเชื่อถือผลการคำนวณจากไคลเอนต์ และใส่ การตรวจสอบสิทธิ์, การตรวจสอบ schema และ migration เข้าไปในการออกแบบได้ง่าย

ประสบการณ์นักพัฒนาที่ Reflect เปิดเผย

  • Reflect เป็นแนวทางใหม่สำหรับสร้าง เว็บแอปมัลติเพลเยอร์ แบบ Figma, Notion, Google Sheets
  • เป็นวิวัฒนาการของ Replicache ซึ่งเป็นเฟรมเวิร์กซิงก์ฝั่งไคลเอนต์ที่มีอยู่เดิม และใช้ เอนจินซิงก์แบบเกม เดียวกัน
  • ต่างจาก Replicache ตรงที่มี เซิร์ฟเวอร์แบบจัดการให้ทั้งหมด รวมอยู่ด้วย โดยมีเป้าหมายให้สร้างแอปมัลติเพลเยอร์คุณภาพสูงได้ภายในไม่กี่นาที
  • Reflect เปิดให้ใช้งานสาธารณะเป็นครั้งแรก โดยดูบทแนะนำได้ที่ reflect.net และเริ่มต้นได้ที่ hello.reflect.net

โครงสร้างที่ทำให้เกิดความขัดแย้งในการแก้ไขร่วมกัน

  • การแก้ไขร่วมกันย่อมเกิด ความขัดแย้ง อย่างหลีกเลี่ยงไม่ได้
  • หากต้องการสร้าง UI ที่ตอบสนองทันที จะรอเซิร์ฟเวอร์ไม่ได้ และการเปลี่ยนแปลงต้องเกิดขึ้นในเครื่องของไคลเอนต์ก่อน
  • ผู้ใช้หลายคนอาจแก้ไขรายการเดียวกันพร้อมกันได้ จึงต้องซิงก์และแก้ไขความขัดแย้งอย่างเป็นธรรมชาติ เพื่อให้ผู้ใช้ทุกคนเห็นผลลัพธ์เดียวกัน
  • เอนจินซิงก์ส่งผลต่อทั้งประสบการณ์นักพัฒนา ประสบการณ์ผู้ใช้ ประสิทธิภาพที่เป็นไปได้ และประเภทของแอปที่สร้างได้

ตัวอย่างตัวนับใน CRDT และ Yjs

  • ในระบบนิเวศเว็บ CRDT ถูกใช้อย่างแพร่หลายเป็นวิธีซิงก์ข้อมูล
  • CRDT เป็นโครงสร้างข้อมูลที่เมื่อมีการแลกเปลี่ยนการเปลี่ยนแปลงทั้งหมดระหว่างผู้ร่วมงานแล้วจะลู่เข้าสู่ค่าเดียวกัน โดย Yjs และ Automerge เป็นไลบรารี CRDT โอเพนซอร์สที่เป็นตัวแทนสำคัญ
  • Reflect ไม่ใช่ CRDT และใช้ Transactional Conflict Resolution ซึ่งเป็นการดัดแปลง Server Reconciliation ที่ใช้กันมานานในอุตสาหกรรมวิดีโอเกม
  • เหตุผลที่ตัวนับแบบง่ายพังใน Yjs

    • วิธีเก็บค่า count ใน Yjs Map แล้วเขียน prev + 1 กลับลงไป อาจ สูญเสียค่าที่เพิ่ม ในสถานการณ์ที่เกิดพร้อมกัน
    • ตัวอย่างตัวนับที่ถูกต้องในเอกสาร Yjs คือการเพิ่มตัวเลขลงในอาร์เรย์แล้วคำนวณผลรวม
    • Yjs เป็น sequence CRDT จึงแข็งแกร่งกับลิสต์ ชิ้นส่วนข้อความ และแมป แต่ยากที่จะจำลองตัวนับได้อย่างเป็นธรรมชาติ
    • อัลกอริทึม merge ของ Yjs Map เป็นแบบ last-write wins แยกตามคีย์ ดังนั้นหากผู้ใช้สองคนเพิ่มค่าพร้อมกัน การเปลี่ยนแปลงของคนใดคนหนึ่งอาจหายไปได้
    • CRDT เหมาะกับปัญหาบางประเภท แต่มีข้อจำกัดคือขยายไปใช้ได้ยากหากไม่ใช่ปัญหานั้น

วิธีทำงานของ Transactional Conflict Resolution

  • ใน Reflect การเปลี่ยนแปลงถูกทำเป็นฟังก์ชัน JavaScript พิเศษที่เรียกว่า mutator
  • สำเนาของ mutator แต่ละตัวมีอยู่ทั้งในทุกไคลเอนต์และบนเซิร์ฟเวอร์
  • เมื่อผู้ใช้สร้างการเปลี่ยนแปลง Reflect จะสร้าง mutation ซึ่งเป็นประวัติการเรียก mutator
    • mutation มีเพียงชื่อ mutator และอาร์กิวเมนต์ เช่น increment(delta: 1)
    • เนื้อหาการเปลี่ยนแปลงที่เกิดขึ้นจะไม่ถูกรวมอยู่ใน mutation
  • Reflect นำ mutation ไปใช้ในเครื่องทันทีเพื่ออัปเดต UI และผู้ใช้จะเห็นการเปลี่ยนแปลงของตนเองได้ทันที
  • การจัดลำดับเชิงเส้นและการรันซ้ำบนเซิร์ฟเวอร์

    • ไคลเอนต์แต่ละตัวจะเพิ่ม mutation ต่อไปโดยไม่รอเซิร์ฟเวอร์
    • mutation จะถูกสตรีมไปยังเซิร์ฟเวอร์ และเซิร์ฟเวอร์จะ จัดลำดับเชิงเส้น mutation ตามเวลาที่มาถึง แล้วสร้างสถานะที่มีอำนาจถัดไป
    • ตัวอย่างเช่น หาก increment(1) ของไคลเอนต์ 1 และ increment(2) ของไคลเอนต์ 2 เกิดขึ้นพร้อมกัน การรันบนเซิร์ฟเวอร์จะสร้างค่าตัวนับสุดท้ายตามลำดับที่มาถึง
    • เซิร์ฟเวอร์ merge ความขัดแย้งด้วยการจัดลำดับเชิงเส้นของประวัติการรัน โดยไม่ต้องมีความรู้แยกต่างหากว่า increment ทำอะไรหรือ merge อย่างไร
    • สถานะที่มีอำนาจล่าสุดจะถูกสตรีมต่อเนื่องไปยังแต่ละไคลเอนต์
    • เมื่อไคลเอนต์รู้ว่า mutation ที่รออยู่ของตนถูกนำไปใช้ในสถานะที่มีอำนาจแล้ว ก็จะลบออกจากคิวในเครื่อง
    • mutation ที่ยังรออยู่จะถูก rebase โดยรันโค้ด mutator อีกครั้งบนสถานะที่มีอำนาจล่าสุด
    • วงจรทั้งหมดนี้เกิดขึ้นได้สูงสุด 120 ครั้งต่อวินาที ต่อไคลเอนต์

ต้นทุนการใช้งานและข้อดีที่นำไปใช้ได้ทั่วไป

  • การทำวิธีนี้ต้องมี datastore ที่รวดเร็วซึ่งสามารถย้อนกลับ fork และสร้าง branch ได้
  • ฝั่งเซิร์ฟเวอร์ก็ต้องมีที่เก็บข้อมูลที่รวดเร็วสำหรับประมวลผล mutation ที่เข้ามาเช่นกัน
  • ต้องมีวิธีซิงก์ mutator และกระบวนการกู้คืนเมื่อไคลเอนต์หรือเซิร์ฟเวอร์เกิดความขัดแย้งระหว่างการซิงก์
  • สิ่งที่ได้กลับมาคือการจัดลำดับเชิงเส้นของฟังก์ชันใด ๆ ทำงานเป็นกลยุทธ์ซิงก์ที่ค่อนข้างครอบคลุม
  • ตัวอย่างที่จัดการได้โดยไม่ต้องมีโค้ดซิงก์แยกต่างหาก

    • การคำนวณทางเลขคณิตถูกจัดการได้อย่างเป็นธรรมชาติ
    • setHighScore บันทึกค่าที่มากกว่าระหว่าง high-score เดิมกับคะแนนที่เสนอเข้ามา
    • การปรับแต่งลิสต์ส่วนใหญ่ก็ทำงานได้
    • append เพิ่มรายการต่อท้ายลิสต์ช้อปปิ้ง
    • insertAt แทรกรายการในตำแหน่งที่กำหนด โดย splice() จะปรับตำแหน่งให้ถูกต้อง
    • remove ควรรับรายการหรือ ID ที่เสถียรเป็นอาร์กิวเมนต์ เพราะอินเด็กซ์อาจเปลี่ยนได้
    • invariant ระดับสูงก็สามารถบังคับใช้ได้เช่นกัน
    • addChild อัปเดต childIDs ของพาเรนต์และ parentID ของลูกพร้อมกัน เพื่อให้สอดคล้องกันเสมอ
    • ตัวอย่างเหล่านี้ถูก merge ได้อย่างสมเหตุสมผล แม้ไม่มีโค้ดแยกต่างหากที่คำนึงถึงการซิงก์

อำนาจของเซิร์ฟเวอร์และการตรวจสอบสิทธิ์

  • ใน Reflect เซิร์ฟเวอร์เป็นผู้มีอำนาจตัดสิน
  • สิ่งที่ไคลเอนต์คิดว่าเป็นผลของการเปลี่ยนแปลงจะไม่ถูกแชร์ไปยังเซิร์ฟเวอร์หรือไคลเอนต์อื่น
  • สิ่งที่ส่งไปยังเซิร์ฟเวอร์มีเพียงชื่อ mutation และอาร์กิวเมนต์เท่านั้น และเซิร์ฟเวอร์จะคำนวณผลลัพธ์ของ mutation ใหม่เอง
  • เซิร์ฟเวอร์ไม่จำเป็นต้องรันโค้ดเดียวกับไคลเอนต์ และอาจเรียกดูบริการภายนอกหรือใช้ค่าสุ่มได้ด้วย
  • การกำหนดสิทธิ์แบบละเอียด

    • การออกแบบนี้ทำให้ใส่ การตรวจสอบสิทธิ์ แบบละเอียดได้อย่างเป็นธรรมชาติ
    • ตัวอย่างคือในโปรแกรมออกแบบร่วมกัน อนุญาตให้ guest แสดงความคิดเห็นและไฮไลต์ได้ แต่ห้ามแก้ไขงานออกแบบจริง
    • ใน CRDT การใช้งานทำได้ยาก เพราะไม่มีตำแหน่งให้วางลอจิกสำหรับปฏิเสธการเปลี่ยนแปลงที่ไม่มีสิทธิ์
    • ใน Reflect mutator ที่รันบนเซิร์ฟเวอร์สามารถตรวจสอบค่าอย่าง tx.user.canEdit และโยนข้อผิดพลาด unauthorized ได้หากไม่มีสิทธิ์
    • แม้ mutator จะรันโค้ดบนเซิร์ฟเวอร์แตกต่างจากไคลเอนต์ก็ไม่มีปัญหา เพราะเซิร์ฟเวอร์เป็นผู้ตัดสินขั้นสุดท้าย

การตรวจสอบ schema และคำแนะนำการใช้งาน

  • ในแนวทางของ Reflect การตรวจสอบ schema และ migration ก็สามารถถูกรวมอยู่ในการออกแบบได้อย่างเป็นธรรมชาติ
  • การเลือกกลยุทธ์ซิงก์เป็นหัวใจของระบบมัลติเพลเยอร์ และ Reflect มองว่า Transactional Conflict Resolution ที่เรียนรู้มาจากแนวทางของอุตสาหกรรมเกมเป็นวิธีที่เรียบง่าย ยืดหยุ่น และทรงพลัง
  • หากกำลังสร้างแอปมัลติเพลเยอร์ สามารถลองได้ที่ หน้าเริ่มต้นของ Reflect
  • หากต้องการคุยกับทีมพัฒนา สามารถใช้ discord.reflect.net หรือ @hello_reflect

1 ความคิดเห็น

 
GN⁺ 2023-10-19
ความคิดเห็นบน Hacker News
  • เดโมที่ด้านบนของหน้าโฮมเพจ(https://reflect.net/)ค่อนข้างสนุก
    พอดูไปเรื่อย ๆ ทุกครั้งที่ต่อปริศนาเสร็จ ผู้คนจะดีใจด้วยการเขย่า เคอร์เซอร์ เหมือนกำลังพูดว่า “พวกเราทำได้แล้ว!”

    • เริ่มเอาชิ้นส่วนมาต่อเป็นคำว่า FUCK แล้วพอคนอื่นสังเกตเห็นและเข้ามาร่วมด้วย กลับกลายเป็นประสบการณ์ที่อบอุ่นกว่าที่คิด
    • มีบั๊กด้านภาพที่ทำให้ซ่อนชิ้น e ไว้หลังส่วน e ที่ต่อเสร็จแล้วได้
      ตัวอักษรอื่น ๆ ยังเห็นเส้นขอบอยู่ตลอด จึงทำแบบเดียวกันไม่ได้
    • เดโมนี้ดูไม่น่าจะเป็นตัวอย่างที่แสดงความสามารถของไลบรารีได้ดีนัก
      เมื่อหยิบชิ้นส่วน ชิ้นนั้นดูเหมือนจะถูกล็อกให้ผู้ใช้คนนั้น ทำให้แทบไม่มีความขัดแย้งให้ต้องแก้ เหลือแค่กรณีคนสองคนหยิบพร้อมกันแล้วให้คนที่หยิบก่อนเท่านั้น
      อยากเห็นเดโมตัวอย่างที่ดีกว่านี้ ซึ่งมีการเกิด การแก้ไขความขัดแย้ง จริง ๆ
    • เป็นประสบการณ์ที่สนุกจริง ๆ
      วิดีโอที่บันทึกฉากที่อธิบายไว้ดูได้ที่นี่: https://streamable.com/asu261
  • คุณอาจจำได้ว่าเคยมีการพูดถึง Replicache ขึ้นมาหนึ่งหรือสองครั้งก่อนหน้านี้
    Reflect เป็นรูปแบบที่เพิ่มเซิร์ฟเวอร์ซิงก์แบบจัดการเต็มรูปแบบและเร็วมากเข้าไป
    ช่วงนี้สาย local-first/realtime เริ่มคึกคักมาก แต่ Replicache/Reflect มี data model และโมเดลการเขียนโค้ดที่เรียบง่ายสวยงาม จึงควรค่าแก่การลองดู
    เมื่อเทียบกับ CRDT ข้อดีคือสามารถจัดการความขัดแย้งเองด้วยโค้ดลำดับขั้นแบบธรรมดาได้ และผมมองว่าการเพิ่มการแก้ไขความขัดแย้งเฉพาะแอปพลิเคชันเข้าไปใน CRDT เมื่อกฎในตัวไม่ตรงความต้องการนั้นอาจซับซ้อน

    • เห็นด้วยเต็มที่
      PowerSync ก็เลือกใช้ สถาปัตยกรรมการปรับให้ตรงกับเซิร์ฟเวอร์ แทน CRDT เช่นกัน และในแอปพลิเคชันที่มีเซิร์ฟเวอร์กลาง ความเรียบง่ายของโครงสร้างแบบนี้น่าดึงดูดมาก
      อยากยกเครดิตให้ Aaron ที่ทำให้แนวคิด server reconciliation เป็นที่รู้จักและแพร่หลาย: https://www.gabrielgambetta.com/client-side-prediction-serve...
    • เธรดเก่าที่เกี่ยวข้องมีดังนี้
      A JavaScript framework to build offline-first but collaborative webapp - https://news.ycombinator.com/item?id=33269440 - ตุลาคม 2022
      Linear clone, with realtime sync and instant UI - built with Replicache - https://news.ycombinator.com/item?id=31331660 - พฤษภาคม 2022
      Replicache: Easy Offline-First for Existing Applications - https://news.ycombinator.com/item?id=22173500 - มกราคม 2020
  • ประมาณ 15 ปีก่อน ตอนอยู่บ้านคุณยายเพื่อหนีความเบื่อ ผมเคยลงลึกกับหัวข้อนี้พอสมควร และเคยทำ implementation ด้วย JavaScript ด้วย
    คำว่า “การดำเนินการทางคณิตศาสตร์แค่ทำงานได้เอง” นั้นแน่นอนว่าเป็นเรื่องที่ทำให้เป็น การดำเนินการแบบ idempotent ได้ง่ายมาก แต่คำว่า “การดำเนินการกับลิสต์ก็แค่ทำงานได้เอง” นั้นผมเห็นด้วยยาก
    เช่น สมมติว่ามีอาร์เรย์อย่าง [1, 2, 3, 4, 5] โดย A ลบช่วง 2~4, B ลบช่วง 3~5 และ C แทรกบางอย่างระหว่าง 3 กับ 4 เมื่ออัปเดตทั้งสามมาถึงเซิร์ฟเวอร์พร้อมกัน คำตอบจะกำกวม
    ถ้าพึ่งพา timestamp หรือ Last Write Wins ก็จะทำให้ transaction model พัง และถ้าเลือกให้รายการหนึ่งเป็นผู้ชนะ ก็ยังมีปัญหาว่าผู้ใช้คนอื่นจะเห็นอะไรและจะสื่อสารเรื่องนี้อย่างไร
    ถ้าคำตอบคือ “ส่งอาร์เรย์ทั้งหมดใหม่” แบบนั้น transaction model ก็พังเช่นกัน

    • ข้อทักท้วงสมเหตุสมผล
      จริง ๆ แล้วถ้อยคำที่ตั้งใจน่าจะใกล้กับ “การดำเนินการกับลิสต์จำนวนมากแค่ทำงานได้เอง” มากกว่า
      ใน Reflect ไม่สามารถใช้ ดัชนีของลิสต์ เป็นตัวระบุสำหรับการแก้ไข/ลบได้ เพราะดัชนีไม่เสถียร และนี่คือเหตุผลที่แนะนำให้ใช้ตัวรายการเองหากเป็นอะตอมิก หรือโดยทั่วไปให้ใช้ ID ที่เสถียร: https://i.imgur.com/IKzmf0q.png
      ยังมีปัญหาที่การแทรกของ C อาจถูกลบได้ แต่ในบริบทของการทำงานร่วมกันแบบเรียลไทม์ ไม่มีโปรโตคอลใดแก้ได้สมบูรณ์
      จากมุมของ C สิ่งที่เพิ่งเขียนอาจหายไปและทำให้รู้สึกแย่ได้ เรื่องแบบนี้เกิดจากเจตนาของมนุษย์ที่ทำงานพร้อมกันในพื้นที่เดียวกันไม่ตรงกัน ดังนั้นกลไกเชิงสังคมอย่าง undo และการแสดงว่าใครกำลังทำงานอยู่จึงช่วยได้
    • แม้ไม่ต้องยกเรื่องการลบมา “พร้อมกัน” ถ้า A เพิ่ม “a” ลงในลิสต์ L, B เพิ่ม “b” และ C เพิ่ม “c” เซิร์ฟเวอร์ก็จะนำการเพิ่มไปใช้ตามลำดับใดก็ได้ แล้วส่งผลลัพธ์กลับไปยังไคลเอนต์
      ระหว่างนั้น ไคลเอนต์แต่ละตัวอาจนำการเพิ่มของตัวเองไปใช้ในเครื่อง ทำให้ A เห็น [“a”], B เห็น [“b”], C เห็น [“c”] และเมื่อเซิร์ฟเวอร์ส่งสถานะ [“c”, “b”, “a”] มา ไคลเอนต์ก็น่าจะทิ้งการเปลี่ยนแปลงที่รออยู่ แล้วถือว่าสถานะของเซิร์ฟเวอร์คือความจริงของโลก
      แต่ถ้าการเพิ่มแต่ละครั้งมีผลทำนองว่า “ถ้าการเปลี่ยนแปลงของฉันถูกนำไปใช้ก่อน ฉันชนะ” ก็สงสัยว่าในช่วง 300ms ที่รออัปเดตจากเซิร์ฟเวอร์ ทุกคนจะเห็น “you win” กันหมดหรือไม่
    • ระบบแบบนี้ส่วนใหญ่จัดการการลบด้วย tombstone
      แบบนั้นจึงแทรกไว้หลังหรือระหว่างรายการที่ถูกลบได้อย่างปลอดภัย
    • ในแอปพลิเคชันสำหรับการทำงานร่วมกัน เซิร์ฟเวอร์สามารถแก้สามการดำเนินการนี้ในลำดับใดก็ได้
      ผู้ใช้ทั้งสามเห็นผลลัพธ์ แล้วสังเกตได้ว่าต่างคนต่างแตะข้อมูลเดียวกันพร้อมกันจนสถานะยุ่งเหยิง จากนั้นก็แก้ไขได้
      ให้นึกถึงการแก้ไขเอกสาร
      ถ้ามองไม่เห็นว่าอีกฝ่ายกำลังทำงานอยู่ ก็อาจแปลกใจได้ แต่สุดท้ายก็แก้ให้เป็นสถานะที่ต้องการได้
      หากไม่ตรวจสอบผลลัพธ์หรือไม่สามารถตรวจสอบได้ สถานะก็จะไม่ถูกต้อง แต่ในแอปเชิงโต้ตอบอย่างการแก้ไขร่วมกันหรือเกม โดยปกติจะไม่ดำเนินไปแบบนั้น
  • ผมเป็นหนึ่งในคนที่เข้าร่วมโปรเจกต์นี้ ถ้ามีคำถามก็ตอบได้

    • Aaron เป็นคนถ่อมตัว เลยขอพูดแทนว่าเขาเป็นคนสร้าง Greasemonkey และมีส่วนเกี่ยวข้องกับนวัตกรรมสำคัญ ๆ หลายอย่างของเบราว์เซอร์ตลอดหลายปีที่ผ่านมา
    • ก่อนหน้านี้ผมเคยสร้าง ระบบซิงก์แบบมัลติเพลเยอร์ หลายตัวใน codebase ที่ไม่เปิดเผย และติดตามวงการนี้มาตลอด
      เคยสร้างไลบรารี “redux-pubsub” ที่มี rebase และ server authority ด้วย ซึ่งเท่าที่เข้าใจคล้ายกับ TCR
      โมเดลนี้มีหลายส่วนที่ชอบ และบทความที่ลิงก์ไว้ก็อธิบายได้ชัดเจนมาก
      เห็นบอกว่า “การตรวจสอบสคีมาและ migration แทบจะได้มาฟรีตามธรรมชาติจากการออกแบบ” เลยอยากรู้ว่าในการ migration มีแนวทางไหนที่ใช้ได้ผลจริงบ้าง
      อีกอย่าง ถ้าเป็น use case ที่ต้องจัดการการแก้ไขข้อความร่วมกันปริมาณมากในระบบ TCR ปกติผมคงนึกถึง Yjs กับ Tiptap/ProseMirror เป็นพื้นฐาน เลยสงสัยว่าวิธีที่ดีที่สุดคือวางเอกสาร CRDT กับเอกสาร TCR ไว้คู่ขนานกันหรือไม่
    • อยากรู้ว่า การพัฒนา Replicache ต่อจากนี้จะเป็นอย่างไร
      อยากทราบว่า codebase ฝั่งไคลเอนต์ส่วนใหญ่แชร์กันอยู่ จึงคาดหวังการอัปเดตต่อไปได้ไหม หรือมีโอกาสสูงที่ Reflect จะเข้ามาเป็นโฟกัสหลักแทน
    • อยากรู้ว่าคิดอย่างไรกับ ElectricSQL
    • อยากรู้ว่า จำนวนผู้ใช้สูงสุด ที่เข้ามาอยู่ในห้องเดียวพร้อมกันได้ประมาณเท่าไร
  • คำศัพท์ชวนสับสนนิดหน่อย
    ในเกมมี “ผู้เล่น” เลยเรียกระบบซิงก์ว่า “มัลติเพลเยอร์” แต่ในซอฟต์แวร์ทั่วไปมี “ผู้ใช้” ดังนั้นเรียกว่า หลายผู้ใช้ น่าจะเหมาะกว่า
    ในหน้าเว็บใช้ทั้ง “ผู้ใช้” และ “มัลติเพลเยอร์” ปนกัน เลยอ่านแล้วรู้สึกแปลก

    • อาจจะเป็นอย่างนั้นได้ แต่คำว่า “ผู้ใช้” ฟังดูเหมือนผู้บริโภค
      HN เป็นบริการแบบหลายผู้ใช้ แต่จะเรียก HN ว่ามัลติเพลเยอร์ก็ดูแปลก
      การโต้ตอบพร้อมกันแบบเรียลไทม์มีอะไรที่แรงกว่าคำว่า หลายผู้ใช้ และคำว่า มัลติเพลเยอร์ ที่หมายถึงผู้ใช้หลายคนลงมือทำและโต้ตอบกัน ก็ดูเป็นการใช้คำที่โอเค
    • ลอจิกนี้อิงจากลอจิกที่ใช้ใน เกมมัลติเพลเยอร์ มาหลายสิบปี
      ในทางกลับกัน ซอฟต์แวร์เว็บทั้งหมดล้วนเป็นแบบหลายผู้ใช้อยู่แล้ว คำนั้นอย่างเดียวจึงไม่ได้ให้ข้อมูลอะไร
    • ทุกวันนี้คำศัพท์มันตกผลึกไปแบบนั้นแล้ว
      มัลติเพลเยอร์หมายถึง หลายผู้ใช้แบบไลฟ์ ที่เห็นได้ว่าผู้ใช้อื่นกำลังทำอะไรอยู่ในทุกขณะ
  • ช่วงประมาณ 2 ปีที่ผ่านมาได้ดู CRDT แบบคร่าว ๆ และสงสัยมาตลอดว่า การให้สิทธิ์ จัดการกันอย่างไร
    บทความนี้ดูเหมือนจะชี้ว่าแค่ไลบรารี CRDT อย่าง Y.js อย่างเดียว ยากที่จะรองรับแอปพลิเคชันที่การเปลี่ยนแปลงต้องผ่านการตรวจสิทธิ์ได้ดี
    เพราะไม่มี authority กลาง หรือก็คือเซิร์ฟเวอร์ และ Reflect ดูเหมือนจะสมมติว่าเซิร์ฟเวอร์เป็นตัวกลางในการไกล่เกลี่ยปฏิสัมพันธ์ของไคลเอนต์ เข้าใจแบบนี้ถูกไหม

    • คำว่า “ทำไม่ได้” แรงไป
      ถ้าพยายามก็จัดการสิทธิ์ใน CRDT ได้ เช่น วางเซิร์ฟเวอร์คั่นระหว่างทุกอย่าง แล้วให้เซิร์ฟเวอร์ย้อนกลับการเปลี่ยนแปลงที่ไม่มีสิทธิ์ตามที่เซิร์ฟเวอร์เห็น
      แต่ยิ่งแอปพลิเคชันใหญ่ขึ้น ก็ยิ่งต้องดูแลมากขึ้นและเปราะบางขึ้น แถมยังเสียข้อดีบางส่วนของ CRDT ไปตั้งแต่แรกด้วย
      ถ้ามีเซิร์ฟเวอร์อยู่ตรงกลางอยู่แล้ว การใช้ โปรโตคอล ที่เซิร์ฟเวอร์ปฏิเสธข้อความได้ตั้งแต่ต้นจะเรียบง่ายกว่ามาก
    • CRDT ก็มีเซิร์ฟเวอร์ได้ เพียงแต่เซิร์ฟเวอร์ไม่จำเป็นต้องอยู่ตรงกลางของทุกอย่างเสมอไป
      เช่น การตรวจสอบสิทธิ์การเชื่อมต่อให้เซิร์ฟเวอร์รับผิดชอบได้
      ถ้ามีคนเชื่อมต่อแบบ P2P แล้วอ้างสิทธิ์บางอย่าง ก็สามารถไปตรวจสอบคำอ้างนั้นกับเซิร์ฟเวอร์ได้
      อีกทั้ง CRDT ไม่ได้แปลว่าต้องเป็น P2P เสมอไป และยังสามารถใช้เซิร์ฟเวอร์กลางส่งต่อข้อความ พร้อมคงโมเดล CRDT ที่ทั้งเซิร์ฟเวอร์และไคลเอนต์ resolve สถานะปัจจุบันได้
    • สามารถจัดการสิทธิ์ได้ด้วยการ เซ็นชื่อ แต่ละ operation หรือ message แล้วตรวจสอบว่าตรงกับ public key ใน access control list หรือไม่
      วิธีนี้ทำให้ CRDT ทำงานได้แม้ในบริบท P2P
      แต่ถ้าเซิร์ฟเวอร์เป็น authority ก็แค่ปฏิเสธข้อความจากไคลเอนต์ที่ไม่มีสิทธิ์ได้เลย
  • อยากรู้ว่าจัดการ การอัปเกรด mutator อย่างไร
    ถ้าไคลเอนต์ยังรันโค้ดเก่าอยู่ operation ก็จะต่างจากเซิร์ฟเวอร์ คำตอบที่ชัดเจนคือ version แยกกันอย่าง increment_v1, increment_v2 แต่สงสัยว่ามีวิธีที่ดีกว่านี้ไหม

    • ตอนนี้ยังไม่มีคำตอบที่ดีกว่าการไม่ลบ mutator เดิม จนกว่าจะรู้ว่าไม่มีไคลเอนต์ที่ยังมีการเปลี่ยนแปลงค้างอยู่แล้ว
      เพราะยังไม่ได้เปิด persistence ตอนนี้ช่วงเวลานั้นจึงค่อนข้างสั้น
  • ยินดีด้วยกับการเปิดตัว
    อยากรู้ว่าอยู่ในสายเดียวกับ https://partykit.io/ หรือเปล่า

    • อยู่ในพื้นที่เดียวกัน
      ความต่างหลักคือแต่ละฝั่งมี การออกแบบที่มีความเห็นชัดเจน แค่ไหน
      PartyKit ไม่กำหนดแนวทางมากนัก และใกล้เคียงกับเซิร์ฟเวอร์ JavaScript น้ำหนักเบาที่เริ่มได้เร็วและ autoscale ได้
      ส่วนใหญ่ดูเหมือนจะรัน yjs บน PartyKit แต่ก็รัน automerge หรือ Replicache ได้เช่นกัน
      Reflect โฟกัสเต็มที่กับการมอบประสบการณ์มัลติเพลเยอร์ที่ดีที่สุดเท่าที่ทำได้ และมีแผนจะผสานตัวเลือกจำนวนมากทั่วทั้งสแตกอย่างแน่นหนา เพื่อให้มัลติเพลเยอร์ใช้งานได้เลย และให้โฟกัสกับการสร้างแอปพลิเคชัน
  • สำหรับข้อมูลเพิ่มเติม กลยุทธ์นี้ในวงการพัฒนาเกมเรียกว่า deterministic lockstep และมักใช้บ่อยเป็นพิเศษในเกมที่มีเอนทิตีจำนวนมากซึ่งต้องซิงโครไนซ์สถานะ
    ตัวอย่างที่เด่นชัดคือเกมวางแผนแบบเรียลไทม์ ซึ่งแทบทั้งหมดใช้วิธีนี้

    • พูดให้แม่นยำกว่านั้น เนื่องจากไคลเอนต์ไม่รอการตอบกลับจากเซิร์ฟเวอร์ก่อนแสดงการเปลี่ยนแปลงในเครื่อง จึงใกล้เคียงกับ deterministic rollback มากกว่า
      มีงานนำเสนอ GDC ที่ยอดเยี่ยมซึ่งอธิบายการใช้งานของ Mortal Kombat และ Injustice 2 อย่างลึกซึ้ง: https://youtu.be/7jb0FOcImdg
    • นี่ไม่ใช่ deterministic lockstep
      deterministic lockstep เป็นอัลกอริทึมเกมแบบ P2P โดยผู้เข้าร่วมแต่ละคนจะรออินพุตจากผู้เล่นคนอื่นทั้งหมดก่อนจึงดำเนินการจำลองเกมต่อ
      เหตุผลที่เรียกว่า “deterministic” คือมีการแชร์อินพุต ไม่ใช่ผลลัพธ์ของการจำลอง และการจำลองให้ผลแน่นอนเมื่อใช้อินพุตเดียวกัน ส่วนเหตุผลที่เรียกว่า “lockstep” คือไคลเอนต์ทั้งหมดเดินหน้าไปด้วยความเร็วที่ประสานกัน
      เนื่องจากซีรีส์ Age of Empires ใช้วิธีนี้ เมื่อคลิกแล้วหน่วยจึงไม่เคลื่อนที่ทันที และ StarCraft ก็ใช้วิธีนี้เช่นกัน แต่มีเทคนิคที่ทำให้ความรู้สึกในการเล่นลื่นไหลขึ้น
      Reflect ไม่ใช่ P2P แต่ใกล้เคียงกับ server-authoritative simulation มากกว่า
      ไคลเอนต์ส่งอินพุตไปยังเซิร์ฟเวอร์ แต่ไม่รอผลลัพธ์และทำนายในเครื่อง ขณะที่เซิร์ฟเวอร์ย้อนเวลาและเล่นอินพุตซ้ำเพื่อชดเชยความหน่วงของไคลเอนต์แต่ละราย
      หลังจากนั้น เมื่อไคลเอนต์ได้รับผลลัพธ์จากเซิร์ฟเวอร์ที่รวมอินพุตของตัวเองแล้ว ก็จะปรับแก้การจำลองในเครื่อง
      คำสำคัญของอัลกอริทึมนี้คือ server authority, prediction, lag compensation และ prediction reconciliation
      ไม่แน่ใจว่ามีใน Reflect หรือไม่ แต่ในเกม FPS มักพบ client-side interpolation ซึ่งเมื่อได้รับการอัปเดตของโลกแล้ว จะค่อย ๆ แทรกค่าเอนทิตีไปยังตำแหน่งและการหมุนใหม่ในช่วงเวลาหนึ่ง
      เนื่องจากมีผู้มีอำนาจเพียงเซิร์ฟเวอร์เดียว determinism จึงไม่ได้สำคัญมากนัก แต่ก็มีประโยชน์ในการลดการทำนายผิดพลาดในการทำนายฝั่งไคลเอนต์
      การทำนายผิดพลาดเกิดขึ้นเมื่ออินพุตจากไคลเอนต์อื่นเปลี่ยนสถานะโลกอย่างมาก หรือเมื่อการจำลองไม่เป็น deterministic เช่น การสร้างเลขสุ่มไม่ได้ซิงโครไนซ์กัน
      Counter Strike มีตัวอย่างที่ไม่ซิงโครไนซ์เลขสุ่มของการกระจายกระสุน เพื่อป้องกันชีต “nospread”
      https://www.gabrielgambetta.com/client-side-prediction-live-...
      https://developer.valvesoftware.com/wiki/Latency_Compensatin...
      https://developer.valvesoftware.com/wiki/Source_Multiplayer_...
  • Reflect ยอดเยี่ยมมาก
    ตอนนี้เราใช้ เวอร์ชันอัลฟา ในโปรดักชันอยู่ และพอใจมากทั้งกับระบบ รวมถึง Aaron และทีม
    หากมีคำถามจากมุมมองของลูกค้า ผม/ฉันสามารถตอบได้