iOS/macOS向け小説エディタ「with」を開発しています。
with は、基本的には「ひとりの作家」が、
- Mac
- iPhone
- iPad
を行き来しながら執筆するためのエディタです。
この「ひとりで複数端末を使う」という前提は、一般的な共同編集システムとはかなり性質が違います。
今回は、そのために設計した、
単一ユーザー・多端末編集文書群の先祖参照同期モデル
について書きます。
最初は iCloud 同期だけで十分だと思っていた
with は当初、
- iCloud Documents
- Handoff
によってプロジェクト共有を行っていました。
しかし、しばらく使っていると問題が見えてきました。
問題1: 「どちらが新しいのか」がわからない
たとえば:
- Macで執筆
- iPhoneで続きを書く
- Macをスリープから復帰
すると、
- Mac側
- iPhone側
- iCloud側
のどれが「正しい最新版」なのかわからなくなることがあります。
ファイルの更新日時は信用できません。
なぜなら:
- 自動保存
- iCloud同期遅延
- 端末時計
- オフライン編集
が絡むからです。
問題2: 「上書きしていいか」がわからない
もっと重要なのはこっちでした。
たとえば:
text Mac: A -> B iPhone: A -> C
このとき、
- BでiCloudを上書きしてよいのか
- Cを取り込んでよいのか
がわからない。
普通のクラウド同期は、ここをかなり雑に扱います。
しかし、小説原稿でそれをやると事故ります。
Gitをそのまま使うのも違う
最初に思いつくのは Git 的なモデルです。
しかし、小説家に:
- branch
- merge commit
- rebase
- cherry-pick
を要求するのは違う。
欲しいのは:
「安心して取り込んでいいですよ」
という判定でした。
単一ユーザー同期は、共同編集とは違う
ここで重要なのは:
with の同期は「共同編集」ではない
ということです。
Google Docs 的な:
- 同時編集
- リアルタイム競合解決
ではありません。
with の世界では:
- 著者は1人
- authoritative source も1人
です。
競合相手は:
- 他人
ではなく、
過去の自分
別端末の自分
オフライン中の自分
です。
つまり必要なのは:
single-user multi-device sync
でした。
最終的に辿り着いたモデル
with の同期モデルは、かなり単純です。
local → remote
これは「同期」ではなく、
自動バックアップ
として扱います。
条件:
text local.lastKnownRemoteContentHash == remote.contentHash
つまり:
「自分が最後に見たiCloud版から、iCloud側は変わっていない」
なら、自動保存してよい。
これは destructive ではないので、自動化できます。
remote → local は別
逆方向は危険です。
なぜなら:
手元の作業状態を書き換える
からです。
なので、ここだけは必ずユーザー確認を挟きます。
「安心して受け入れ可能」を判定したい
しかし:
毎回 diff を見せる
のもつらい。
本当に欲しいのは:
「これはあなたの現在原稿の続きです」
という判定でした。
そこで ancestor chain を導入した
remote 側は:
text contentHash revisionChain[]
を持ちます。
たとえば:
text A -> B -> C -> D
なら:
text contentHash = D revisionChain = [ C, B, A ]
を持つ。
そして:
text local.contentHash ∈ remote.revisionChain
なら:
remote は local を祖先に持つ後続版
と判定できます。
つまり:
「現在の原稿は、iCloud版の履歴に含まれています。
安心して受け入れられます」
と言える。
これ、Gitに似ているけどGitではない
内部的にはかなり Git 的です。
- content hash
- ancestor
- merge
- branch detection
をやっています。
しかし違うのは:
履歴グラフをユーザーに見せない
ことです。
with の主役は:
「現在の原稿」
です。
Git のように:
immutable commit graph
が主役ではない。
なぜこれで成立するのか
これは小説原稿だからです。
つまり:
- 軽い
- テキスト中心
- AST化できる
- paragraph/hash比較が安い
そのため:
- paragraph hash
- heading hash
- file hash
- revision chain
を持ってもコストが小さい。
ASTベース差分
with の原稿は:
- ファイル
- 見出し
- 段落
単位でIDとハッシュを持っています。
そのため:
text same id + same hash -> unchanged same id + different hash -> modified new id -> inserted missing id -> deleted
という比較ができます。
競合も:
「第3章のこの段落だけ」
まで局所化できます。
編集者との協業は「PR」にする
さらに面白いのはここです。
with は将来的に編集者との協業も考えています。
しかし、それも:
リアルタイム共同編集
にはしません。
編集者は:
原稿を書き換える人
ではなく、
変更提案を送る人
です。
つまり:
- author mainline
- editor proposal
の構造になります。
これはかなり GitHub PR に近い。
まとめ
今回設計したのは:
単一ユーザー・多端末向け祖先参照同期モデル
でした。
ポイントは:
- local → remote は自動バックアップ
- remote → local は手動確認
- ancestor chain で「安心して取り込める」を判定
- merge はAST単位
- でもGitは露出しない
ことです。
小説エディタの同期は、
- Dropbox 的でも
- Git 的でも
- Google Docs 的でも
少し違う。
その中間にある設計になったと思っています。