- Vorstellung eines neuen Ansatzes zur Lösung des Problems kollaborativer Textbearbeitung, der sich auch ohne komplexe Algorithmen umsetzen lässt
- Vermeidet die Komplexität und Einschränkungen bestehender CRDT- und OT-Ansätze und nutzt stattdessen ein einfaches, ID-basiertes Einfügeverfahren
- Da der Server dabei direkt angewiesen wird, was wo eingefügt werden soll, ist dieser Ansatz besonders flexibel
- Zur optimistischen lokalen Aktualisierung wird eine Server-Reconciliation-Strategie verwendet, um Probleme bei der Zustandssynchronisierung zu lösen
- Mit seiner Skalierbarkeit und Verständlichkeit stellt dieser Ansatz eine direkt implementierbare Alternative für die Entwicklung kollaborativer Apps dar
Collaborative Text Editing without CRDTs or OT
Problemstellung
- Kollaborative Textbearbeitung ist eine sehr schwierige Funktion, insbesondere bei gleichzeitiger Bearbeitung tritt das Problem verzogener Textindizes (index rebasing) auf
- Die bisherigen Ansätze CRDT (Conflict-free Replicated Data Type) und OT (Operational Transformation) beruhen jeweils auf komplexen mathematischen Modellen
- CRDT: Verfolgt jedes Zeichen per ID und sortiert auf Baum-Basis
- OT: Passt Indizes dynamisch an, um Eingaben anderer Nutzer zu berücksichtigen
- Beide Ansätze sind stark von Bibliotheken abhängig und lassen sich nur schwer an individuelle Anforderungen von Entwicklern anpassen
Neuer Ansatz
Kernidee
- Jedes Zeichen erhält eine eindeutige ID (UUID), und der Client sendet dem Server den Befehl, „welches Zeichen nach welcher ID eingefügt werden soll“
- Beispiel: "insert ' the' after
f1bdb70a" → f1bdb70a ist die ID des Zeichens, nach dem eingefügt wird
- Der Server interpretiert dies direkt und fügt entsprechend ein, wodurch Konflikte vermieden werden
Umgang mit Löschungen
- Auch beim Löschen eines Zeichens bleibt dessen ID in der internen Liste erhalten und wird über ein
isDeleted-Flag verarbeitet
- Im sichtbaren Text erscheint es nicht mehr, Referenzen bleiben jedoch erhalten, sodass spätere Operationen weiterhin möglich sind
Client-Verarbeitung und optimistische Updates
- Nutzer müssen das Ergebnis direkt nach der Eingabe sehen können, daher wird es vor der Serverantwort lokal angewendet (optimistisches Update)
- Mithilfe einer Server-Reconciliation-Strategie wird dabei:
- jede lokale, noch unbestätigte Operation zurückgenommen
- die Server-Operation angewendet
- anschließend die lokalen Operationen erneut angewendet, um den final synchronisierten Zustand zu erhalten
Unterschiede zu bestehenden Ansätzen
- CRDT enthält einen Algorithmus zur automatischen ID-Sortierung, während dieser Ansatz dem Server nur die explizite Einfügeposition übermittelt
- Das Ergebnis ist insgesamt einfacher und sorgt für ein klar nachvollziehbares Verhalten
Behandlung gleichzeitiger Einfügungen
- Beispiel: In „My name is“ fügen zwei Nutzer gleichzeitig „ Charlie“ und „ Dave“ an derselben Stelle ein
- Je nach Reihenfolge des Servereingangs wird daraus „My name is Dave Charlie“
- Das gilt als natürliches Verhalten; im Unterschied zu manchen CRDT-Ansätzen kommt es nicht zu einem Vermischen auf Zeichenebene (interleaving)
Unterstützung flexibler Operationen
- Über einfaches Insert/Delete hinaus lassen sich verschiedene weitere Operationen unterstützen:
- insert-before
- bedingtes Einfügen (z. B. „u“ nur hinzufügen, wenn „color“ vorhanden ist)
- Neupositionierung bei Drag & Drop usw.
- Diese Flexibilität ist nicht an festgelegte mathematische Eigenschaften gebunden
Unterstützung für Rich Text
- Bereiche lassen sich ID-basiert definieren, um Formatierungen anzuwenden („fett von ID X bis ID Y“ usw.)
- Bei der Integration mit Editoren wie ProseMirror lassen sich Konflikte auf einfache Weise lösen
- Die Grundstruktur bleibt erhalten, während Rich-Text-Funktionen ergänzt werden können
Verteilte Version (Decentralized)
- Auch ohne zentralen Server kann der Ansatz funktionieren, wenn die Reihenfolge der Operationen über Lamport-Zeitstempel bestimmt wird
- In diesem Fall zeigt er ähnliche Ergebnisse wie CRDTs wie RGA, Peritext, Fugue
- Auch ohne Bäume oder mathematische Beweise lässt sich eine Konsistenz auf CRDT-Niveau erreichen
Begleitbibliothek: Articulated
- Eine Bibliothek für den effizienten Umgang mit Strukturen der Form
Array<{ id, isDeleted }>
- Verwendet statt UUIDs eine Struktur aus
{ bunchId, counter }, um den Speicherverbrauch zu optimieren
- Eine B+Tree-basierte Struktur unterstützt schnelles Finden und Einfügen per ID
- Als persistente Datenstruktur passt sie gut zu Server-Reconciliation
Fazit
- Dieser Ansatz ist im Vergleich zu CRDT/OT leichter zu verstehen und direkt implementierbar
- Verschiedene Bearbeitungsfunktionen, Berechtigungen, Einschränkungen und Formatierungen lassen sich frei anwenden, was für die praktische Umsetzung kollaborativer Editoren vorteilhaft ist
- Die Bibliothek Articulated wird als Werkzeug bereitgestellt, um diesen Ansatz in der Praxis nutzbar zu machen
1 Kommentare
Hacker-News-Kommentare
Der Algorithmus wirkt ziemlich clever. Es wird beschrieben, wie jedem Zeichen eine global eindeutige ID (z. B. UUID) zugewiesen wird, sodass es jederzeit konsistent referenziert werden kann; der Client teilt dem Server mit, dass ein Zeichen nach einer bestimmten ID eingefügt werden soll, der Server verarbeitet die Einfügung an dieser Position, Löschungen werden nur auf dem Bildschirm ausgeblendet und intern zur Positionsreferenz weiter aufbewahrt. Man kann sich vorstellen, dass sich dieser Ansatz nicht nur für Texteditoren, sondern auch für andere Bereiche wie die Synchronisierung von Spielwelten nutzen lässt.
ctrl+a,ctrl+xundctrl+v.Es wird die Sicht geteilt, dass sich der Unterschied zu CRDTs darin zeigt, dass ein zentraler Server Synchronisationsaufgaben wie die Sortierung der Reihenfolge übernimmt und man der eigentlichen Datenstruktur keine vorab festgelegte Ordnung geben muss. Da nur Kommunikation zwischen Client und Server stattfindet, kann der Server zunächst alle lokalen Operationen des Clients verarbeiten und erst danach Remote-Updates senden.
Es überrascht jemanden, dass nicht über andere Datenstrukturen wie dict/map oder Arrays beliebiger Typen gesprochen wird. In realen Apps braucht man oft mehr verschiedene kollaborative Datenstrukturen als reine kollaborative Textbearbeitung. Es gibt interessante Beispiele wie Datenvalidierung, partielles Laden oder Operationen auf höherer Ebene, und es wird vermutet, dass solche Funktionen in Yjs usw. weniger wegen CRDTs selbst fehlen, sondern eher weil sie generell schwer umzusetzen sind.
Es wird den Ausführungen zu verschiedenen Datenstrukturen stark zugestimmt: Wenn man ein Array „atomarer“ Objekte erstellt und Änderungen an den Properties der Objekte nicht möglich sind, muss man statt Strings nur den Typ austauschen; Änderungen innerhalb eines Objekts seien lediglich wegen Problemen bei Traversierung/Speicherung von Bäumen etwas komplexer. Außerdem wird erwartet, dass Nutzer von Helper-Libraries eigene Logik für ein „semantisches Modell“ wie einen Hook anschließen können, um ungültige Zustände zu verhindern, etwa wenn ein Todo
isDone: trueund zugleichstate: inProgresshat. Das stehe in einem ähnlichen Kontext wie die im verlinkten Beitrag erwähnte Rich-Text-Formatierung.CRDTs führen bei Konflikten eine Zusammenführung durch, indem sie konsistent eine Seite auswählen. Das Problem ist, dass dabei Datenverlust oder ungültige Daten entstehen können. Man solle sich das wie einen git merge vorstellen, bei dem Konflikte immer einfach durch die Wahl einer Seite gelöst werden — das würde in den meisten Fällen zu falschen Ergebnissen oder Compilerfehlern führen. Es wird darauf hingewiesen, dass sich durch rein automatisierte Auflösung Originalität und Bedeutung der Daten womöglich nicht angemessen bewahren lassen; darin wird ein Grund gesehen, warum sich CRDTs bislang nicht breit durchgesetzt haben.
Jemand fand den erklärenden Beitrag zu diesem Ansatz sehr interessant, habe selbst schon seit Langem dieselbe Methode verwendet und finde es bemerkenswert, dass sie in der Forschung fast nie erwähnt wird. Die Person sagt, sie habe das in einer dezentralisierten Umgebung als CRDT implementiert und dabei Eigenschaften wie Assoziativität, Immutability und Vertauschbarkeit erhalten können.
Es wird versucht, die Kernaussage des Artikels so zusammenzufassen, dass wirklich komplexe CRDTs/OT nur dann nötig sind, wenn es keinen zentralen Server gibt.
Auch ohne zentralen Server könne man die Komplexität von CRDT/OT vermeiden, wenn man in einem verteilten System einen Weg hat, sich auf eine globale Reihenfolge der Operationen zu einigen und diese anzuwenden. Ein verlinkter Beitrag wird genannt. Natürlich ist auch das wiederum eine Form von CRDT (in generalisierter Form), aber es wird betont, dass Undo/Replay nicht trivial zu implementieren ist und man es als Alternative in Betracht ziehen kann, wenn traditionelle CRDT/OT-Strukturen noch komplexer wirken.
Es wird angemerkt, dass OT (Operational Transformation) einen zentralen Server benötigt.
Im Kern ist es ebenfalls ein CRDT, nur dass der zentrale Server die Reihenfolge der auf das Dokument angewandten Operationen festlegt. Google Docs und Zoho Writer arbeiten ebenfalls mit OT plus zentralem Server. Es wird eingeräumt, dass der vorgestellte Ansatz zwar nach CRDT-Stil aussieht, für einen tatsächlich serverbasierten Dienst aber praktischer ist.
Eine Meinung lautet, dass der Hauptunterschied zu CRDTs wie Automerge in der Art der Server-Koordination liegt. Automerge sortiert gleichzeitige Einfügungen anhand einer definierten Ordnung aus Sequenznummern und Agent-ID, während der hier beschriebene Ansatz sie in der Reihenfolge des Eintreffens auf dem Server verarbeitet. Unter Verweis auf den beschriebenen Artikel wird zitiert: „Keine fancy Algorithmen nötig, also einfacher.“ Da viele Dienste ohnehin einen zentralen Server einsetzen, wirkt das praktisch, aber weil bei der Server-Koordination lokales Undo/Replay nötig ist, bleibt unklar, wie viel einfacher es in der Praxis wirklich ist.
Auch „rewind/replay“ wirke schon wie ein fancy Ansatz, und ein Persistent B+Tree sei ebenfalls nicht gerade simpel.
Auch Automerge kann intern letztlich eine globale Reihenfolge herstellen, verarbeitet Textoperationen in der Praxis jedoch mit traditionellen CRDT-Ansätzen (RGA usw.). Der Grund sei gerade, dass Undo/Replay nicht einfach ist.
Es wirkt wie ein nicht optimiertes CRDT — der scherzhafte Vergleich lautet, als würde man einfach
max set size=1setzen und es dann drauflos benutzen.Wenn man Server-Reconciliation verwendet, besteht das Risiko, dass das Merge-Problem auf Client-Seite schwierig wird. Es wird gefragt, wie sich die UX des Editors flüssig halten lässt, wenn Server-Updates nacheinander eintreffen. Was passiert etwa, wenn eine Einfügeanfrage fehlschlägt — wird sie erneut versucht, und was macht man, wenn in der Zwischenzeit andere Updates eintreffen? Als vorgeschlagene Lösungen werden das Zurückspulen und erneute Anwenden des Edit-Verlaufs oder das Warten mit anschließendem Queue-Flush genannt. Aus Frontend-Sicht scheint es etliche nicht explizit behandelte UI/UX-Edge-Cases zu geben, weshalb CRDTs am Ende sogar einfacher sein könnten. Insbesondere wird gefragt, wie die Benutzererfahrung in instabilen Netzwerken wie etwa in der U-Bahn aussieht.
Jemand schlägt vor, dass man mit einem LLM (z. B. einem 4b-Modell) versuchen könnte, Merge-Konflikte jenseits einfacher Fälle zu lösen, ohne besonders komplexe CRDTs oder OT zu verwenden. Die Energieeffizienz könnte zwar schlecht sein, aber überraschenderweise könnte es funktionieren.