← すべてのドキュメント

ノード単位の Yjs CRDT エンジン

v2.2 で提供された textSync: "crdt" モードのアーキテクチャと組み込みのリファレンスです。エンドツーエンドの使い方については、共同編集のデモを参照してください。

このモードとは

editor.collab.attach({ doc, provider, textSync: "crdt" }) は、段落単位の Yjs バインディングを有効にし、編集可能な DOM を共有の Y.Doc 内の Y.XmlFragment に反映します。2 人のユーザーが同時に同じ段落を編集しても、競合しない Yjs の操作になります。挿入/削除/書式設定の各範囲は Yjs のベクタークロックを持ち、CRDT がそれらを決定的にマージします。後勝ち(last-write-wins)はなく、通常の負荷ではキャレットが飛ぶこともなく、プロキシサーバーも不要です。エンジンは完全にブラウザー内で動作し、任意の Yjs プロバイダー(y-websocket、Hocuspocus、y-webrtc、Liveblocks、その他の CRDT 互換の通信手段)と組み合わせられます。この選択がデモではほとんど目立たないのに、本番環境では非常に重要になる理由については、CRDT と OT:共同編集エディターが編集をマージする方法を参照してください。

モードの一覧

textSync処理先動作
false(既定)プレゼンスのみの、何もしないハンドルアウェアネス/カーソルのみで、文書は同期しません。コンテンツが別の場所にある場合(サーバーに保存した HTML など)に使います。
true(従来)スナップショットモードの MVP共有の Y.Text が、エディターの HTML を 1 つの不透明な文字列として保持します。マージは大まかで、同じ段落での競合は後勝ちになります。v2.0 の顧客との後方互換性のために残しており、2027 年に非推奨になります。
"crdt"(推奨)ノード単位のエンジン(このページ)完全な文字単位の CRDT。同じ段落での同時入力はマージされ、プレゼンスのカーソルは Y.RelativePosition で表し、作成者ごとの Y.UndoManager を使います。

アーキテクチャ

エンジンは、window.RichTextEditorCrdt を公開する独立したプラグインのスクリプト(richtexteditor/plugins/crdt-engine.min.js、縮小後約 88 KB)として提供されます。yjscollab プラグインの collab.attach メソッドはこのグローバル変数を調べ、存在する場合は、アウェアネスのオーバーレイ、プレゼンスのパネル、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

エンジンが担うもの

  • Y.XmlFragment と DOM の双方向の同期。ブロック要素、末端の要素、インラインのマークは、すべて往復で変換されます。ツリーの途中での childList の変更(リスト、表)では、文書全体を大まかに再描画するのではなく、該当するサブツリーだけを的確に再構築します。
  • 選択範囲の保持。リモートの変更による再描画の前にパスベースで選択範囲を記録し、再描画の後に復元します。共同編集者が別の場所で入力しても、ユーザーのキャレットはその場にとどまります。
  • エコーの防止。ローカルの編集には LOCAL_ORIGIN のタグが付き、リモートのハンドラーはそれを無視します。リモートの描画のたびに MutationObserver.takeRecords() を同期的に取り出すので、オブザーバーの非同期のコールバックが、ローカルの編集の送出を Y.Doc に戻してループさせることはありません。
  • カーソルのプレゼンス。選択範囲の位置は Y.RelativePosition のバイト列(同時の挿入/削除があっても変わらない安定した識別子)としてエンコードされ、アウェアネスのチャネルで公開されます。相手側は受信時にそれをデコードし、リモートのカーソルを自分のローカルの DOM に投影します。
  • 作成者ごとの元に戻す操作。trackedOrigins: { LOCAL_ORIGIN } を指定した Y.UndoManager により、Ctrl-Z で元に戻るのはローカルのユーザーの編集だけで、共同編集者の編集が元に戻ることはありません。

ホストが担うもの

  • ツールバー、メニュー、ダイアログ、貼り付けの処理。
  • Yjs プロバイダーへの接続と認証(エンジンはネットワークと通信しません)。
  • アウェアネスのペイロードの構成(ユーザーの識別情報、色、名前)。
  • カーソルのオーバーレイの描画。エンジンは位置のデータを返し、ホストが描画します。
  • 競合時の UX(ホバー時に共同編集者の名前を表示する、その選択範囲を強調表示するなど)。

組み込みのコード

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>

アタッチされたモードを確認する

本番環境の組み込みでは、アタッチした後にモードを確認してください。getStatus().yjsBridge は、Y.Doc がエンジンに同梱されているものとは別の Yjs インスタンスから作られており、プラグインがその間で更新を中継している場合に true になります(これを有効にするには attach に Y を渡します)。requireCrdt: true を指定すると、エンジンを開始できない場合でもプレゼンスとレビューの記録は使えるままになりますが、文書の編集が黙って従来のスナップショットモードに切り替わることはありません。集中的な監視や、目に見える回復のメッセージには onCollabStatus を使ってください。

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 }}
        />
    );
}

textSync: true からの移行

一気に切り替えるか、並行して展開するかによって、2 つの方法があります。

方法 A — 新規に作成する(推奨)

別のキーで新しい Y.Doc を作成します(たとえば、既存の文書 ID に "-crdt" を付け加えます)。ある文書を初めてアタッチするときに、エンジンの移行ヘルパーを実行し、保存済みの HTML から新しい文書にデータを入れます。

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",
});

方法 B — 並行して切り替える

機能フラグによる展開の間、両方のモードを並行して実行します。重要:ある文書の共同編集者は全員、同じモードを使わなければなりません。同じ Y.Doc で従来のスナップショットモードと CRDT モードを混在させると、状態に不整合が生じます。フラグはテナント単位で設定してください。

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

非推奨化のスケジュール(暫定)

時期内容
2026 年第 2 四半期textSync: "crdt" をオプトインのフラグ付きで提供。textSync: true は既定のまま(破壊的変更なし)。
2026 年第 4 四半期textSync: "crdt" が既定になる。textSync: true は従来の後方互換用の shim によって引き続き動作。
2027 年第 2 四半期textSync: true は実行時に非推奨の警告を出す。
2027 年第 4 四半期textSync: true を削除。顧客は "crdt" を使う必要がある。

技術的な詳細

ソースの構成、テストのコーパス、コントリビューター向けのドキュメントは、内部の RichTextEditorCloud/CrdtEngine/ ワークスペースにエンジンと一緒に置かれています。公開 API は window.RichTextEditorCrdt.* としてエクスポートされています。

  • attachCollab(opts) — ディスパッチャー。textSync のモードによって振り分けます
  • attachCrdtBinding(opts) — バインディングを直接使う入り口(ディスパッチャーを経由しません)
  • attachAwareness(opts) — カーソルのプレゼンスの接続
  • attachUndoManager(opts) — 作成者ごとの Y.UndoManager のラッパー
  • htmlToYFragment(opts) — HTML からノード単位の Y への一括移行ヘルパー
  • LOCAL_ORIGIN — エコー防止のため、ローカルで発生した Yjs のトランザクションにタグを付けるシンボル