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?

TypeScriptで画像生成ジョブのポーリングを安全に実装する

0
Posted at

画像生成APIは、通常のフォーム送信より長く実行されます。リクエストがタイムアウトしたとき、「生成に失敗した」とは限りません。サーバーではジョブが作成済みで、レスポンスだけが失われた可能性もあります。

ここで同じ生成リクエストをすぐ再送すると、ジョブやクレジット消費が重複するおそれがあります。本稿では、image-to-imageのWeb UIを想定し、TypeScriptで安全にジョブ状態を追跡する設計をまとめます。コードは一般的な実装例であり、特定サービスの内部コードではありません。

ジョブ状態を判別可能なUnionにする

まず、loading: booleanではなく、APIの状態を判別可能なUnionで表します。

type JobState =
  | { type: "queued"; jobId: string }
  | { type: "processing"; jobId: string; progress?: number }
  | { type: "reviewing"; jobId: string; resultUrl: string }
  | { type: "completed"; jobId: string; resultUrl: string }
  | { type: "failed"; jobId: string; code: string; retryable: boolean }
  | { type: "unknown"; jobId: string; reason: "network" | "timeout" };

unknownは重要です。クライアントが状態を取得できないことと、サーバー上のジョブが失敗したことを分離できます。

reviewingも独立させます。モデルがファイルを返しても、人物、商品、文字、境界、光、構図が意図どおりとは限りません。システム上の成功を、そのままユーザーの受け入れ完了にしないための状態です。

ポーリング関数はキャンセル可能にする

画面遷移や新しいジョブの開始後も古いタイマーが残ると、別ジョブの結果でUIを上書きする可能性があります。AbortSignalを受け取り、待機中もキャンセルできるようにします。

const wait = (ms: number, signal: AbortSignal) =>
  new Promise<void>((resolve, reject) => {
    const timer = window.setTimeout(resolve, ms);
    signal.addEventListener("abort", () => {
      window.clearTimeout(timer);
      reject(new DOMException("Aborted", "AbortError"));
    }, { once: true });
  });

async function pollJob(
  jobId: string,
  signal: AbortSignal,
  onState: (state: JobState) => void,
) {
  let delay = 1000;

  while (!signal.aborted) {
    try {
      const response = await fetch(`/api/jobs/${jobId}`, { signal });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);

      const state = await response.json() as JobState;
      onState(state);

      if (["reviewing", "completed", "failed"].includes(state.type)) return;
      delay = Math.min(Math.round(delay * 1.6), 10000);
    } catch (error) {
      if (signal.aborted) return;
      onState({ type: "unknown", jobId, reason: "network" });
      delay = Math.min(Math.round(delay * 2), 15000);
    }

    await wait(delay, signal);
  }
}

実運用ではランダムなjitterも加えます。多数のクライアントが同時に再接続したとき、同じ秒に問い合わせが集中するのを避けるためです。

Reactではeffectのcleanupで古い監視を止める

useEffect(() => {
  if (!jobId) return;

  const controller = new AbortController();
  void pollJob(jobId, controller.signal, setJobState);

  return () => controller.abort();
}, [jobId]);

依存対象はジョブIDです。フォームのプロンプトやモデル選択を依存配列に入れると、編集中の変更だけでポーリングが再起動します。実行中ジョブは、送信時に固定したリクエストスナップショットと結び付けます。

タブが非表示なら頻度を下げる

画像生成中に別タブへ移動する利用者は多いため、document.visibilityStateを使って非表示中の問い合わせ間隔を伸ばせます。ただし、非表示になった瞬間に監視情報を破棄してはいけません。

ジョブIDをsession storageやサーバー側のユーザーセッションに保存し、画面再読み込み後も同じジョブを再取得できるようにします。保存するのは追跡に必要な識別子であり、期限付きの結果URLを永続的なジョブIDの代わりに使わないようにします。

作成APIには冪等キーを付ける

ポーリングが安全でも、ジョブ作成が重複すれば意味がありません。ユーザーが生成を確定した時点で冪等キーを作成し、同じリクエストの再送では既存のジョブIDを返します。

const response = await fetch("/api/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify(requestSnapshot),
});

HTTP応答が失われても、同じキーを使います。プロンプト、参照画像、モデル、アスペクト比、解像度などをユーザーが変更し、明示的に新しい試行を始めるときだけ新しいキーを発行します。

エラーごとに復旧動作を変える

  • ネットワークエラー: ジョブIDを保持し、状態を再取得する
  • 能力バージョン競合: 最新設定を取得し、画像とプロンプトを保持する
  • 認証・クレジット不足: キュー投入前に止め、復帰後に再検証する
  • モデル失敗: 安全に表示できる分類を示し、自動再実行しない
  • 結果URLの期限切れ: アクセス認可を更新し、画像を再生成しない
  • 品質不合格: 元のスナップショットを残して新しい試行を作る

クライアント側の一定時間経過は「失敗」ではなく「現在確認できない」と扱います。サーバー側の終端状態だけがジョブ失敗の根拠です。

製品制約を状態設計に反映する

具体例として参照したImage to Image Generatorでは、単一画像モードはJPEG・PNG・WebPの1枚、複数画像合成は2〜5枚を入力します。公開UI上の上限は1ファイル24MB、プロンプトは1000文字です。モデル、アスペクト比、解像度、ログイン要件、クレジットコスト、可用性は変わる可能性があり、4Kは対応するワークフローに限られます。

また、処理時間、完全な変換、人物同一性、欠陥のない出力、独自性、第三者権利のクリアランスは保証できません。したがって、ポーリングの終点を単なる「ファイル取得」にせず、結果確認へ接続する必要があります。

設計の背景にした自作ツールはImage to Image Generatorです。再利用できる要点は、タイムアウトと失敗を分け、ジョブIDと不変スナップショットを保持し、監視をキャンセル可能にすることです。

homepage-desktop.png

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?