再接続とエラーの扱い
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)で断った |
バックオフで再接続する |
- 再接続するかどうかを、WebSocket の close code で判断しないでください(文字おこしくんは close code を規定していません)。あなたのクライアントは、直前に受け取った
error.codeで判断してください。 - 知らないコードも受け入れてください。 文字おこしくんは、互換性のある変更として、エラーコードを追加します。あなたのクライアントは、
fatalに従い、fatal な知らないコードをinternalと同じに扱ってください。 - 一覧と機械可読な版: エラーコード・errors.json
バックオフ
待ち時間 = min(30 秒, 0.5 秒 × 2^試行回数) × (1 − 0.5 × random())
- クライアントは、待ち時間を、1 回目は 0.25〜0.5 秒、2 回目は 0.5〜1 秒 … と倍々に延ばし、30 秒で頭打ちにします。
- ジッター(
random())は必ず入れてください。ジッターを入れると、文字おこしくんのサーバーの再起動のあとに、全員のクライアントが同時につなぎ直すことを避けられます。 welcomeを受け取ってから 10 秒以上つながっていた接続が切れたら、クライアントは、試行回数を 0 に戻します。session_endedと再接続せずに終えるべきエラーの後は、クライアントは、再接続のタイマーと生存確認(ping)のタイマーを止め、接続も閉じます。
生存確認
ブラウザは WebSocket の ping / pong フレームを扱えないので、クライアントは、接続が生きているかを、プロトコルの JSON で確かめます。
- クライアントは、
welcomeの後、15 秒ごとに{"type":"ping"}を送る。文字おこしくんは{"type":"pong"}を返す。 pingを送ってから 10 秒ほど何も届かなければ(pongに限らない)、クライアントは、半開きの接続とみなして接続を自分で閉じ、再接続する。- 15 秒ごとの
pingで、途中のプロキシ(Cloudflare など)のアイドル切断も防げます。
再接続したとき
Subscribe
- クライアントは、もう一度
helloから始め、welcomeで状態を丸ごと置き換えます。 - 文字おこしくんには、切断中の差分を取り戻す仕組みはありません。クライアントは、代わりに
welcome.recent(直近の発話の最新の改訂)で状態を作り直します。記録を取りこぼしたくないなら、セッションが終わった後に記録を HTTP で書き出して、受け取った発話と比べてください(記録と書き出し)。 - 再接続を待っている間は、表示を薄くするなどして、表示が古いかもしれないことを示すとよいでしょう。発声中の表示(
speakingTrackIds)も古いままです。
Ingest で送り直すもの(話者・トラック・切断中の音声)と順番は、音声を送るにあります。
トークンの期限
- 文字おこしくんは、トークンを接続するときだけ確かめます。つながっている接続は、トークンの期限が来ても切れません。
- 期限の後に再接続すると、文字おこしくんは
unauthorized(再接続せずに終えるべきエラー)を返します。長く動かすプログラムは、期限の前(例: 有効期間の 8 割が過ぎたら)に新しいトークンを取り、次の再接続から使ってください。 - ブラウザ・OBS で使う URL のトークンが切れたら、Discord で
/linkを実行して、リンクを取り直します。
疑似コード
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 と偽のタイマーで、上の分岐をすべてテストする(実サーバーには依存しない)。