---
title: データアーキテクチャ
date: 2026-02-24T23:17:56.360Z
updated: 2026-05-06T05:53:18.000Z
tags:
  - garden
url: https://baku89.com/ja/garden/data-architecture
---

# データアーキテクチャ

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](./information-architecture) のコンテンツパイプライン）。\
File System Access API を介してブラウザからアクセスする。これがオリジナルで、クラウドはそのミラー。

### Local DB

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

### Remote DB

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

## 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 の内容で上書きされる

`assumedMasterState` が `undefined`（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 ハンドルを\
触り合うレースが構造的に消える。
