0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

伝書鳩の飛行速度で届くメッセージ——距離・ETA・遅延配送の設計メモ

0
Last updated at Posted at 2026-10-28

いまのメッセージアプリは、だいたい「書いた瞬間に届く」前提です。一方 Carrier Pidge は、あえて逆を行きます。友だちへ手紙を書き、伝書鳩の現実的な飛行速度で距離を運ばせ、ルートと到着予定時刻(ETA)を地図上で見守る——待つこと自体がプロダクトの中心です。

Carrier Pidge 日本語トップ

本稿では日本語版の公開説明から読み取れる制約をもとに、「距離から到着時刻を決め、サーバ側で遅延配送する」ときの設計メモをまとめます。コードはすべてサンプルコードであり、プロダクトのソースではありません。

プロダクトから見える制約

公開ページの要点を、実装の言葉に直すと次のようになります。

  • インスタントメッセージではない。意図的に遅い
  • 承認済みの友だちに短いメッセージを託す
  • 各メッセージにルートと飛行があり、地図で追跡できる。ETAは変化しうる
  • 鳩舎(ロフト)は おおまかなエリア を使う。正確な住所は共有せず、バックグラウンド追跡もしない
  • 世界中の友だち向け。おおまかなグローバル位置から飛行距離を計算する

待つことが体験の中心(4ステップ)

技術的な核は、「送信APIが成功した=受信箱に本文が見える」ではなく、送信成功=飛行ジョブがスケジュールされた という状態遷移です。

距離から飛行時間を出す

伝書鳩の巡航速度は文献によって幅がありますが、プロダクトが「現実の鳩速度」をうたうなら、サーバ側で 定数の速度モデル を一つ決め、距離に比例した飛行時間を出すのが素直です。以下は説明用のサンプルです。

// サンプル: 巡航 60 km/h、昼夜を問わず等速と仮定(単純化)
const PIGEON_KMH = 60;

/** ハバースイン(大円距離, km) */
function haversineKm(
  a: { lat: number; lon: number },
  b: { lat: number; lon: number },
): number {
  const R = 6371;
  const toRad = (d: number) => (d * Math.PI) / 180;
  const dLat = toRad(b.lat - a.lat);
  const dLon = toRad(b.lon - a.lon);
  const lat1 = toRad(a.lat);
  const lat2 = toRad(b.lat);
  const h =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}

function estimateFlight(from: { lat: number; lon: number }, to: { lat: number; lon: number }) {
  const distanceKm = haversineKm(from, to);
  const durationMs = (distanceKm / PIGEON_KMH) * 3600_000;
  return { distanceKm, durationMs };
}

ポイント:

  1. 速度はサーバ定数。 クライアントが「もう着いたことにする」再計算をしても認可に使わない
  2. 距離の入力は粗い座標。 公開文にある通り、正確な住所ではなくエリア代表点(例: 市区の中心、ジオハッシュのセル中心)を使う
  3. ETAは再計算されうる。 モデルを更新した場合や、経路の見せ方を変えた場合に備え、表示用ETAと「配達確定時刻」を分けて持つと安全です

粗い位置:距離は欲しいが住所は出したくない

場所を明かさずに距離を感じる

距離体験とプライバシーは衝突しやすいです。Carrier Pidge の説明は、ここを明示しています——おおまかなエリアで意味のあるルートを作り、正確な住所は共有しない。

実装のイメージ(サンプル):

/** 例: 約 20〜30km 粒度のセル中心へ丸める */
function coarsen(lat: number, lon: number, gridDeg = 0.25) {
  const q = (v: number) => Math.round(v / gridDeg) * gridDeg;
  return { lat: q(lat), lon: q(lon) };
}

function loftForUser(userId: string, precise: { lat: number; lon: number }) {
  const area = coarsen(precise.lat, precise.lon);
  // DBには area だけを友だち向けに露出。precise は暗号化 or 別権限
  return { userId, area };
}

友だち画面に出すのは「広い地域」まで。リアルタイムGPSの連続送信はしない——公開FAQの「バックグラウンドで追跡しない」と整合します。

遅延配送:送信と受信を時間で切り離す

飛行が数時間〜数日に及ぶなら、リクエストを開いたまま待つわけにはいきません。典型パターンは次です。

  1. POST /messages で本文を保存し、状態を in_flight に
  2. deliver_at = now + durationMs を確定(またはジョブキューに遅延投函)
  3. ワーカーが deliver_at <= now を拾い、状態を delivered に変え、通知を送る
  4. 受信APIは delivered のみ本文を返す(飛行中はルート/ETAメタだけ)
type Letter = {
  id: string;
  fromUserId: string;
  toUserId: string;
  body: string;
  fromArea: { lat: number; lon: number };
  toArea: { lat: number; lon: number };
  distanceKm: number;
  departedAt: Date;
  deliverAt: Date;
  status: 'in_flight' | 'delivered';
};

function serializeLetter(letter: Letter, viewerId: string, now = new Date()) {
  const isParty = viewerId === letter.fromUserId || viewerId === letter.toUserId;
  if (!isParty) return null;

  const progress = Math.min(
    1,
    Math.max(0, (now.getTime() - letter.departedAt.getTime()) /
      (letter.deliverAt.getTime() - letter.departedAt.getTime())),
  );

  const delivered = letter.status === 'delivered' || now >= letter.deliverAt;

  return {
    id: letter.id,
    distanceKm: letter.distanceKm,
    eta: letter.deliverAt.toISOString(),
    progress,
    status: delivered ? 'delivered' : 'in_flight',
    // 飛行中は本文を出さない(サンプル)
    body: delivered ? letter.body : null,
  };
}

ここでも Deco My Tree の時間ロックと同じ原則が効きます——届いていない本文をクライアントに渡さない。進捗バーは progress とルート描画用の端点だけで足ります。

タイムゾーンと通知

ETA表示はユーザのローカルタイムゾーンで行い、配達判定はサーバの UTC(または単一の基準時計)で行います。通知は「配達瞬間」に一度送るのが基本ですが、長距離フライトでは途中ステータス(例: 半分通過)を出すかどうかはプロダクト判断です。出すなら、通知ペイロードに本文を載せないこと。

// サンプル: 配達ワーカー
async function deliverDue(now = new Date()) {
  const due = await db.letters.findMany({
    where: { status: 'in_flight', deliverAt: { lte: now } },
  });
  for (const letter of due) {
    await db.letters.update(letter.id, { status: 'delivered' });
    await notify(letter.toUserId, {
      type: 'pigeon_arrived',
      letterId: letter.id,
      // body は入れない
    });
  }
}

クライアント時計で「着いたので本文APIを呼ぶ」は認可に使わず、サーバが delivered と認めたものだけを返します。

フロントの役割

  • やる: ルート線と変化するETAの表示、飛行中カード、配達後の本文表示
  • やらない: ローカルで deliverAt を早めるチート、飛行中本文の先読みキャッシュ、精密GPSの常時送信

「見守る」体験(公開の4ステップの3番目)は、サーバが返す progress/端点/ETAの再取得間隔設計そのものです。ポーリング間隔はフライト長に応じて伸ばすと負荷と電池に優しいです。

まとめ

Carrier Pidge のような「遅いメッセージ」は、機能追加というより 時間を一級のドメイン概念にする 設計です。

  1. 粗い位置から距離を計算し、サーバ定数の速度で飛行時間を出す
  2. 送信=ジョブ登録、受信本文は配達後のみ
  3. ETA表示と配達判定の時計を分ける
  4. 通知とAPIの両方で、未配達本文を漏らさない

実際のUIとコピーは Carrier Pidge で確認できます。距離を物語にするプロダクトでは、ジオ計算そのものより、「いつ・誰に・何を返してよいか」の境界をサーバに置くことの方が、長期的には効いてきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?