定義
Notionのようにブロックで構成された文書エディタを作るとき、文書データの主は自分のアプリが握りつつ、重いリッチテキスト編集エンジンは「今カーソルが置かれた1ブロックだけに」一時的に付けたり外したりする構造を使いたいときのアーキテクチャである。文書全体を丸ごと1つのエディタに任せないことが要点である。
Focused guest editor(訳注: このような標準用語はない、本稿でつけた仮称である)は、文書の意味、すなわちブロックツリー(ブロックの階層構造)・パッチ(変更履歴)・権限をhost(文書データの実際の主 = 自分のアプリコード)が所有し、リッチテキストエンジンは現在フォーカスされたテキストブロックだけに一時的にマウントされるguestとして置く方式である。guestはinject(hostがそのブロックの現在値を入れてくれる)/commit(編集した内容をhostに返す)/keymap(キーボードショートカットのマッピング)委譲のような狭いチャネルでのみhostと会話する。文書全体を1つのエディタ文書(doc)として置く一般的なモデルとは正反対である。
なぜ必要か
文書全体を単一のリッチテキスト文書として置くと、ブロックツリー・変更履歴・権限・リアルタイム購読がすべてエディタのprojection(エディタが内部的に作る文書表現)に縛られ従属する。するとブロックごとに異なる権限、ブロック外のウィジェット、複数ブロックにまたがるジェスチャを付けにくい。hostがsource of truth(真実の源 = このデータの唯一の基準)で、guestはフォーカスlease(一時的に借りる編集権限)だけを持てば、インライン編集の使用感はそのままに、文書の意味を自分のドメインモデルに残せる。
動作原理
- Hostがブロック配列とパッチapplyの関門を持つ。非フォーカスブロックは静的レンダー(または読み取り専用)である。
- ユーザーがテキストブロックをフォーカスすると、guestがそのブロックだけにマウントされ、hostが現在の
contentをinjectする。 - タイピング・マークはguestセッション内で処理される。構造演算(タイプ変更、分割、移動)はhostコールバックに委譲する。
- フォーカス喪失・退去時にguestがcommitで内容をhostに返し、アンマウントする。
- split/merge直後の遅いblur commitが新しい状態を上書きしないよう、構造演算前にcommit-before-delegateとlease retireが必要である。
| 項目 | 説明 | 注意点 |
|---|---|---|
| Host SoT | ツリー・パッチ・権限・autosave | guest内部doc ≠ 文書全体 |
| Focus lease | フォーカスブロックだけにguest | アンマウント時にcommit必須 |
| 通信チャネル | inject / commit / keymap委譲 | チャネル外の突然変異禁止 |
| Stale commit | 遅いblurが以後のパッチを上書き | retire·commit-before-delegate |
| キールーティング3層 | テキスト=guest、構造=host、無フォーカス=selection | モードを混ぜるとキー衝突 |
実務での適用
クロスブロックのテキスト移動は、guestが1ブロックしか所有しないためguestネイティブのDnDでは解けない。コンテナでHTML5 DnDを聴取し、hostパッチで反映する。フォーカスストアをReactの外に置くならuseSyncExternalStoreで購読し、スナップショット同一性のルールを守る。
トレードオフ
- ブロック数に関係なくguestインスタンスが1つ(または少数)なのでコストが予測可能である。代わりにフォーカス切り替えごとにmount/commitコストとrace処理が生じる。
- host/guestの境界を守るとテスト・権限が明確になるが、「文書全体が1つのエディタ」APIに慣れたライブラリ機能をそのまま使いにくい。
使ってはいけない場合
- 文書が短い単一フィールド(コメント1行)のとき — guest/host分離が過剰である。
- 協調編集CRDTが文書全体を1つの共有型として扱うモデルがすでにSoTのとき — 境界を無理に分けると同期コストだけが増える。
よくある間違い
- 非フォーカスブロックにもguestをマウントして「便利さ」を取り、性能・フォーカスraceを起こす。
- blur commitと構造パッチの順序を保証せずstale overwriteを起こす。
- テキストキーと構造キーを1つのkeymapに混ぜ、Mod-Dのようなショートカットがモードごとに違って壊れる。
- クロスブロック移動をguest DnDで実装しようとして失敗する。
関連概念
- two-stack-inverse-undo — host文書の履歴とguestセッションの履歴を層に分ける。
- usesyncexternalstore-snapshot-identity — フォーカス/レイアウトストアを外部状態に置くときのスナップショット同一性。
- html5-drag-and-drop — guest境界を越えるテキスト移動のチャネル。