7 पॉइंट द्वारा GN⁺ 2023-10-19 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Reflect एक ऐसा फ्रेमवर्क है जिसे Figma, Notion, Google Sheets जैसे collaborative apps जल्दी बनाने के लिए पेश किया गया है, जिसमें Replicache के game-style synchronization engine के साथ fully managed server जोड़ा गया है
  • collaborative UI को server response का इंतज़ार किए बिना local changes तुरंत दिखाने चाहिए, इसलिए जब एक ही data को एक साथ बदला जाता है तब conflict handling का तरीका product experience तय करता है
  • Reflect, CRDT के बजाय Transactional Conflict Resolution चुनता है, जिसमें client और server दोनों एक ही mutator call history चलाते हैं और server arrival order के अनुसार authoritative state बनाता है
  • Yjs जैसे sequence CRDT text, list और map के लिए मजबूत हैं, लेकिन counter जैसे मामलों में जहाँ अलग merge rules चाहिए, वहाँ increment खो सकते हैं; Reflect mutator re-execution के ज़रिए arithmetic, list operations और higher-level invariants संभालता है
  • server केवल mutation name और arguments लेकर result को फिर से compute करता है, इसलिए client के computed result पर भरोसा नहीं किया जाता और permission checks, schema validation और migrations को design में शामिल करना आसान हो जाता है

Reflect का सार्वजनिक किया गया डेवलपर अनुभव

  • Reflect Figma, Notion, Google Sheets जैसे multiplayer web apps बनाने का एक नया तरीका है
  • यह मौजूदा client-side sync framework Replicache का विकसित रूप है, और वही game-style synchronization engine इस्तेमाल करता है
  • Replicache से अलग, इसमें fully managed server शामिल है, ताकि कुछ ही मिनटों में high-quality multiplayer apps बनाए जा सकें
  • Reflect पहली बार सार्वजनिक रूप से उपलब्ध कराया गया है, और इसका परिचय reflect.net पर देखा जा सकता है तथा शुरुआत hello.reflect.net से की जा सकती है

collaborative editing में conflict क्यों बनते हैं

  • collaborative editing में conflict होना अनिवार्य है
  • तुरंत प्रतिक्रिया देने वाला UI बनाने के लिए server का इंतज़ार नहीं किया जा सकता, और changes पहले client local पर होने चाहिए
  • कई users एक ही item को एक साथ edit कर सकते हैं, इसलिए conflicts को sync और स्वाभाविक तरीके से resolve करना ज़रूरी है ताकि सभी users को एक जैसा result दिखे
  • synchronization engine developer experience, user experience, achievable performance, और किस तरह के apps बनाए जा सकते हैं — इन सब पर असर डालता है

CRDT और Yjs counter उदाहरण

  • web ecosystem में CRDT data synchronization के लिए व्यापक रूप से इस्तेमाल होता है
  • CRDT ऐसे data structures हैं जो collaborators के बीच सभी changes exchange होने के बाद एक ही value पर converge करते हैं, और Yjs तथा Automerge इसके प्रमुख open source CRDT libraries हैं
  • Reflect, CRDT नहीं है; यह video game industry में लंबे समय से इस्तेमाल होने वाले Server Reconciliation के बदले हुए रूप Transactional Conflict Resolution का उपयोग करता है
  • Yjs में साधारण counter क्यों टूट जाता है

    • Yjs Map में count value स्टोर करके prev + 1 दोबारा लिखने का तरीका concurrency में increment खो सकता है
    • Yjs documentation में सही counter उदाहरण numbers को array में जोड़कर total compute करने का है
    • Yjs एक sequence CRDT है, इसलिए यह list, text fragments और map में मजबूत है, लेकिन counter को स्वाभाविक रूप से model करना कठिन है
    • Yjs Map का merge algorithm key के हिसाब से last-write wins तरीका अपनाता है, इसलिए अगर दो users एक साथ increment करें तो उनमें से एक change गायब हो सकता है
    • CRDT कुछ खास समस्याओं के लिए बहुत उपयुक्त है, लेकिन जब समस्या वैसी न हो तो उसे extend करना कठिन हो सकता है

Transactional Conflict Resolution कैसे काम करता है

  • Reflect में changes को mutator नाम की विशेष JavaScript function से implement किया जाता है
  • हर mutator की copy सभी clients और server पर मौजूद होती है
  • जब user कोई change करता है, Reflect mutator call record के रूप में एक mutation बनाता है
    • mutation में केवल mutator name और arguments होते हैं, जैसे increment(delta: 1)
    • उससे बने actual changes mutation में शामिल नहीं होते
  • Reflect mutation को तुरंत local पर apply करके UI update कर देता है, इसलिए user अपने changes तुरंत देख सकता है
  • server linearization और re-execution

    • हर client server का इंतज़ार किए बिना लगातार mutation जोड़ता रहता है
    • mutations server पर stream होते हैं, और server arrival time के क्रम में mutation को linearize करके अगली authoritative state बनाता है
    • उदाहरण के लिए, अगर client 1 का increment(1) और client 2 का increment(2) एक साथ हों, तो server execution में arrival order के अनुसार final count बनता है
    • server को increment क्या करता है या उसे कैसे merge करना है, इसकी अलग जानकारी की ज़रूरत नहीं होती; वह execution history को linearize करके conflicts merge करता है
    • नई authoritative state लगातार हर client तक stream की जाती है
    • client जब देखता है कि उसकी pending mutation authoritative state में apply हो चुकी है, तो उसे local queue से हटा देता है
    • बाकी pending mutations को नई authoritative state के ऊपर mutator code फिर चलाकर rebase किया जाता है
    • यह पूरा cycle हर client पर प्रति सेकंड अधिकतम 120 बार तक हो सकता है

implementation cost और generalize होने वाले फायदे

  • इस तरीके को implement करने के लिए ऐसा तेज datastore चाहिए जो rewind, fork और branch बना सके
  • server side पर भी incoming mutations process करने के लिए fast storage चाहिए
  • mutator synchronization का तरीका, और sync के दौरान client या server में conflict होने पर recovery handling भी चाहिए
  • इसके बदले arbitrary functions की linearization काफ़ी general-purpose synchronization strategy की तरह काम करती है
  • अलग synchronization code के बिना संभाले जाने वाले उदाहरण

    • arithmetic operations स्वाभाविक रूप से संभल जाते हैं
    • setHighScore मौजूदा high-score और candidate score में से बड़ा value स्टोर करता है
    • ज़्यादातर list operations भी काम करते हैं
    • append shopping list के अंत में item जोड़ता है
    • insertAt तय position पर item insert करता है, और splice() positions को adjust करता है
    • remove में index बदल सकता है, इसलिए item या stable ID को argument के रूप में देना चाहिए
    • higher-level invariants भी enforce किए जा सकते हैं
    • addChild parent के childIDs और child के parentID को साथ update करता है ताकि वे हमेशा consistent रहें
    • ऐसे उदाहरण synchronization-aware अलग code के बिना भी उचित तरीके से merge हो जाते हैं

server authority और permission checks

  • Reflect में server ही authority है
  • client अपने change के result के बारे में क्या सोचता है, यह server या दूसरे clients के साथ share नहीं किया जाता
  • server को केवल mutation name और arguments भेजे जाते हैं, और server खुद mutation result को दोबारा compute करता है
  • server को client जैसा ही code चलाने की ज़रूरत भी नहीं है; वह external services query कर सकता है या random numbers इस्तेमाल कर सकता है
  • granular authorization

    • इस design में granular permission checks को स्वाभाविक रूप से जोड़ा जा सकता है
    • उदाहरण के लिए, किसी collaborative design program में guest को comments और highlights की अनुमति हो लेकिन actual design changes की नहीं
    • CRDT में unauthorized changes को reject करने वाला logic रखने की सही जगह नहीं होती, इसलिए इसे implement करना कठिन है
    • Reflect में server पर चलने वाला mutator tx.user.canEdit जैसे value को check करके permission न होने पर unauthorized error throw कर सकता है
    • mutator का server पर client से अलग code चलाना भी समस्या नहीं है, क्योंकि अंतिम फैसला server लेता है

schema validation और उपयोग मार्गदर्शन

  • Reflect के approach में schema validation और migrations भी design में स्वाभाविक रूप से शामिल किए जा सकते हैं
  • synchronization strategy का चुनाव multiplayer systems का मुख्य हिस्सा है, और Reflect game industry के approach से सीखे गए Transactional Conflict Resolution को एक सरल, flexible और शक्तिशाली तरीका मानता है
  • अगर आप multiplayer app बना रहे हैं, तो Reflect start page पर इसे आज़मा सकते हैं
  • development team से बात करने के लिए discord.reflect.net या @hello_reflect का उपयोग किया जा सकता है

1 टिप्पणियां

 
GN⁺ 2023-10-19
Hacker News की राय
  • homepage के ऊपर वाला demo (https://reflect.net/) काफ़ी मज़ेदार है
    देखते-देखते जब भी puzzle पूरा होता है, लोग “हमने कर दिखाया!” जैसा महसूस करते हुए cursor हिलाकर खुशी जताते हैं

    • किसी ने pieces से FUCK शब्द बनाना शुरू किया, और जब दूसरे लोगों ने ध्यान देकर साथ देना शुरू किया, तो यह अप्रत्याशित रूप से काफ़ी wholesome अनुभव बन गया
    • पूरे हो चुके e वाले हिस्से के पीछे e piece छिपाने वाला एक visual bug है
      बाकी अक्षरों में outline लगातार दिखती रहती है, इसलिए उनके साथ उसी तरह नहीं हो पाता
    • मुझे नहीं लगता कि यह demo library की capability दिखाने का अच्छा उदाहरण है
      जब आप कोई piece पकड़ते हैं, तो वह piece उस user के लिए lock हो जाता है, इसलिए resolve करने के लिए conflict लगभग नहीं बचता; बस अगर दो लोग एक साथ उठाएं तो पहले उठाने वाले user को दे देना जैसा मामला रह जाता है
      मैं कोई बेहतर example demo देखना चाहूंगा जहां सच में conflict resolution होता हो
    • यह सचमुच मज़ेदार अनुभव है
      जिस scene का वर्णन किया गया है, उसका video यहां है: https://streamable.com/asu261
  • आपको याद हो सकता है कि यह पहले Replicache के रूप में एक-दो बार आया था
    Reflect, उसमें पूरी तरह managed और बेहद तेज़ sync server जोड़ने जैसा है
    local-first/realtime क्षेत्र इन दिनों काफ़ी crowded है, लेकिन Replicache/Reflect का data model और coding model खूबसूरती से सरल है, इसलिए इन्हें देखना worthwhile है
    CRDT की तुलना में फायदा यह है कि आप conflicts को सीधे सरल sequential code में handle कर सकते हैं; जब built-in rules fit न बैठें, तब application-specific conflict resolution को CRDT में जोड़ना जटिल हो सकता है

    • पूरी तरह सहमत हूं
      PowerSync ने भी CRDT के बजाय server reconciliation architecture चुना, और जहां central server मौजूद हो ऐसे applications में इस structure की simplicity बहुत आकर्षक लगती है
      server reconciliation concept को सामने लाने और फैलाने का श्रेय Aaron को देना चाहूंगा: https://www.gabrielgambetta.com/client-side-prediction-serve...
    • इससे जुड़े पुराने threads ये हैं
      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 साल पहले दादी के घर boredom से बचने के लिए मैंने इस topic को काफी गहराई से explore किया था, और JavaScript implementation भी बनाया था
    “arithmetic operations बस काम कर जाते हैं” वाली बात तो जाहिर है कि idempotent operations बनाना बहुत आसान है, लेकिन “list operations भी बस काम कर जाते हैं” से सहमत होना मुश्किल है
    उदाहरण के लिए [1, 2, 3, 4, 5] जैसे array में मान लें A range 2~4 delete करता है, B range 3~5 delete करता है, और C 3 और 4 के बीच कुछ insert करता है; जब ये तीनों updates एक साथ server पर पहुंचें, तो solution ambiguous हो जाता है
    timestamp या Last Write Wins पर निर्भर करने से transaction model टूट जाता है, और अगर किसी एक को winner चुना जाए तो यह भी समस्या है कि बाकी users क्या देखेंगे और इसे communicate कैसे किया जाएगा
    अगर जवाब “पूरा array फिर से भेजो” है, तो तब भी transaction model टूटता है

    • आपकी बात सही है
      असल में intended wording “कई list operations बस काम कर जाते हैं” के ज्यादा करीब थी
      Reflect में edit/delete identifier के रूप में list index इस्तेमाल नहीं किया जा सकता, क्योंकि index stable नहीं होते; अगर atomic हो तो item itself, और आम तौर पर stable ID इस्तेमाल करने को कहने की वजह यही है: https://i.imgur.com/IKzmf0q.png
      C का insertion delete हो जाने की समस्या भी है, लेकिन realtime collaboration context में कोई भी protocol इसे पूरी तरह solve नहीं कर सकता
      C के नजरिए से, अभी-अभी लिखा content गायब हो जाना दुखद हो सकता है, और यह वही phenomenon है जो एक ही space में साथ काम कर रहे humans के अलग-अलग intentions से पैदा होता है; इसलिए undo और current collaborators दिखाने जैसे social mechanisms मददगार होते हैं
    • deletion को शामिल न भी करें, तो अगर “simultaneously” A list L में “a” जोड़ता है, B “b” जोड़ता है, और C “c” जोड़ता है, server arbitrary order में additions apply करके result clients को भेजता है
      इस दौरान हर client अपना addition locally apply कर सकता है, इसलिए A [“a”], B [“b”], C [“c”] देख रहे हो सकते हैं; और अगर server [“c”, “b”, “a”] state भेजता है, तो लगता है clients pending changes छोड़कर server state को world की truth मान लेंगे
      लेकिन अगर हर addition का effect कुछ ऐसा हो कि “मेरी change पहले apply हुई तो मैं जीतता हूं”, तो उत्सुकता है कि server update का इंतज़ार करने वाले 300ms के दौरान क्या सबको “you win” दिखेगा
    • ऐसे systems में ज्यादातर deletion को tombstone के रूप में handle किया जाता है
      तब deleted items के बाद या उनके बीच safely insert किया जा सकता है
    • collaborative applications में server तीनों operations को किसी भी order में resolve कर सकता है
      तीनों users result देखकर समझ सकते हैं कि उन्होंने same data को simultaneously touch किया और state गड़बड़ा गई है, फिर उसे ठीक कर सकते हैं
      document editing के बारे में सोचिए
      अगर वे एक-दूसरे को काम करते हुए नहीं देख सकते तो चौंक सकते हैं, लेकिन आखिरकार desired state में सुधार कर सकते हैं
      अगर result देखा नहीं जाता या देखा नहीं जा सकता, तो state सही नहीं होगी, लेकिन collaborative editing या games जैसे interactive apps में आम तौर पर flow ऐसा नहीं होता
  • मैं इस प्रोजेक्ट में शामिल लोगों में से एक हूँ, और अगर सवाल हों तो जवाब दे सकता हूँ

    • Aaron विनम्र हैं, इसलिए उनकी जगह कहूँ तो उन्होंने Greasemonkey बनाया था और कई वर्षों तक कई अहम browser innovations से जुड़े रहे हैं
    • पहले private codebase में कई multiplayer synchronization systems बनाते हुए मैं इस क्षेत्र पर नज़र रखता रहा हूँ
      मैंने rebase और server authority वाली एक “redux-pubsub” library भी बनाई थी, और मेरी समझ के अनुसार वह TCR जैसी है
      इस model में मुझे कई चीज़ें पसंद हैं, और linked article भी बहुत स्पष्ट है
      आपने कहा था कि “schema validation और migrations design से स्वाभाविक रूप से लगभग मुफ्त में मिल जाते हैं”; मुझे जानना है कि migrations में वास्तव में कौन-सा तरीका अच्छा चला
      और TCR system में अगर काफी मात्रा में shared text editing संभालने वाला use case हो, तो आम तौर पर Yjs और Tiptap/ProseMirror ही default विकल्प लगते हैं; सोच रहा हूँ कि क्या CRDT document और TCR document को parallel रखना ही सबसे अच्छा तरीका है
    • आगे Replicache development का क्या होगा, यह जानना चाहता हूँ
      क्या client-side codebase ज़्यादातर shared है, इसलिए आगे भी updates की उम्मीद रखी जा सकती है, या Reflect के primary focus बनकर उसकी जगह लेने की संभावना ज़्यादा है?
    • ElectricSQL के बारे में आपकी क्या राय है, यह जानना चाहता हूँ
    • एक room में एक साथ आ सकने वाले maximum users कितने हो सकते हैं, यह जानना चाहता हूँ
  • terminology थोड़ी confusing है
    games में “players” होते हैं, इसलिए synchronization system को “multiplayer” कहना ठीक है, लेकिन सामान्य software में “users” होते हैं, इसलिए multi-user कहना ज़्यादा सही लगता है
    page पर “users” और “multiplayer” को मिलाकर इस्तेमाल किया गया है, इसलिए पढ़ने में अटपटा लगता है

    • हो सकता है, लेकिन “user” consumer जैसा सुनाई देता है
      HN एक multi-user service है, लेकिन HN को multiplayer कहना अजीब होगा
      real-time simultaneous interaction में multi-user से कुछ ज़्यादा मजबूत बात है, और कई users के action लेने और interact करने के अर्थ में multiplayer ठीक usage लगता है
    • यह logic दशकों से multiplayer games में इस्तेमाल हो रहे logic पर आधारित है
      इसके उलट, सभी web software multi-user होते हैं, इसलिए सिर्फ वह शब्द कोई जानकारी नहीं देता
    • आजकल terminology बस ऐसे ही establish हो गई है
      multiplayer का मतलब live multi-user है, जहाँ दूसरे users हर पल क्या कर रहे हैं यह visualize होता है
  • लगभग 2 साल तक CRDT को हल्के तौर पर देखते हुए मुझे हमेशा यह जिज्ञासा रही कि authorization कैसे होता है
    यह लेख संकेत देता लगता है कि Y.js जैसी CRDT libraries भर से उन applications को सही से संभालना मुश्किल है जिनमें changes को authorization checks से गुजरना पड़ता है
    वजह यह है कि कोई central authority, यानी server, नहीं होता; और Reflect मानता दिखता है कि server client interactions को mediate करता है—क्या मेरी यह समझ सही है?

    • “नहीं कर सकते” कहना थोड़ा मजबूत है
      मेहनत करें तो CRDT में भी authorization handle किया जा सकता है; उदाहरण के लिए हर चीज़ के बीच server रखकर, server को दिखने वाले unauthorized changes को revert कराया जा सकता है
      लेकिन application बढ़ने पर maintenance बढ़ती है और चीज़ें fragile हो जाती हैं, और शुरुआत में CRDT के कुछ फायदे भी खत्म हो जाते हैं
      अगर server पहले से बीच में है, तो ऐसा protocol इस्तेमाल करना कहीं सरल है जिसमें server शुरू से ही messages reject कर सके
    • CRDT में भी server शामिल हो सकता है, बस server का हर चीज़ के बीचोंबीच होना ज़रूरी नहीं
      उदाहरण के लिए connection authentication server संभाल सकता है
      अगर कोई P2P से connect होकर कोई खास permission claim करे, तो उस claim को server से verify किया जा सकता है
      साथ ही CRDT का मतलब अनिवार्य रूप से P2P नहीं है; central server से messages relay कराते हुए भी server और client दोनों तरफ current state resolve करने वाला CRDT model बनाए रखा जा सकता है
    • हर operation या message पर signature करके और access control list की public key से match करता है या नहीं, यह verify करके authorization handle किया जा सकता है
      यह तरीका CRDT को P2P context में भी काम करने देता है
      हालांकि अगर server authority है, तो unauthorized client के messages को सीधे reject करना ही काफी है
  • mutator upgrade कैसे handle करते हैं, यह जानना चाहता हूँ
    अगर client पुराना code चला रहा हो तो operation server से अलग हो जाएगा; obvious answer तो increment_v1, increment_v2 की तरह अलग-अलग versioning करना है, लेकिन क्या कोई बेहतर तरीका है?

    • फिलहाल, जब तक यह पता न चल जाए कि pending changes वाले clients अब नहीं बचे हैं, पुराना mutator न हटाने से बेहतर जवाब नहीं है
      अभी persistence enable नहीं किया है, इसलिए फिलहाल यह अवधि काफी छोटी है
  • launch के लिए बधाई
    क्या यह https://partykit.io/ जैसी ही category में आता है?

    • हाँ, same space में है
      core difference यह है कि किसका design कितना opinionated है
      PartyKit बहुत non-prescriptive है, और जल्दी शुरू होने वाले, auto-scaling lightweight JavaScript server के ज़्यादा करीब है
      लगता है ज़्यादातर लोग PartyKit पर yjs चलाते हैं, लेकिन automerge या Replicache भी चला सकते हैं
      Reflect पूरी तरह best possible multiplayer experience देने पर focused है, और plan यह है कि पूरे stack में कई choices को tightly integrate करके multiplayer को बस काम करने लायक बनाया जाए, ताकि application implementation पर focus किया जा सके
  • संदर्भ के लिए, इस रणनीति को गेम डेवलपमेंट में deterministic lockstep कहा जाता है, और यह खास तौर पर उन games में बहुत इस्तेमाल होती है जिनमें state synchronize करने वाली entities की संख्या ज्यादा होती है
    इसका प्रमुख उदाहरण real-time strategy games हैं, जिनमें लगभग सभी इसी तरीके का उपयोग करते हैं

    • और सटीक कहें तो, चूंकि client local बदलाव दिखाने से पहले server response का इंतज़ार नहीं करता, यह deterministic rollback के ज्यादा करीब है
      Mortal Kombat और Injustice 2 के implementation को गहराई से समझाने वाली एक शानदार GDC presentation है: https://youtu.be/7jb0FOcImdg
    • यह deterministic lockstep नहीं है
      deterministic lockstep एक P2P game algorithm है, जिसमें हर participant game simulation आगे बढ़ाने से पहले बाकी सभी players के inputs का इंतज़ार करता है
      इसे “deterministic” इसलिए कहा जाता है क्योंकि simulation result नहीं, बल्कि inputs share किए जाते हैं और समान inputs के लिए simulation deterministic होती है; और “lockstep” इसलिए क्योंकि सभी clients समन्वित speed से आगे बढ़ते हैं
      Age of Empires series इसी तरीके का उपयोग करती है, इसलिए click करने पर units तुरंत move नहीं करते; StarCraft भी यही इस्तेमाल करता है, लेकिन play feel को smooth बनाने की कुछ तरकीबें हैं
      Reflect P2P नहीं है, बल्कि server-authoritative simulation के ज्यादा करीब है
      client input server को भेजता है, लेकिन result का इंतज़ार किए बिना local रूप से predict करता है, और server individual client latency की भरपाई करने के लिए time को rewind करके inputs replay करता है
      इसके बाद client जब अपने input वाला server result प्राप्त करता है, तो local simulation को correct करता है
      इस algorithm के key terms हैं server authority, prediction, latency compensation, और prediction reconciliation
      Reflect में यह है या नहीं, पता नहीं, लेकिन FPS games में world update मिलने पर entities को कुछ समय तक नई position और rotation तक interpolate करने वाली client-side interpolation भी आम है
      चूंकि authority सिर्फ एक server के पास होती है, determinism बहुत महत्वपूर्ण नहीं है, लेकिन client prediction में misprediction घटाने के लिए उपयोगी है
      misprediction तब होता है जब दूसरे clients के inputs world state को काफी बदल देते हैं, या random number generation synchronize नहीं होती—यानी simulation deterministic नहीं होती
      Counter Strike में “nospread” cheat रोकने के लिए bullet spread के random numbers को synchronize न करने का उदाहरण भी है
      https://www.gabrielgambetta.com/client-side-prediction-live-...
      https://developer.valvesoftware.com/wiki/Latency_Compensatin...
      https://developer.valvesoftware.com/wiki/Source_Multiplayer_...
  • Reflect बेहतरीन है
    हम अभी production में alpha version इस्तेमाल कर रहे हैं, और system के साथ-साथ Aaron और उनकी team से भी बहुत संतुष्ट हैं
    ग्राहक के तौर पर अगर कोई सवाल हों, तो मैं जवाब दे सकता हूं