diff --git a/docs/backend.md b/docs/backend.md index 5c32de9..305f2da 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -464,6 +464,14 @@ Node版との差分(意図的な改善): - あくまで表示を先出しするだけで、**実際に引けるかは常にサーバーが決める** (端末の時計は書き換えられる)。残り時間は8時間(最長のクールダウン)で頭打ち。 別端末では保存が無いので、従来どおり peek の応答から復元される。 +- **問い合わせが終わるまで「引く」は押せない**(`statusChecked`)。保存済みの状態で + 先に画面を出すため、確認前でもボタンが見えている。ここで押せると、演出の最中に + peek の応答が返って `state` を上書きし、**演出が畳まれて引く前の画面に戻る** + (リロードされたように見える)。併せて、**その問い合わせより後に演出が始まっていたら + 応答を捨てる**(`sceneCount` の世代で判定)。「今 animating か」で判定してはいけない + — 演出が結果より先に終わったときの取り直し(`onLanded`)はまだ `animating` のまま + 呼ぶため、それを捨てると演出が閉じずに固まる。演出中にクールダウンが明けたときの + タイマーも `state` を触らない。 ### フロント diff --git a/web/pages/omikuji.vue b/web/pages/omikuji.vue index 6c871f4..2fa6a77 100644 --- a/web/pages/omikuji.vue +++ b/web/pages/omikuji.vue @@ -19,9 +19,19 @@
- +
+ お伺いを立てています… +
(前回の結果は下に表示されています)
@@ -108,6 +118,11 @@ export default { data() { return { state: "loading", // loading | available | animating | cooldown | empty(物理乱数枯渇) | error + // サーバーへの状態問い合わせが1度でも完了したか。保存済みの状態で先に + // 画面を出すので、これが false の間は「引く」を押せないようにする。 + statusChecked: false, + // 演出を始めた回数。飛行中の問い合わせの応答が古いかどうかの判定に使う。 + sceneCount: 0, result: null, pendingResult: null, // 演出中に保持(着地まで表示しない) remaining: 0, // 次に引けるまでの秒 @@ -157,6 +172,8 @@ export default { if (this.state !== "cooldown" && this.state !== "available") { this.state = "loading"; } + // 応答が返るまでに演出が始まったかを見分けるための世代。 + const sceneAtRequest = this.sceneCount; const token = await this.getToken(); if (!token) return; try { @@ -165,14 +182,29 @@ export default { { github_id: this.user.github_id, peek: true }, { headers: { Authorization: `Bearer ${token}` } } ); + // この問い合わせを投げた後に演出が始まっていたら、応答はもう古い。 + // 抽選側が状態を持っているので触らない(触ると演出が畳まれて引く前の + // 画面に戻ってしまう)。 + // + // 「今 animating か」で判定してはいけない。演出が結果より先に終わった + // ときの取り直し(onLanded)はまだ animating のまま呼ぶため、応答を + // 捨てると演出が閉じずに固まる。 + if (this.sceneCount !== sceneAtRequest) return; this.applyResponse(res.data); } catch (e) { + if (this.sceneCount !== sceneAtRequest) return; this.state = "error"; + } finally { + this.statusChecked = true; } }, // 「引く」→ 演出(鈴の緒儀式)を開始。実際の抽選は鈴が鳴った時(onRang)。 startScene() { if (this.state === "animating") return; + // 確認前は引かせない(ボタンも disabled だが、念のため入口でも止める)。 + if (!this.statusChecked) return; + // 飛行中の問い合わせがあれば、その応答は古いものとして捨てさせる。 + this.sceneCount++; this.pendingResult = null; this._pendingRemaining = 0; this.state = "animating"; @@ -272,6 +304,8 @@ export default { clearInterval(this.timerId); this.timerId = null; this.remaining = 0; + // 演出中にクールダウンが明けても状態を触らない(演出が消えてしまう)。 + if (this.state === "animating") return; // 引ける状態へ。前回結果は残しつつボタンを出す。 this.state = "available"; saveOmikujiState(