← Gesamte Dokumentation

Yjs-CRDT-Engine pro Knoten

Referenz zu Architektur und Integration des Modus textSync: "crdt", ausgeliefert in v2.2. Die Verwendung von Anfang bis Ende zeigt die Demo zur Zusammenarbeit.

Was dieser Modus ist

editor.collab.attach({ doc, provider, textSync: "crdt" }) aktiviert eine Yjs-Bindung pro Absatz, die das bearbeitbare DOM in ein Y.XmlFragment in Ihrem gemeinsamen Y.Doc spiegelt. Bearbeiten zwei Benutzer gleichzeitig denselben Absatz, entstehen konfliktfreie Yjs-Operationen: Jedes Einfügen, Löschen oder Formatieren eines Bereichs trägt eine Yjs-Vektoruhr, und das CRDT führt sie deterministisch zusammen. Kein Last-Write-Wins, keine Cursorsprünge unter normaler Last, kein Proxy-Server nötig – die Engine läuft vollständig im Browser und arbeitet mit jedem Yjs-Provider zusammen (y-websocket, Hocuspocus, y-webrtc, Liveblocks oder jedem anderen CRDT-kompatiblen Transport). Warum diese Wahl in einer Demo kaum auffällt, in der Produktion aber viel ausmacht, beschreibt CRDT vs. OT: Wie kollaborative Editoren Änderungen zusammenführen.

Modusübersicht

textSyncFührt zuVerhalten
false (Standard)Handle nur für Präsenz, ohne FunktionNur Awareness / Cursor; keine Synchronisierung des Dokuments. Verwenden Sie das, wenn der Inhalt anderswo liegt (z. B. auf dem Server gespeichertes HTML).
true (Legacy)MVP im Snapshot-ModusEin gemeinsamer Y.Text enthält das HTML des Editors als einen undurchsichtigen String. Grobe Zusammenführung – Last-Write-Wins bei Konflikten im selben Absatz. Für die Abwärtskompatibilität mit Kunden von v2.0 beibehalten; 2027 veraltet.
"crdt" (empfohlen)Engine pro Knoten (diese Seite)Vollständiges CRDT auf Zeichenebene. Gleichzeitiges Tippen im selben Absatz wird zusammengeführt; Präsenz-Cursor über Y.RelativePosition; Y.UndoManager pro Autor.

Architektur

Die Engine wird als separates Plugin-Skript ausgeliefert (richtexteditor/plugins/crdt-engine.min.js, ~88 KB minifiziert), das window.RichTextEditorCrdt bereitstellt. Die Methode collab.attach des Plugins yjscollab prüft, ob dieses globale Objekt existiert; ist es vorhanden, überlässt sie die Synchronisierung des Inhalts der Engine und behält selbst das Awareness-Overlay, das Präsenz-Panel und die Brücke zum Review-Ledger.

   Browser editable (rte.js)
                          ↓ MutationObserver
                    [crdt-engine.js]
                          ↓ Y.Doc transactions (LOCAL_ORIGIN)
                       Y.XmlFragment
                          ↓ Yjs provider (y-websocket / Hocuspocus / etc.)
                        Network
                          ↓
                       Other peers' Y.Docs

   Other peer's Y.Doc updates
                          ↓ ydoc.on("update", origin !== LOCAL_ORIGIN)
                     [crdt-engine.js renders to DOM,
                      preserves selection via path-based capture]
                          ↓
                  Browser editable updates

Wofür die Engine zuständig ist

  • Bidirektionale Synchronisierung Y.XmlFragment ↔ DOM. Blockelemente, Blattelemente und Inline-Markierungen überstehen alle den Hin- und Rückweg. childList-Änderungen mitten im Baum (Listen, Tabellen) lösen gezielte Neuaufbauten von Teilbäumen aus statt grober Neudarstellungen des ganzen Dokuments.
  • Erhalt der Auswahl. Pfadbasierte Erfassung vor jeder durch entfernte Änderungen ausgelösten Neudarstellung, danach wiederhergestellt – der Cursor des Benutzers bleibt an seinem Platz, wenn Mitwirkende anderswo tippen.
  • Echo-Vermeidung. Lokale Änderungen werden mit LOCAL_ORIGIN gekennzeichnet; der Handler für entfernte Änderungen ignoriert sie. MutationObserver.takeRecords() wird nach jeder entfernten Darstellung synchron geleert, damit der asynchrone Callback des Observers die Ausgabe lokaler Änderungen nicht im Kreis zurück in das Y.Doc schickt.
  • Cursor-Präsenz. Auswahlpositionen werden als Y.RelativePosition-Bytes kodiert – stabile Bezeichner, die gleichzeitiges Einfügen und Löschen überstehen – und über den Awareness-Kanal veröffentlicht; Peers dekodieren sie beim Empfang, um entfernte Cursor auf ihr lokales DOM zu projizieren.
  • Rückgängig pro Autor. Y.UndoManager mit trackedOrigins: { LOCAL_ORIGIN } – Strg+Z macht nur die Änderungen des lokalen Benutzers rückgängig, nie die der Mitwirkenden.

Wofür der Host zuständig ist

  • Symbolleiste, Menüs, Dialoge, Behandlung beim Einfügen.
  • Verbindung und Authentifizierung des Yjs-Providers (die Engine kommuniziert nicht mit dem Netzwerk).
  • Zusammenstellung der Awareness-Daten (Benutzeridentität, Farbe, Name).
  • Zeichnen des Cursor-Overlays – die Engine liefert Positionsdaten, der Host zeichnet.
  • Darstellung von Konflikten (Namen von Mitwirkenden beim Überfahren anzeigen, ihre Auswahlrechtecke hervorheben usw.).

Integrationscode

Vanilla JS

<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/richtexteditor/plugins/crdt-engine.min.js"></script>

<script type="module">
    import * as Y from "https://esm.sh/yjs";
    import { WebsocketProvider } from "https://esm.sh/y-websocket";

    const editor = new RichTextEditor("#editor", {
        commentsEnabled: true,
        trackChangesEnabled: true,
    });

    const ydoc = new Y.Doc();
    const provider = new WebsocketProvider("wss://your-yjs-server", "doc-1", ydoc);

    editor.collab.attach({
        doc: ydoc,
        provider: provider,
        Y: Y,                                 // your Yjs module — lets the engine bridge a doc built by it
        textSync: "crdt",                    // ← per-node CRDT
        requireCrdt: true,                    // fail closed if the engine is missing
        user: { id: "u-1", name: "Alice", color: "#2563eb" }
    });
</script>

Den angebundenen Modus prüfen

Integrationen in der Produktion sollten den Modus nach dem Anbinden prüfen. getStatus().yjsBridge ist true, wenn Ihr Y.Doc von einer anderen Yjs-Instanz stammt als der, die die Engine mitbringt, und das Plugin Updates zwischen beiden weiterleitet (übergeben Sie Y an attach, um das zu aktivieren). requireCrdt: true hält Präsenz und Review-Ledger verfügbar, wenn die Engine nicht starten kann, schaltet Änderungen am Dokument aber nicht stillschweigend in den alten Snapshot-Modus. Verwenden Sie onCollabStatus für zentrale Telemetrie oder eine sichtbare Meldung zur Wiederherstellung.

const editor = new RichTextEditor(host, {
  onCollabStatus(status) {
    if (status.fallbackReason) reportCollaborationIssue(status.fallbackReason);
  }
});

const status = editor.collab.getStatus();
if (status.textSyncMode !== "crdt") {
  reportCollaborationIssue(status.fallbackReason);
}

React

import { useEffect, useRef } from "react";
import { RichTextEditorComponent } from "@richscripts/richtexteditor/react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";

export function CollabEditor({ docId, user }) {
    const editorRef = useRef();

    useEffect(() => {
        const editor = editorRef.current.getEditor();
        const ydoc = new Y.Doc();
        const provider = new WebsocketProvider(
            "wss://your-yjs-server", docId, ydoc,
        );
        editor.collab.attach({
            doc: ydoc, provider, Y, textSync: "crdt", user,
        });
        return () => { provider.destroy(); ydoc.destroy(); };
    }, [docId]);

    return (
        <RichTextEditorComponent
            ref={editorRef}
            config={{ commentsEnabled: true, trackChangesEnabled: true }}
        />
    );
}

Migration von textSync: true

Zwei Wege, je nachdem, ob Sie hart umstellen oder beide Modi nebeneinander ausrollen wollen.

Weg A – Neuanfang (empfohlen)

Erzeugen Sie ein neues Y.Doc mit einem anderen Schlüssel (hängen Sie z. B. "-crdt" an Ihre bestehende Dokument-ID an). Beim ersten Anbinden eines Dokuments führen Sie die Migrationshilfe der Engine aus, um das neue Dokument aus Ihrem gespeicherten HTML zu füllen:

const newDoc = new Y.Doc();
window.RichTextEditorCrdt.htmlToYFragment({
    ydoc: newDoc,
    html: legacyHtmlString,
    parserDocument: document,
});
editor.collab.attach({
    doc: newDoc,
    provider: new WebsocketProvider(url, docId + "-crdt", newDoc),
    Y,
    textSync: "crdt",
});

Weg B – Umstellung im Parallelbetrieb

Betreiben Sie beide Modi während eines Rollouts per Feature-Flag parallel. Wichtig: Alle Mitwirkenden an einem Dokument müssen denselben Modus verwenden; werden der alte Snapshot-Modus und der CRDT-Modus auf demselben Y.Doc gemischt, entsteht ein inkonsistenter Zustand. Setzen Sie das Flag pro Mandant.

const mode = featureFlag("rte_crdt_for_tenant_" + tenantId) ? "crdt" : true;
editor.collab.attach({ doc: ydoc, provider, textSync: mode });

Zeitplan für die Abkündigung (vorläufig)

DatumEreignis
Q2 2026textSync: "crdt" wird hinter einem Opt-in-Flag ausgeliefert; textSync: true bleibt der Standard (keine Breaking Change).
Q4 2026textSync: "crdt" wird zum Standard; textSync: true funktioniert weiterhin über die Kompatibilitätsschicht.
Q2 2027textSync: true gibt zur Laufzeit eine Abkündigungswarnung aus.
Q4 2027textSync: true wird entfernt. Kunden müssen "crdt" verwenden.

Technische Details

Quellcodestruktur, Testkorpus und Dokumentation für Mitwirkende liegen neben der Engine im internen Workspace RichTextEditorCloud/CrdtEngine/. Die öffentliche API wird über window.RichTextEditorCrdt.* exportiert:

  • attachCollab(opts) – der Dispatcher; verteilt nach dem Modus textSync
  • attachCrdtBinding(opts) – direkter Einstieg in die Bindung (ohne Dispatcher)
  • attachAwareness(opts) – Verdrahtung der Cursor-Präsenz
  • attachUndoManager(opts) – Wrapper für Y.UndoManager pro Autor
  • htmlToYFragment(opts) – einmalige Migrationshilfe von HTML → Y pro Knoten
  • LOCAL_ORIGIN – Symbol, mit dem lokale Yjs-Transaktionen zur Echo-Vermeidung gekennzeichnet werden