目的
ユーザー操作によりクライアント(FUSOU-APP)から Fleet スナップショットを保存し、R2 にペイロードを置き、Supabase にメタを残す。R2 の重複アップロードを避けつつ、短縮 token による閲覧を提供する。
関係コンポーネント(簡潔)
- クライアント(FUSOU-APP): スナップショットの収集・送信を行う。
performSnapshotSyncを使用。 - Cloudflare Pages / Functions:
POST /api/fleet/snapshot(アップロード受け口)。 - Cloudflare R2: 圧縮ペイロードの格納先。
- Supabase:
fleetsメタテーブルに token / r2_key / メタ情報を保存。 - Worker (
GET /s/:token): token を使ってスナップショットを取得・公開(ETag/304 対応)。
1) 手動スナップショット同期(Client → Pages API → R2 / Supabase)
-
スナップショット取得(クライアント側)
-
実装場所とエクスポート:
- 現状の実装は
packages/FUSOU-APP/src/utility/snapshot.tsにあり、モジュール内レジストリとして動作します。 - エクスポートされている主な関数は
registerSnapshotCollector(fn),collectSnapshot(timeoutMs)とhasCollectors()です。
- 現状の実装は
-
動作の要点(現状):
registerSnapshotCollector(fn)はコンポーネントのマウント時に呼ばれ、アンマウント時に解除される想定です(各コンポーネントが自身のコレクタを登録するパターン)。collectSnapshot(timeoutMs)は登録済みのコレクタ関数を登録順に呼び出します。各コレクタは同期でも非同期でもかまいません。実装は各コレクタの Promise を待ち、最初に「有効」と判断される非 null のスナップショットを返します。- タイムアウト: コレクタ応答待ちに上限を設けています(
main.tsxのトレイ同期ハンドラでは 5000ms を使用しています)。タイムアウト内に有効な結果が得られなければnullが返り、呼び出し元は同期を中止するべきです。
-
現在登録しているコンポーネント(例、現状の正直な状況):
- 現在、主要な登録実装を追加したのは
SnapshotViewerとFleetInspectorの2箇所です。つまり、アプリ内のすべての画面や UI 状態が自動的にスナップショット提供者として登録されているわけではありません。 - 他の画面やモジュールがスナップショットを提供していない場合、
collectSnapshotはこれら登録済みコレクタに頼るため、取得できる情報は登録済みコンポーネントが提供する内容に依存します。
- 現在、主要な登録実装を追加したのは
-
スナップショットの内容と一貫性(現状の課題):
- スナップショットは「JSON シリアライズ可能なオブジェクト」であり、フリートの状態(艦隊、選択セル、表示中の詳細など)を含む想定ですが、プロジェクト内で厳密なスキーマが中央管理されているわけではありません。
- 現状では “正規化(deterministic canonicalization)” や不要な一時フィールドの除去が自動的に行われていないため、同一の論理的状態でもフィールド順や一時的 UI データの違いで異なるハッシュが生成される可能性があります。R2 の重複排除や content-hash に基づく判定を正しく行うには、アップロード前にクライアント側でソートや不要フィールドの削除などの正規化を行う必要があります(この正規化処理は現状自動的には適用されていません)。
-
部分スナップショットとエラー耐性:
-
「部分スナップショット」とは: コレクタが返すオブジェクトがフリート全体の完全な状態を含まず、選択中のアイテムや表示中のサブセットのみを返すケースを指します。たとえば
SnapshotViewerは現在表示している snapshot データを返す設計であり、必ずしも全てのフリート状態(例: 未表示の艦隊データや細かい UI 状態)を含むとは限りません。 -
現状の扱い(率直):
collectSnapshotは登録コレクタを順に呼び出し、各コレクタが返す最初の「非 null / 非 undefined」の戻り値を有効なスナップショットとして採用します。つまり「部分的な」オブジェクトでも非 null であればそれが即座に採用されます。- 採用後の内容に対する構造チェック(必須フィールドの存在確認や一貫性検証)は現在の実装に組み込まれていません。呼び出し元(例:
performSnapshotSync)が独自に検証している箇所は限定的です。 - コレクタ内で例外が発生した場合、レジストリは例外を捕捉してログ化し、次のコレクタへ進む処理になっています(単一コレクタの失敗で全体が停止しないようにする処理)。ただし、複数コレクタが存在しても結果をマージする仕組みはなく、最初に返ってきたものを採用する単純な戦略です。
-
現状の欠点(実務で問題になり得る点):
- 部分スナップショットをそのままアップロードすると、期待するフィールドが欠けた状態で R2/Supabase に保存されてしまう可能性があります。
- 同一の論理状態を複数のコレクタが部分的に提供している場合でも、現在はマージを行わないため情報が欠落するリスクがあります。
- スキーマ検証や必須フィールドチェックが無いため、サーバ側の重複判定(content-hash)前の正規化が不十分だと無駄な重複や誤検出が発生します。
-
改善案(推奨):
- クライアント側でスナップショットの最小必須フィールドを検査するバリデータを追加する(例:
fleets配列が存在するか、少なくとも 1 つの艦隊が含まれるか等)。 - 複数コレクタの出力をマージする仕組みを導入する(キー毎に優先度をつける、または部分的な結果を統合して完全性を高める)。
- 正規化ルール(フィールドソート、不要な一時フィールド除去)をコレクタ呼び出し後に一元的に実行してから圧縮・ハッシュを行う。
- 既存の最後の成功スナップショットをキャッシュしておき、今回取得したスナップショットが不完全である場合はキャッシュを使って補完またはアップロードを中止するロジックを検討する。
- クライアント側でスナップショットの最小必須フィールドを検査するバリデータを追加する(例:
-
まとめ(現状): 現在の実装は「最初に有効な戻り値を採用する」シンプルな戦略で稼働しており、部分スナップショットや構造不備に対する防御は限定的です。運用で安定させるには上記のような検証・マージ・正規化の導入が必要です。
-
-
設計ポリシー(重要):
- グローバル変数やグローバル関数を使わず、モジュールスコープのレジストリでコンポーネントから snapshot を集める方針が採られています。これはテスト容易性と型安全性の向上を目的とした意図的な設計です。
-
実運用での注意点(推奨):
- スナップショットを R2 の content-hash ベースで重複排除したい場合、クライアント側で圧縮後に SHA-256 を計算する前に一貫した正規化を必ず行ってください。
- 全コンポーネントを網羅的に登録するには、各主要な画面・状態プロバイダに
registerSnapshotCollectorを追加する作業が必要です。
(注)上の説明は現在のコードベースの実装状態を率直に記したものです。フローの強化(自動正規化、完全性検査、より多くのコンポーネント登録)は次の改善タスクとして推奨されます。
-
-
事前処理(クライアント)
- JSON シリアライズ後に gzip 等で圧縮し、圧縮後のバイトサイズを計測する。
- クライアント側で SHA-256(content-hash)を計算し、
X-Content-Hashヘッダに付与する。 Idempotency-Key(UUID 等)を生成してヘッダに付与する。- 必要に応じて
Authorization: Bearer <access_token>を付与する(Supabase 認証など)。
-
アップロード(Pages API へ POST)
- POST
/api/fleet/snapshotに圧縮ペイロードを送る。推奨ヘッダ:Idempotency-Key: <uuid>X-Content-Hash: sha256:<hex>Content-Encoding: gzipAuthorization: Bearer <token>(必要時)
- ボディは multipart/form-data または application/octet-stream(実装に合わせる)。
- POST
-
サーバ側処理(Pages API)
- JWT 検証(必要時): Bearer トークンを検証して操作権限を確認する。
- 重複判定(サーバ設計のいずれか):
- 推奨: R2 のオブジェクトキーに
content-hashを用いる(例:snapshots/<user>/<sha256>.json.gz)。PUT 前に存在確認を行い、存在する場合は PUT をスキップして既存扱いとする。 - 代替: Supabase の
fleetsメタテーブルで同一 hash のレコードを検索してから R2 PUT を行う。
- 推奨: R2 のオブジェクトキーに
- R2 へ格納(必要であれば):
ASSET_PAYLOAD_BUCKET.put(key, body)。 - Supabase に metadata を upsert(token, r2_key, size, sha256, created_at, public_flag など)。
-
サーバ応答
- 成功: 201/200 と JSON(例:
{ token, r2_key, etag })。 - 重複: 200 または 409(実装による)。
- エラー: 4xx/5xx を返す。
- 成功: 201/200 と JSON(例:
設計上の注意点(短記)
- R2 key に content-hash を利用すると重複の排除が単純になる。
- サーバ側で圧縮後サイズ上限(制限)を設け、一定以上は拒否する(413)。
Idempotency-Keyはクライアント側で生成し、同一キーの再送はサーバで安全に扱う。
2) スナップショット閲覧フロー(Token を使った GET /s/:token)
-
クライアント(閲覧者)が
GET /s/:tokenを要求する。 -
Worker の処理(Edge)
- Supabase の metadata を token で参照し、
r2_key,publicフラグ等を取得する。 - アクセス制御:
publicが true → そのまま配信。publicが false → JWT を検証し、閲覧権を確認する(JWKS をキャッシュして RS256 検証)。
- ETag/Cache 判定:
- metadata または R2 の ETag(content-hash)を参照し、
If-None-Matchと一致する場合は 304 を返す。
- metadata または R2 の ETag(content-hash)を参照し、
- R2 からペイロードを取得してレスポンスを返す(必要に応じて Cache API を使いエッジでキャッシュ)。
- Supabase の metadata を token で参照し、
-
レスポンス
- 成功: 200 +
Content-Type: application/json+ETag: <etag>。ボディに snapshot JSON。 - 304: If-None-Match が一致する場合(ボディ無し)。
- 401/403/404: アクセス不可または token 不存在。
- 成功: 200 +
閲覧用 Token の性質
- token は推測困難なランダム文字列にする(短すぎない)。
- token の公開/非公開設定を Supabase 側で管理し、失効や削除が可能な仕組みにする。
使い方(ユーザー向け、簡潔)
-
スナップショットを同期するには:
- FUSOU-APP 内で
Syncボタンを押す、またはトレイメニューのSync snapshotを選択します。 - アプリが現在のスナップショットを収集してサーバへ送信します。処理が成功するとアプリ側に「アップロード成功」のメッセージが表示されます。
- FUSOU-APP 内で
-
スナップショットを閲覧するには:
- 管理画面や API 応答で得た
tokenを使って、ブラウザでhttps://<host>/s/<token>を開いてください(公開設定であれば誰でも閲覧可能、非公開ならログインが必要)。 - ページは ETag を利用して効率的にデータ更新を確認します。
- 管理画面や API 応答で得た
このドキュメントはフローの詳細と基本的な使い方に絞って記載しています。運用手順・テスト手順・実行例は別ドキュメントとして用意することを推奨します。