Baku Hashimoto

データアーキテクチャ

garden.ooo のデータは3つの層を持っている。ローカルのテキストファイルを SSOT(Single Source of Truth)として、ブラウザ内の DB、リモートの DB へと段階的にミラーしていく構造。

Local File (.md)  ←→  Local DB (RxDB/IndexedDB)  ←→  Remote DB (RxDB/MongoDB)
     SSOT                   ブラウザ内                    サーバー

各層の役割

Local File(SSOT)

ローカルディスク上のテキストファイル群。現時点では Obsidian 互換の Markdown + YAML frontmatter を採用しているけれど、将来的には AST にパース可能な任意の軽量マークアップ言語を扱えるようにする想定(→ information-architecture.md のコンテンツパイプライン)。
File System Access API を介してブラウザからアクセスする。これがオリジナルで、クラウドはそのミラー。

Local DB

ブラウザ内の RxDB インスタンス(バックエンドは Dexie/IndexedDB)。
ページの検索、バックリンクの解決、リアクティブなクエリといった CMS としての機能を担っている。
Local File から読み込んだ内容をパースして、構造化されたドキュメントとして保持する。

Remote DB

サーバー上の RxDB インスタンス(バックエンドは MongoDB)。
Web 公開、モバイル端末との同期、複数人での共同編集を可能にする。
Local DB と HTTP pull/push で同期する。レプリケーション戦略の詳細は replication.md を参照。

content_id によるリネーム追跡

ファイル名(path)はいつでもリネームされうる。「編集とリネームを同時に行った」場合でも追跡できるように、各ページは content_id という不変の識別子を持っている。

  • ULID で生成される(26文字の英数字)
  • frontmatter に埋め込まれ、ファイルと一緒に移動する
  • DB 上では content.content_id から導かれる id が primary key として使われる

Local File ↔ Local DB の同期

Local DB ↔ Remote DB と同じく replicateRxCollection を使った
replication
として実装している(utils/sync/replicateLocalFiles.ts)。
つまり db.pages コレクションには 2 本の replication が同時に走る:

Local File   ⇆   Local DB   ⇆   Remote DB
       replicateRxCollection × 2

replicationIdentifier でそれぞれ flag されるので、FS から pull した
doc は Remote へ push されるが FS には押し戻されない、逆も同様。
ping-pong は RxDB 側で標準的に防がれる。

Pull (FS → DB)

fs.list() でディレクトリを走査し、各ファイルを localPageMeta.lastSyncedHash
と突き合わせる。ハッシュが違うものだけを doc として emit する。

  • 初回起動時は全ファイルが pendingPaths キューに積まれ、batchSize ごとに
    pull が呼ばれる
  • それ以降は FileSystemObserver のイベントを pull.stream$ に流して
    RxDB を起こす
  • 消えたファイル: localPageMeta.lastSeenPath で逆引きして softDeleted: true を emit
  • orphan (content_id が frontmatter に無いファイル): pull 時に新しい id を
    生成し、その場で FS に書き戻して から doc を emit する。echo-prevention
    のために push handler が押し戻すことができないため

判定基準は frontmatter の updated でも mtime でもなく コンテンツの
ハッシュ
source_hash 相当)。mtime はクロスプラットフォームで信頼できない
(Dropbox/iCloud による書き換え、エディタによる保持、NTP 補正での巻き戻り等)し、
updated はユーザが任意でスタンプする訪問者向けの値で同期判定には使えない。

Push (DB → FS)

DB の doc が変わると push handler が呼ばれる。assumedMasterState (RxDB が
記憶している前回の master 状態) の hash と現在の FS ファイルの hash を比較:

  • 一致 → 書き込み or 削除を実行、lastSyncedHash を更新
  • 不一致 → FS の現状を conflict として返却。conflictHandler
    realMasterState (= FS) を採用 → ローカル doc が FS の内容で上書きされる

assumedMasterStateundefined(fresh replication 起動時など)の場合は
localPageMeta.lastSyncedHash にフォールバックする。これがないと pull した
ばかりの同一内容ファイルが全部 conflict 扱いになる。

Echo 防止

「DB→FS 書き込みが FileSystemObserver から Local→DB として検知される」
ループは、localPageMeta.lastSyncedHash が唯一の echo フィルタとして
畳まれている:

発火源hash vs lastSyncedHash結果
自分の push 直後一致(直前に更新済み)pull 側で skip
Dropbox の同内容書き戻し一致skip
Dropbox 経由の新内容不一致真の変更として処理
ユーザの編集不一致真の変更として処理
FS Observer のバースト発火一致(2回目以降)自動的に冪等

旧実装にあった「抑制セット」「保存前の content 比較」は不要になった
(ハッシュ比較に統合された)。

マルチタブ協調

waitForLeadership: true (デフォルト) と RxDBLeaderElectionPlugin により、
リーダータブ 1 つだけが FS replication を回す。他のタブは Local DB の
変更を BroadcastChannel 経由で受け取るだけ。複数タブで同時に FS ハンドルを
触り合うレースが構造的に消える。

Some Rights Reserved. (cc) 2026 Baku Hashimoto
This site is generated by Garden.ooo