---
title: レプリケーションのマージ戦略
date: 2026-02-24T23:35:53.621Z
updated: 2026-05-06T05:53:18.000Z
tags:
  - garden
url: https://www.baku89.com/ja/garden/replication
---

# レプリケーションのマージ戦略

> このドキュメントは [data-architecture.md](./data-architecture) の「Local DB ↔ Remote DB」同期の詳細。

## 概要

各ブラウザはローカルの RxDB (IndexedDB) を持ち、HTTP pull/push を介して\
中央の RxDB (MongoDB) サーバと同期する。\
さしあたり共同同時編集は想定せず、単一ユーザが一度に一つのデバイスで操作する前提。

## 戦略: Server Wins

push 時に conflict が検出された場合、サーバの状態を正とする。\
クライアントはサーバ状態を受け入れ、ローカルの差分を破棄する。\
RxDB の `conflictHandler.resolve` も常に `realMasterState` を返す。

## Pull: seq による単調カーソル

サーバが accept した書き込みごとに、サーバ側の `seq` ローカルドキュメント\
(`db.pages.local('seq')`) を `incrementalModify` で原子的に +1 し、\
新しい seq を doc にスタンプする。

- **Pull cursor**: `{seq: number}` の単一値
- **Pull selector**: `{seq: {$gt: checkpoint.seq}}`、`sort: [{seq: 'asc'}]`
- **wire 形**: クライアント schema には `seq` が無いので、サーバはレスポンス組み立て時に\
  各 doc から `seq` を destructure で除去してから返す

`seq` は **サーバ側のスキーマだけ** に存在する。`pageSchema.ts` で\
`import.meta.server` 分岐により schema を fork し、サーバ schema は\
`seq` を required + indexed、クライアント schema は `seq` を持たない\
(`additionalProperties: false` で混入を弾く)。

## Conflict 検出 (Push)

各 push 行には `assumedMasterState`（クライアントが想定するサーバの現在状態）が\
付与される。サーバはこれを実際の状態と比較する:

- ID 一致 + `isPageIdentical(assumed, current)` → 書き込みを受理 → 新しい seq を\
  発番してスタンプ → upsert
- いずれかが不一致 → 書き込みを拒否し、サーバの実状態を conflict として返却

`isPageIdentical` は `softDeleted` + `content.path` + `content.source_hash` の\
3 フィールド比較のみ。`source_hash`（markdown 全文の `ohash` フィンガープリント）が\
content の正規 identity なので、これだけで content equality が決まる。

クライアントの conflict handler はサーバ状態を採用して resolve する。

なお `conflictHandler` は collection 単位で 1 つしか持てない仕様だが、\
同じハンドラを **Remote DB / FS の両 replication で共用できる**。各 replication\
の視点で master = upstream が勝つ、という「master wins」だけ書いておけば、\
Remote replication からは Server Wins、FS replication からは FS Wins として\
自然に意味が通るため。詳しくは [data-architecture.md](./data-architecture) の\
「Local File ↔ Local DB」セクションを参照。

## 削除

ページの soft delete は `PageDoc.softDeleted: true` を立てて upsert するだけ。\
RxDB native の `_deleted` ではなく通常フィールドなので、replication は\
他のフィールド変更と同じ経路で削除フラグを伝播する。tombstone 専用の\
コレクションは持たない。

クライアント側のクエリは `selector: {softDeleted: false}` で生存ページだけを\
表示する。

## なぜ単純 seq か

CouchDB スタイルの rev hash や HLC は採らない。理由:

- **単一ユーザ + Server Wins** — 因果的順序や rev tree のリッチさが要らない
- **オフライン書き込みの正しさ** — クライアントが offline 中に作成した doc が\
  オンライン復帰時に push されたとき、サーバが新しい seq をスタンプするので、\
  他デバイスの pull 検査で必ず拾える。クライアント時計ベースの cursor だと、\
  古い時計値で書かれた doc を他デバイスの checkpoint が追い越して取りこぼす\
  可能性があった（旧 `indexedAt` 方式の問題）
- **構造的単純さ** — 単一の整数 cursor で済む。複合 keyset\
  (`{id, indexedAt}`) より扱いやすい
