再接続とエラーの扱い

WebSocket のクライアントは、接続が切れるものとして作ってください。文字おこしくんのサーバーの再起動・ネットワークの揺れ・プロキシのアイドル切断・クライアントの受信の遅れで、つながっている接続は切れます。

判断の表

エラーは {"type":"error","error":{"code":"…","message":"…","fatal":true|false}} で届きます。fatal: true なら、文字おこしくんはこの後に接続を閉じます。

起きたこと どうするか
fatal: false の error 接続を使い続ける(Ingest なら送った内容を直す)
unauthorized 再接続しない。新しいトークンを取ってからやり直す
protocol_version_unsupported 再接続しない。クライアントを更新する
session_ended(エラーでもメッセージでも) 再接続しない。そのセッションは終わった
superseded 再接続しない(Ingest)。同じセッションに別の送り手がつないだ
quota_exceeded 再接続しない(Ingest)。利用上限に達し、セッションも終わっている。次の UTC 0 時の後に、新しいセッションを作る
fatal な invalid_message 再接続しない。文字おこしくんが hello を拒んだので、同じ hello を送り直しても通らない
fatal な rate_limited 上限の待ち時間(30 秒)をおいて再接続する
fatal な internal・知らないコード バックオフで再接続する
error を受け取らずに切れた バックオフで再接続する
非 fatal な error の直後に切れた(hello のタイムアウト・受信の遅いクライアントへの rate_limited など) バックオフで再接続する
接続するときに、文字おこしくんが HTTP 503 / 429(too_many_connections)で断った バックオフで再接続する

バックオフ

待ち時間 = min(30 秒, 0.5 秒 × 2^試行回数) × (1 − 0.5 × random())

生存確認

ブラウザは WebSocket の ping / pong フレームを扱えないので、クライアントは、接続が生きているかを、プロトコルの JSON で確かめます。

再接続したとき

Subscribe

Ingest で送り直すもの(話者・トラック・切断中の音声)と順番は、音声を送るにあります。

トークンの期限

疑似コード

const STOP = new Set(["unauthorized", "protocol_version_unsupported", "session_ended", "superseded", "quota_exceeded"]);
let attempt = 0, stopped = false, openedAt = 0;

function connect() {
  const ws = new WebSocket(url);
  ws.onopen = () => ws.send(JSON.stringify({ type: "hello", protocolVersion: 1, token: currentToken() }));
  ws.onmessage = ({ data }) => {
    const m = JSON.parse(data);
    if (m.type === "welcome") openedAt = Date.now();
    if (m.type === "session_ended") stopped = true;
    if (m.type === "error") {
      const { code, fatal } = m.error;
      if (STOP.has(code) || (fatal && code === "invalid_message")) stopped = true;
      else if (fatal && code === "rate_limited") attempt = 6; // 次は上限の 30 秒
    }
    handle(m);
  };
  ws.onclose = () => {
    if (stopped) return;
    if (openedAt !== 0 && Date.now() - openedAt > 10_000) attempt = 0;
    openedAt = 0;
    const delay = Math.min(30_000, 500 * 2 ** attempt++) * (1 - 0.5 * Math.random());
    setTimeout(connect, delay);
  };
}

参照実装 examples/mojiokoshi-subscribe.js に、疑似コードを完成させたものがあります。

コーディングエージェント向けのプロンプト

文字おこしくんの WebSocket クライアント(Subscribe / Ingest)に、再接続とエラーの扱いを実装してください。
仕様: https://moji.kaitoranma.com/docs/reconnection.md と errors.json
(https://moji.kaitoranma.com/docs/errors.json)。

- 予期しない切断では min(30s, 0.5s × 2^n) × (1 − 0.5 × random) の指数バックオフで再接続する。welcome から 10 秒以上続いた接続の後は n を 0 に戻す。
- unauthorized / protocol_version_unsupported / session_ended / superseded / quota_exceeded と fatal な invalid_message、session_ended メッセージでは再接続しない。fatal な rate_limited は 30 秒待つ。知らないコードは fatal に従い、fatal なら internal と同じく再接続する。
- close code では判断しない。直前の error.code で判断する。
- welcome の後 15 秒ごとに {"type":"ping"} を送り、10 秒ほど何も届かなければ自分で閉じて再接続する。
- 再接続では hello からやり直し、Subscribe は welcome で状態を丸ごと置き換える。トークンは再接続のたびに有効なものを使う。
- 偽の WebSocket と偽のタイマーで、上の分岐をすべてテストする(実サーバーには依存しない)。