RORK LABEN
PRODUCT — Rork Maxがネイティブ Swift アプリを生成。iPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageに対応しますNATIVE — Rork MaxはAR/LiDAR・Metalの3Dゲーム・Dynamic Island・Live Activities・HealthKit・Core MLなどを解放しますCLASSIC — 通常のRorkはReact Native(Expo)で、英語の説明だけからiOS/Androidアプリを生成しストア配信できますFUNDING — a16zから$2.8Mを調達(別途$15Mも)。月743,000訪問・成長率85%と伸びていますPRICING — 無料で始められ、有料プランは月25ドル〜。Rork Maxは月200ドルですCHOICE — クロスプラットフォームのRorkか、Apple専用機能まで踏み込むRork Maxか、用途で選び分けられますPRODUCT — Rork Maxがネイティブ Swift アプリを生成。iPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageに対応しますNATIVE — Rork MaxはAR/LiDAR・Metalの3Dゲーム・Dynamic Island・Live Activities・HealthKit・Core MLなどを解放しますCLASSIC — 通常のRorkはReact Native(Expo)で、英語の説明だけからiOS/Androidアプリを生成しストア配信できますFUNDING — a16zから$2.8Mを調達(別途$15Mも)。月743,000訪問・成長率85%と伸びていますPRICING — 無料で始められ、有料プランは月25ドル〜。Rork Maxは月200ドルですCHOICE — クロスプラットフォームのRorkか、Apple専用機能まで踏み込むRork Maxか、用途で選び分けられます
記事一覧/開発ツール
開発ツール/2026-06-23上級

オフラインで書いた投稿が、復帰した瞬間にもう一つ増える——再送に強い送信アウトボックスの設計

永続キューで再送するだけでは、失われた応答が二重投稿を生みます。冪等キー・一時IDの付け替え・毒メッセージの隔離を備えた送信アウトボックスを、動くTypeScriptで設計します。

offline-first2react-native11expo10architecture5idempotency

プレミアム記事

地下鉄のホームで、手記系のアプリに長めの投稿を書いていただいたユーザーがいました。電波がない区間で「保存」を押し、改札を出たあたりで通信が戻る。その数秒後、同じ投稿がタイムラインに二つ並んでいた——というレビューが届いたことがあります。

私自身、個人開発で書き込みのあるアプリをいくつか運用していますが、この「復帰した瞬間に増える一つ」は、オフライン対応を一段進めた人ほど踏みやすい落とし穴だと感じています。永続キューに積んで、再接続したら流す。そこまでは多くの記事が書いている通りで、私も実装してきました。けれど、それだけでは二重投稿は消えません。

ここで扱いたいのは、送信を後ろに回す楽観的更新そのものではなく、その先にある「再送の安全性」です。具体的には、冪等キーで二重書き込みを止め、オフラインで作った一時IDをサーバーのIDへ付け替え、決して成功しないミューテーションを隔離する。書き込み経路を長く運用するための、骨の部分です。

「再送すれば安全」が崩れる瞬間

送信キューの再送は、たいてい「失敗したら、もう一度送る」で書かれています。問題は、何をもって失敗とみなすかです。

ネットワークは、リクエストがサーバーに届き、サーバーが処理を終え、その応答がクライアントへ戻る途中で切れることがあります。クライアントから見ればタイムアウト、つまり失敗です。けれどサーバーから見れば、投稿はすでに作られています。ここで素直に再送すると、サーバーは二つ目の投稿を作ります。

これは設計の不備というより、ネットワーク越しの書き込みが本質的に持つ性質です。応答が失われうる以上、クライアントの再送は「少なくとも一回(at-least-once)」になります。二重を避けたければ、受け取る側が「同じ書き込みを二回数えない」仕組みを持つしかありません。それが冪等キーです。

冪等キーは、生成時に一度だけ決める

肝は、キューに積む瞬間に一度だけキーを発番し、再送では絶対に変えないことです。再送のたびに新しいキーを振ってしまうと、サーバーから見れば別々の書き込みになり、何の役にも立ちません。

まず、アウトボックスの一件を表す型を決めます。

// outbox/types.ts
export type OutboxStatus = 'pending' | 'inflight' | 'failed' | 'dead';
 
export interface OutboxItem {
  localId: string;        // 端末内の主キー(UUID)
  idempotencyKey: string; // サーバーへ渡す冪等キー(生成時に1回だけ)
  endpoint: string;       // 例: 'POST /posts'
  payload: unknown;       // 送信本文
  status: OutboxStatus;
  attempts: number;
  nextAttemptAt: number;  // 指数バックオフの再試行時刻(epoch ms)
  dependsOn?: string;     // 依存する別アイテムの localId(後述)
  createdAt: number;
}

積むときは、idempotencyKey をこの一回で固定します。

// outbox/enqueue.ts
import { randomUUID } from 'expo-crypto';
 
export function buildOutboxItem(
  endpoint: string,
  payload: unknown,
  dependsOn?: string,
): OutboxItem {
  const now = Date.now();
  return {
    localId: randomUUID(),
    idempotencyKey: randomUUID(), // ← ここで決めたら二度と変えない
    endpoint,
    payload,
    status: 'pending',
    attempts: 0,
    nextAttemptAt: now,
    dependsOn,
    createdAt: now,
  };
}

送信時は、このキーをヘッダーに載せます。サーバー側は、同じキーの二回目を受け取ったら新規作成せず、一回目の結果をそのまま返す約束にします。

// outbox/send.ts
async function send(item: OutboxItem): Promise<Response> {
  return fetch(`https://api.example.com${item.endpoint.split(' ')[1]}`, {
    method: item.endpoint.split(' ')[0],
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': item.idempotencyKey, // 再送でも同じ値
    },
    body: JSON.stringify(item.payload),
  });
}

サーバーを自分で持っている場合、受け側は「キーを主キーにした受信ログ」を一行書くだけで足ります。INSERT ... ON CONFLICT DO NOTHING で二回目を弾き、すでにある結果を返す。Stripe のような外部 API なら、多くが Idempotency-Key ヘッダーを公式に受け付けますので、それに合わせます。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
再送が二重書き込みを生む仕組みと、Idempotency-Key で止める実装
オフラインで作った親に子がぶら下がる依存を、一時IDの付け替えで解く方法
常に422で失敗するミューテーションを隔離し、キュー全体を詰まらせない設計
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-05-23
Rork 生成プロジェクト特有の expo start --offline forbidden — テンプレ構成が原因の4パターン
Rork が生成するテンプレート構成が原因で expo start --offline が forbidden になる4つのパターン(tsconfigPaths・expo-router キャッシュ・ネイティブプリビルド・lock 不一致)を、Rork プロジェクト固有の観点で切り分けます。汎用的な Expo 側の対処(プロキシ403・依存検証)は別記事にまとめています。
開発ツール2026-05-20
iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処
Rork で作った iOS アプリが起動直後に消える。クラッシュログに 0x8badf00d。Apple のウォッチドッグが React Native の重い初期化を切ったときの正しい直し方を、私の運用中アプリで実際に効いた手順で説明します。
開発ツール2026-03-29
AIエージェント × モバイルアプリ設計 — 「全部AIにやらせない」関心の分離アーキテクチャ
モバイルアプリにAIエージェントを組み込む際、すべてをAIに任せるのではなく決定論的タスクを分離する設計パターンを解説。Rorkアプリでの実践的なアーキテクチャ例を紹介します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →