2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

小説エディタ「with」のために、単一ユーザー・多端末同期モデルを設計した話

2
Posted at

iOS/macOS向け小説エディタ「with」を開発しています。

with は、基本的には「ひとりの作家」が、

  • Mac
  • iPhone
  • iPad

を行き来しながら執筆するためのエディタです。

この「ひとりで複数端末を使う」という前提は、一般的な共同編集システムとはかなり性質が違います。

今回は、そのために設計した、

単一ユーザー・多端末編集文書群の先祖参照同期モデル

について書きます。


最初は iCloud 同期だけで十分だと思っていた

with は当初、

  • iCloud Documents
  • Handoff

によってプロジェクト共有を行っていました。

しかし、しばらく使っていると問題が見えてきました。

問題1: 「どちらが新しいのか」がわからない

たとえば:

  1. Macで執筆
  2. iPhoneで続きを書く
  3. 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 的でも

少し違う。

その中間にある設計になったと思っています。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?