- 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
- mutation มีเพียงชื่อ mutator และอาร์กิวเมนต์ เช่น
- 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 ความคิดเห็น
ความคิดเห็นบน Hacker News
เดโมที่ด้านบนของหน้าโฮมเพจ(https://reflect.net/)ค่อนข้างสนุก
พอดูไปเรื่อย ๆ ทุกครั้งที่ต่อปริศนาเสร็จ ผู้คนจะดีใจด้วยการเขย่า เคอร์เซอร์ เหมือนกำลังพูดว่า “พวกเราทำได้แล้ว!”
ตัวอักษรอื่น ๆ ยังเห็นเส้นขอบอยู่ตลอด จึงทำแบบเดียวกันไม่ได้
เมื่อหยิบชิ้นส่วน ชิ้นนั้นดูเหมือนจะถูกล็อกให้ผู้ใช้คนนั้น ทำให้แทบไม่มีความขัดแย้งให้ต้องแก้ เหลือแค่กรณีคนสองคนหยิบพร้อมกันแล้วให้คนที่หยิบก่อนเท่านั้น
อยากเห็นเดโมตัวอย่างที่ดีกว่านี้ ซึ่งมีการเกิด การแก้ไขความขัดแย้ง จริง ๆ
วิดีโอที่บันทึกฉากที่อธิบายไว้ดูได้ที่นี่: 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”], B เห็น [“b”], C เห็น [“c”] และเมื่อเซิร์ฟเวอร์ส่งสถานะ [“c”, “b”, “a”] มา ไคลเอนต์ก็น่าจะทิ้งการเปลี่ยนแปลงที่รออยู่ แล้วถือว่าสถานะของเซิร์ฟเวอร์คือความจริงของโลก
แต่ถ้าการเพิ่มแต่ละครั้งมีผลทำนองว่า “ถ้าการเปลี่ยนแปลงของฉันถูกนำไปใช้ก่อน ฉันชนะ” ก็สงสัยว่าในช่วง 300ms ที่รออัปเดตจากเซิร์ฟเวอร์ ทุกคนจะเห็น “you win” กันหมดหรือไม่
แบบนั้นจึงแทรกไว้หลังหรือระหว่างรายการที่ถูกลบได้อย่างปลอดภัย
ผู้ใช้ทั้งสามเห็นผลลัพธ์ แล้วสังเกตได้ว่าต่างคนต่างแตะข้อมูลเดียวกันพร้อมกันจนสถานะยุ่งเหยิง จากนั้นก็แก้ไขได้
ให้นึกถึงการแก้ไขเอกสาร
ถ้ามองไม่เห็นว่าอีกฝ่ายกำลังทำงานอยู่ ก็อาจแปลกใจได้ แต่สุดท้ายก็แก้ให้เป็นสถานะที่ต้องการได้
หากไม่ตรวจสอบผลลัพธ์หรือไม่สามารถตรวจสอบได้ สถานะก็จะไม่ถูกต้อง แต่ในแอปเชิงโต้ตอบอย่างการแก้ไขร่วมกันหรือเกม โดยปกติจะไม่ดำเนินไปแบบนั้น
ผมเป็นหนึ่งในคนที่เข้าร่วมโปรเจกต์นี้ ถ้ามีคำถามก็ตอบได้
เคยสร้างไลบรารี “redux-pubsub” ที่มี rebase และ server authority ด้วย ซึ่งเท่าที่เข้าใจคล้ายกับ TCR
โมเดลนี้มีหลายส่วนที่ชอบ และบทความที่ลิงก์ไว้ก็อธิบายได้ชัดเจนมาก
เห็นบอกว่า “การตรวจสอบสคีมาและ migration แทบจะได้มาฟรีตามธรรมชาติจากการออกแบบ” เลยอยากรู้ว่าในการ migration มีแนวทางไหนที่ใช้ได้ผลจริงบ้าง
อีกอย่าง ถ้าเป็น use case ที่ต้องจัดการการแก้ไขข้อความร่วมกันปริมาณมากในระบบ TCR ปกติผมคงนึกถึง Yjs กับ Tiptap/ProseMirror เป็นพื้นฐาน เลยสงสัยว่าวิธีที่ดีที่สุดคือวางเอกสาร CRDT กับเอกสาร TCR ไว้คู่ขนานกันหรือไม่
อยากทราบว่า codebase ฝั่งไคลเอนต์ส่วนใหญ่แชร์กันอยู่ จึงคาดหวังการอัปเดตต่อไปได้ไหม หรือมีโอกาสสูงที่ Reflect จะเข้ามาเป็นโฟกัสหลักแทน
คำศัพท์ชวนสับสนนิดหน่อย
ในเกมมี “ผู้เล่น” เลยเรียกระบบซิงก์ว่า “มัลติเพลเยอร์” แต่ในซอฟต์แวร์ทั่วไปมี “ผู้ใช้” ดังนั้นเรียกว่า หลายผู้ใช้ น่าจะเหมาะกว่า
ในหน้าเว็บใช้ทั้ง “ผู้ใช้” และ “มัลติเพลเยอร์” ปนกัน เลยอ่านแล้วรู้สึกแปลก
HN เป็นบริการแบบหลายผู้ใช้ แต่จะเรียก HN ว่ามัลติเพลเยอร์ก็ดูแปลก
การโต้ตอบพร้อมกันแบบเรียลไทม์มีอะไรที่แรงกว่าคำว่า หลายผู้ใช้ และคำว่า มัลติเพลเยอร์ ที่หมายถึงผู้ใช้หลายคนลงมือทำและโต้ตอบกัน ก็ดูเป็นการใช้คำที่โอเค
ในทางกลับกัน ซอฟต์แวร์เว็บทั้งหมดล้วนเป็นแบบหลายผู้ใช้อยู่แล้ว คำนั้นอย่างเดียวจึงไม่ได้ให้ข้อมูลอะไร
มัลติเพลเยอร์หมายถึง หลายผู้ใช้แบบไลฟ์ ที่เห็นได้ว่าผู้ใช้อื่นกำลังทำอะไรอยู่ในทุกขณะ
ช่วงประมาณ 2 ปีที่ผ่านมาได้ดู CRDT แบบคร่าว ๆ และสงสัยมาตลอดว่า การให้สิทธิ์ จัดการกันอย่างไร
บทความนี้ดูเหมือนจะชี้ว่าแค่ไลบรารี CRDT อย่าง Y.js อย่างเดียว ยากที่จะรองรับแอปพลิเคชันที่การเปลี่ยนแปลงต้องผ่านการตรวจสิทธิ์ได้ดี
เพราะไม่มี authority กลาง หรือก็คือเซิร์ฟเวอร์ และ Reflect ดูเหมือนจะสมมติว่าเซิร์ฟเวอร์เป็นตัวกลางในการไกล่เกลี่ยปฏิสัมพันธ์ของไคลเอนต์ เข้าใจแบบนี้ถูกไหม
ถ้าพยายามก็จัดการสิทธิ์ใน CRDT ได้ เช่น วางเซิร์ฟเวอร์คั่นระหว่างทุกอย่าง แล้วให้เซิร์ฟเวอร์ย้อนกลับการเปลี่ยนแปลงที่ไม่มีสิทธิ์ตามที่เซิร์ฟเวอร์เห็น
แต่ยิ่งแอปพลิเคชันใหญ่ขึ้น ก็ยิ่งต้องดูแลมากขึ้นและเปราะบางขึ้น แถมยังเสียข้อดีบางส่วนของ CRDT ไปตั้งแต่แรกด้วย
ถ้ามีเซิร์ฟเวอร์อยู่ตรงกลางอยู่แล้ว การใช้ โปรโตคอล ที่เซิร์ฟเวอร์ปฏิเสธข้อความได้ตั้งแต่ต้นจะเรียบง่ายกว่ามาก
เช่น การตรวจสอบสิทธิ์การเชื่อมต่อให้เซิร์ฟเวอร์รับผิดชอบได้
ถ้ามีคนเชื่อมต่อแบบ P2P แล้วอ้างสิทธิ์บางอย่าง ก็สามารถไปตรวจสอบคำอ้างนั้นกับเซิร์ฟเวอร์ได้
อีกทั้ง CRDT ไม่ได้แปลว่าต้องเป็น P2P เสมอไป และยังสามารถใช้เซิร์ฟเวอร์กลางส่งต่อข้อความ พร้อมคงโมเดล CRDT ที่ทั้งเซิร์ฟเวอร์และไคลเอนต์ resolve สถานะปัจจุบันได้
วิธีนี้ทำให้ CRDT ทำงานได้แม้ในบริบท P2P
แต่ถ้าเซิร์ฟเวอร์เป็น authority ก็แค่ปฏิเสธข้อความจากไคลเอนต์ที่ไม่มีสิทธิ์ได้เลย
อยากรู้ว่าจัดการ การอัปเกรด mutator อย่างไร
ถ้าไคลเอนต์ยังรันโค้ดเก่าอยู่ operation ก็จะต่างจากเซิร์ฟเวอร์ คำตอบที่ชัดเจนคือ version แยกกันอย่าง
increment_v1,increment_v2แต่สงสัยว่ามีวิธีที่ดีกว่านี้ไหมเพราะยังไม่ได้เปิด persistence ตอนนี้ช่วงเวลานั้นจึงค่อนข้างสั้น
ยินดีด้วยกับการเปิดตัว
อยากรู้ว่าอยู่ในสายเดียวกับ https://partykit.io/ หรือเปล่า
ความต่างหลักคือแต่ละฝั่งมี การออกแบบที่มีความเห็นชัดเจน แค่ไหน
PartyKit ไม่กำหนดแนวทางมากนัก และใกล้เคียงกับเซิร์ฟเวอร์ JavaScript น้ำหนักเบาที่เริ่มได้เร็วและ autoscale ได้
ส่วนใหญ่ดูเหมือนจะรัน yjs บน PartyKit แต่ก็รัน automerge หรือ Replicache ได้เช่นกัน
Reflect โฟกัสเต็มที่กับการมอบประสบการณ์มัลติเพลเยอร์ที่ดีที่สุดเท่าที่ทำได้ และมีแผนจะผสานตัวเลือกจำนวนมากทั่วทั้งสแตกอย่างแน่นหนา เพื่อให้มัลติเพลเยอร์ใช้งานได้เลย และให้โฟกัสกับการสร้างแอปพลิเคชัน
สำหรับข้อมูลเพิ่มเติม กลยุทธ์นี้ในวงการพัฒนาเกมเรียกว่า deterministic lockstep และมักใช้บ่อยเป็นพิเศษในเกมที่มีเอนทิตีจำนวนมากซึ่งต้องซิงโครไนซ์สถานะ
ตัวอย่างที่เด่นชัดคือเกมวางแผนแบบเรียลไทม์ ซึ่งแทบทั้งหมดใช้วิธีนี้
มีงานนำเสนอ GDC ที่ยอดเยี่ยมซึ่งอธิบายการใช้งานของ Mortal Kombat และ Injustice 2 อย่างลึกซึ้ง: https://youtu.be/7jb0FOcImdg
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 และทีม
หากมีคำถามจากมุมมองของลูกค้า ผม/ฉันสามารถตอบได้