会議設計サービスに Lexical を利用して同時編集機能を導入する


こんにちは [UNIBA](https://uniba.jp/ "UNIBA inc.") 株式会社 の [Z1チーム](https://z-1.works/ "Z1 Web Site") です。
この記事では [Lexical](https://lexical.dev/ "Lexical.dev") を用いた同時編集機能を実現し、プロダクトに組み込んだという話をします。

[Lexical](https://lexical.dev/ "Lexical.dev") を使うと比較的簡単に 同時編集機能ができるかと思います。
プロダクトの一部に同時編集機能を導入したい・・・と思っている方とかの参考になればと思います。

Lexical の使い心地や実装のポイントやハマりどころなどの技術詳細に対しては、別途記事を書きたいと思っています。
今回はそこまで踏み込んで書いていません。あしからず。

## プロダクト紹介と宣伝

Z1 チームでは [SuperGoodMeetings](https://supergoodmeetings.com/) (以下 SGMs)という、会議を中心にプロジェクトを推進していくサービスを開発しています。

コンサルティングを専門とする [Copilot](https://copilot.jp/) の皆さまと一緒にファシリテーション・会議設計に対して正面から向き合っています。

([Copilot](https://copilot.jp/) の皆さまは [fukabori.fm](http://fukabori.fm) でも 会議に対してめちゃくちゃ良い話をしているので、ぜひお聞きください [https://fukabori.fm/episode/77](https://fukabori.fm/episode/77))

もし `会議が進行がグダグダしている` とか `この会議やって意味あるの？` とか思う方は、 是非 SGMs を使ってみてください。

# 導入の経緯

会議の進行で重要なのが **リアルタイムな情報更新** です。

会議では限られた時間の中で、

- 議事録を書いたり
- アジェンダの更新を行ったり
- 時間調整をしたり

結構することがいっぱいあります。
これらの情報を同期するためにページを再読み込みなどをしているとそれだけで会議のクオリティが下がってしまいます。リアルタイムな情報更新は会議には欠かせません。

SGMs では、 Rails の [ActionCable](https://railsguides.jp/action_cable_overview.html) を利用して、データの同期を行ってきました。

しかし、議事録のような頻繁に情報を更新したり、複数人で書く必要がある項目に対しては、既存の同期の方法では不十分であることがわかりました。

そのため課題解決のために [Lexical](https://lexical.dev/ "Lexical.dev") を用いた同時編集機能の実装を行いました。

# 課題詳細

同期の問題は以下のようなケースで起こりました。

![ざっくりデータの同期パターン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68729/b3877969-1e3e-dfd1-67b8-51d8905b27c6.png)

SGMs では更新された文章を丸ごと保存して同期する設計になっていました。
誰か1人が書いている文章が手元にやってくるパターンには正しく動作するのですが、複数人が文章を変更しようとすると、互いの編集結果で上書きが起こり、誰かの編集したデータがロストしてしまう現象が容易に起こります。
これでは議事録がうまく書けません。

目指すは `Google Document` とか `Dropbox Paper` とかのような、複数人で書ける領域を設けることです。

# CRDT

これを解決するには CRDT と呼ばれる技術を使うことにしました。

詳しくは [https://qiita.com/ryan5500/items/a1debe79816ee15adf9b](https://qiita.com/ryan5500/items/a1debe79816ee15adf9b) などを参考にしてください。

CRDT ではコマンド(編集履歴)を保存しながら、データを保つというアプローチを行なっています。

CRDT の技術の詳細については割愛しますが、この機能を実装しているのが [yjs](https://github.com/yjs/yjs "yjs") です。

yjs は CRDT を用いた同期の機能を提供してくれます。
ただし yjs は同期の機能のみなので、これに対応したエディタを選定する必要があります。

# 議事録エディタの選定について

SGMs の議事録向けの領域には Markdown 方式で書ける リッチテキストエディタを採用しています。 
具体的には [Slate](https://github.com/ianstormtaylor/slate "slate") (Markdown 対応の Editor のメジャーなライブラリ) をベースに拡張された [Plate](https://plate.udecode.io/ "Plate") を利用していました。

yjs 対応をしている新しい Editor を探しているところ、目を向けたのが　[Lexical](https://lexical.dev/) です。 Lexical は Meta 社が提供している Editor です。

Lexical の大きな魅力としては

- Markdown 対応がされている
    - 後発なので使い心地 (ショートカット / コピペの挙動など) も良い
- Collaboration 機能として、 yjs が利用された実装がされている
- SGMs は React で作られているので、 React との親和性が高い (はず)
    - React も Meta 社が提供しているので都合が良い

があります。

もちろんデメリットとしてはリリースされたばっかり (2022/10/7 時点で version 0.5) なので、挙動や仕様がアップデートされていく中で突然動かなくなるというケースもあります。

テストなどを通して、 SGMs の機能としては十分にワークすると判断して導入しました。

# Lexical と サーバの実装

Lexical を使うと、比較的簡単に同時編集機能を体験することができます。
基本的な機能を体験したい時に準備が必要なのは、

- yjs server の用意

ぐらいです。

エディタ側は [実装のサンプル](https://lexical.dev/docs/collaboration/react) を見ても分かるとおり、 `CollaborationPlugin` を導入するだけで、 同時編集機能対応のエディタが出来上がります。

``` js:Sample 抜粋
<CollaborationPlugin
    id="yjs-plugin"
    providerFactory={(id, yjsDocMap) => {
      const doc = new Y.Doc();
      yjsDocMap.set(id, doc);
    
      const provider = new WebsocketProvider(
        // ここのサーバを準備すれば OK.
        "ws://localhost:1234",
        id,
        doc
      );
    
      return provider;
    }}
    initialEditorState={initialEditorState}
    shouldBootstrap={true}
/>
```

yjs の サーバも比較的簡単に準備できます。

```js:y-websokcet/bin/server.js
const WebSocket = require('ws')
const http = require('http')
const wss = new WebSocket.Server({ noServer: true })
const setupWSConnection = require('./utils.js').setupWSConnection

const host = process.env.HOST || 'localhost'
const port = process.env.PORT || 1234

const server = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/plain' })
  response.end('okay')
})

wss.on('connection', setupWSConnection)

server.on('upgrade', (request, socket, head) => {
  // You may check auth of request here..
  // See https://github.com/websockets/ws#client-authentication
  /**
   * @param {any} ws
   */
  const handleAuth = ws => {
    wss.emit('connection', ws, request)
  }
  wss.handleUpgrade(request, socket, head, handleAuth)
})

server.listen(port, host, () => {
  console.log(`running at '${host}' on port ${port}`)
})
```

y-websocket の提供している実行コードを使えば localhost の環境などで簡単にテストすることは可能です。
また、上のコードをベースに データの永続化 やプロダクトに応じた必要な処理を加えることができます。

SGMs では

- redis にデータを保存し永続化
- 編集履歴や議事録データを取得する口を追加

などしています。ここは別途記事で説明したいと思います。
(また、SGMs 特有の課題として、 既存データの変換など準備しています。)

# 導入結果

細かい課題は幾つかあるのですが、今回は省いて結果のみを伝えると

![Untitled (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68729/fea69128-8c00-5067-62b8-80acea3616e4.png)

右部分でわかると思いますが、他者の書いている状態が同期され、かつ上書き問題も解消されています。
現時点では実用可能なレベルと判断しています。

書いている人が画面に映ることで、誰かが議事録を書いてくれている安心感など、視覚的なフィードバックも向上しました。

気になる方は実際のプロダクトを触っていただけると嬉しいです。

技術選定などで時間はかかってしまいましたが、導入しようと決めてからはおおよそ1ヶ月程度で実装が完了しています。

# まとめ + 興味がある方がいたら

SGMs では誰もが Meeting に参加し、ファシリテーションが可能になる世界を目指しています。

実装した同時編集機能によって、 誰が書いても大丈夫 という安心感を与える一歩になりました。

まだまだ SGMs ではプロジェクト推進のための機能のアップデートを続けています。

同時編集以外にも必要な機能はたくさんあります!
プロダクトに興味を持った方は是非一緒に開発しましょう。ご連絡お待ちしております。
また使い方をもっと詳しく知りたい・会議をうまく進行したいと思う方もぜひご相談ください。