mcpField
C
capacitor-offline-first

capacitor-offline-first

github.com/cap-go開発731.1k 回インストール2 週間前に更新
リポジトリ

capacitor-offline-first について

Capacitor アプリのオフラインファースト設計を支援する開発スキルです。SQLite によるデータ保存、outbox を使った同期、競合処理、プラットフォーム固有の注意点を扱います。

主な機能

  1. 01SQLite によるローカルデータ保存
  2. 02トランザクション対応の同期 outbox
  3. 03競合と削除の方針
  4. 04Capacitor のプラットフォーム別トラブル対応

活用シーン

  1. 01オフライン用レコード保存の設計
  2. 02同期時の書き込み重複の防止
  3. 03iOS でのデータ消失の調査
  4. 04ネイティブプラグインの利用維持

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cap-go/capgo-skills --skill capacitor-offline-first

インストール後、Claude Code で/capacitor-offline-firstを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.json
references/
fast-sql-setup.md
sync-engine.md

詳細

オフラインファーストの Capacitor アプリ

使用する場面

次の場合に使用します:

  • アプリがオフライン中も読み書きでき、後で同期する必要がある
  • ユーザーデータ用のローカルストレージ(Preferences、SQLite、IndexedDB)を選択する
  • 同期中に書き込みが重複、消失、または上書きされる
  • ストレージ不足が発生した後、iOS でキャッシュデータが消える
  • サービスワーカーを追加したところ、ネイティブプラグインが動作しなくなった(Plugin not implemented)

次の場合は使用しないでください:

  • @capacitor-community/sqlite/Ionic セキュアストレージの置き換え:sqlite-to-fast-sql
  • 新しい Web コードをデバイスに配信(OTA):capgo-live-updates
  • 暗号化キーの保存、SSL ピンニング:capacitor-security
  • クエリ速度/ブリッジのオーバーヘッド調整:capacitor-performance

判断事項(コードを書く前に決めてください)

質問デフォルトの回答
UI はどこから読み取りますか?ローカル DB のみです。ネットワークからの書き込みは DB に行い、UI は DB を監視します。
レコードのストレージ@capgo/capacitor-fast-sql 経由の SQLite
小さな設定/トークンのストレージ@capacitor/preferences(シークレットには使用しません。capacitor-security を参照してください)
localStorage/IndexedDB一時データのみ:iOS はストレージに負荷がかかると WebView のストレージを削除することがあります
アプリシェルのオフライン対応すでにオフライン対応です:Capacitor は Web アセットをバイナリにバンドルします(または Capgo のライブアップデートバンドルを使用します)。サービスワーカーは不要です。
ネイティブビルドでのサービスワーカー避けてください。Android の SW は Capacitor プラグインの挿入を妨げます。iOS の SW には WKAppBoundDomains が必要で、これも ios.limitsNavigationsToAppBoundDomains を設定しない限り挿入を妨げます。SW は PWA ビルドでのみ使用してください。
IDクライアントで生成する UUID(crypto.randomUUID())。オフラインでの作成にサーバーとの往復が不要になります
競合ポリシーユーザー/プロダクトオーナーに確認します:レコード単位の最終書き込み優先、フィールド単位のマージ、またはユーザーによる確認を伴うサーバー優先
削除サーバーが確認するまでソフト削除(トゥームストーン)を使用します

ワークフロー

  1. 調査:現在のストレージ(localStorage、@capacitor/preferences、その他の SQLite プラグイン)、API の仕様(updated_since/バージョンフィールド/冪等性キーに対応しているか)、認証の更新フロー、既存のサービスワーカー登録を確認します。
  2. 計画を報告し、広範囲に及ぶ変更を行う前に、スキーマ、送信待ちキュー、競合ポリシーをユーザーに伝えます。大規模なアプリでは、エンティティごとに TODO を列挙します。
  3. インストール:npm install @capgo/capacitor-fast-sql @capacitor/network && npx cap sync を実行し、references/fast-sql-setup.md に記載されたプラットフォーム設定を行います(必須:iOS の ATS ローカルネットワーク、Android の localhost クリアテキスト通信)。
  4. スキーマと送信待ちキュー:references/sync-engine.md を参照してください。
  5. 同期のトリガー:アプリ起動時、networkStatusChange で接続時、App の resume 時、各ローカル書き込みの後(デバウンス)、必要に応じてバックグラウンドタスク。
  6. UI:送信待ち件数/最終同期時刻を表示し、ネットワークを理由に書き込みをブロックしないでください。
  7. 検証(以下を参照)。

関連するトピックのリファレンスのみを読み込んでください。

最小構成

import { FastSQL } from '@capgo/capacitor-fast-sql';
import { Network } from '@capacitor/network';
import { App } from '@capacitor/app';

const db = await FastSQL.connect({ database: 'app' });
await db.execute(`CREATE TABLE IF NOT EXISTS outbox (
  id TEXT PRIMARY KEY, entity TEXT NOT NULL, op TEXT NOT NULL,
  payload TEXT NOT NULL, created_at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0
)`);

export async function saveTodo(todo: Todo) {
  await db.transaction(async (tx) => {
    await tx.run('INSERT OR REPLACE INTO todos (id, text, done, updated_at, dirty) VALUES (?, ?, ?, ?, 1)',
      [todo.id, todo.text, todo.done ? 1 : 0, Date.now()]);
    await tx.run('INSERT INTO outbox (id, entity, op, payload, created_at) VALUES (?, ?, ?, ?, ?)',
      [crypto.randomUUID(), 'todo', 'upsert', JSON.stringify(todo), Date.now()]);
  });
  void sync(); // fire and forget; sync() is single-flight
}

Network.addListener('networkStatusChange', (s) => { if (s.connected) void sync(); });
App.addListener('resume', () => void sync());

データ行と送信待ちキューのエントリーは、クラッシュによって片方だけが残ることのないよう、1 つのトランザクションで書き込みます。

注意点

  • Network.getStatus().connected === true でも API に到達できるとは限りません(キャプティブポータル、VPN、サーバーダウンなど)。fetch の失敗はオフラインとして扱い、connected は再試行のヒントとしてのみ使用します。
  • networkStatusChange は連続して複数回発生することがあります。sync() は単一実行にしてください。
  • すべての変更リクエストに冪等性キー(送信待ちキューの行 ID)を付けてください。タイムアウト後に再試行すると、付けていない場合はレコードが重複します。
  • エンティティごとに順番どおり送信待ちキューを処理してください。削除が作成より先に処理されてはなりません。
  • プルのカーソルには、デバイスの時計ではなく、レスポンスに含まれるサーバーのタイムスタンプ/バージョンを使用してください。
  • オフライン中に認証トークンが期限切れになります:送信待ちキューを処理する前に更新し、401 の場合もエントリーを破棄せず保持してください。
  • SQLite の行に大きな Blob を保存しないでください。@capacitor/filesystem でファイルを保存し、DB にはパスを保持してください。
  • iOS のバックグラウンド実行は状況に左右されます。バックグラウンドタスクは役立ちますが、同期タイミングは保証できません。resume 時のフォアグラウンド同期が確実な方法です。

検証

  1. デバイスで機内モードをオンにし、レコードを作成、編集、削除します。アプリを強制終了して再起動しても、データが保持されていることを確認します。
  2. ネットワークを再びオンにします。アウトボックスが 0 件になり、サーバーにすべての変更が一度だけ反映される(重複がない)ことを確認します。
  3. 競合テスト:2 台のデバイスをオフラインにして同じレコードを編集し、両方を再接続します。結果が選択したポリシーに一致することを確認します。
  4. 不安定なネットワーク:Android エミュレーターのネットワークスロットリング/iOS Network Link Conditioner(設定 -> デベロッパ)を使用します。再試行の間隔が徐々に長くなり、データが失われないことを確認します。
  5. ネイティブの確認:
grep -rn "serviceWorker.register" src/ | grep -v "isNativePlatform"   # should be empty or web-guarded
/usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsLocalNetworking" ios/App/App/Info.plist
grep -n "networkSecurityConfig\|usesCleartextTraffic" android/app/src/main/AndroidManifest.xml
  1. DB を確認します。デバッグ画面で SELECT COUNT(*) FROM outbox を表示するか、ファイルを取得します(adb exec-out run-as com.example.app ls databases/)。

エラー処理

エラー/症状原因解決方法
"CapgoCapacitorFastSql" plugin is not implemented on android / Plugin not implementedサービスワーカーまたは WKAppBoundDomains によるインジェクションのブロック、あるいは npx cap sync の未実行ネイティブビルドから SW を削除する/ios.limitsNavigationsToAppBoundDomains を設定する。npx cap sync を実行する
iOS:The resource could not be loaded because the App Transport Security policy requires the use of a secure connectionFast SQL の localhost 通信が ATS によってブロックされているNSAllowsLocalNetworking を追加する
Android:CLEARTEXT communication to localhost not permitted by network security policylocalhost へのクリアテキスト通信がブロックされているlocalhost の例外を network_security_config.xml に追加する
pull 中の SQLITE_CONSTRAINT: UNIQUE constraint failedサーバーの行 ID がローカルにすでに存在するINSERT ... ON CONFLICT(id) DO UPDATE を使用する
database is locked並列書き込み/長時間のトランザクション書き込みを直列化する。トランザクションを短く保つ。同期を単一実行にする
iOS のストレージ圧迫後にデータが消失するlocalStorage/IndexedDB に保存されているSQLite に移行する
再接続後にサーバーレコードが重複する冪等性キーなしで POST が再試行されているoutbox の ID を Idempotency-Key として送信する。サーバー側で重複を排除する

リソース

コメント