Skip to content

fix(dealer): reset countdown state when its placeholder leaves the display - #959

Merged
YusukeHirao merged 2 commits into
devfrom
fix/dealer-countdown-reset
Aug 26, 2026
Merged

fix(dealer): reset countdown state when its placeholder leaves the display#959
YusukeHirao merged 2 commits into
devfrom
fix/dealer-countdown-reset

Conversation

@YusukeHirao

Copy link
Copy Markdown
Member

概要

Lanes%countdown(満了時間, ID, 単位)% プレースホルダが、同じ ID が再登場した瞬間から残り 0 に張り付く問題を修正する。

Display はカウントダウン ID ごとの開始時刻を Map に記録するが、一度書き込んだエントリを削除する経路が存在しなかった。そのため同じ ID が2回目以降に描画されると前回の開始時刻を引き継ぎ、経過時間がすでに満了時間を超えているため Math.max(time - elapsed, 0) が常に 0 を返していた。

同時に、この Map は実行中いちども縮まないため、ユニークな ID の数だけエントリが積み上がっていた。

影響していた箇所

ID を再利用する呼び出しはすべて該当する。

呼び出し元 カウントダウン ID 衝突条件
@d-zero/beholder scraper.ts openPage_<url> 同一 URL のナビゲーションリトライ
@d-zero/dealer deal.ts <index>_interval レーン単位のため2件目以降の全アイテム

変更内容

Display.#dropStaleCountDowns()(新規)

表示スタックが作り直されるたびに、スタック上に存在しないカウントダウン ID の開始時刻を破棄する。ID が再登場すると満了時間から数え直され、Map も画面に出ている ID だけを保持する。

フレーム描画時ではなくスタック更新時に判定している。フレーム間隔(既定 33ms)より短い間に「消えて再登場」する ID を取りこぼさないため — deal()lanes.delete(index) → 次アイテムの update() がまさにこの並びになる。

Display の利用者は Lanes のみで、Lanes.write() は毎回 #logs 全件とヘッダーを送るため、判定対象は常に画面全体となる。

verbose モード

verbose には再描画されるフレームが存在せず、1行は出力された瞬間に確定する。カウントダウン行が出るのは待機の開始時点なので、残り時間を追跡せず常に満了時間を出力するようにした(従来はここでも2回目以降が 0 に潰れていた)。

ドキュメント

  • Lanes.update() の JSDoc に %countdown() の構文・ID の寿命・verbose 時の挙動・@example を追加(従来まったく文書化されていなかった)
  • README の「直接使う場合は using」の注記から Display を除去。Displayindex.ts から再エクスポートされておらず exports. のみのため、パッケージ外からは到達できない

テスト

display.spec.ts に 7 件、lanes.spec.ts に 2 件追加。

  • 継続表示中の減算 / 0 クランプ / s 単位の丸め
  • 同じ ID がスタックから消えて再登場したときに満了時間から数え直す
  • フレームを挟まずに消えて再登場した場合も数え直す(スタック更新時判定の回帰ガード)
  • 生存中の ID は数え続け、消えた ID だけが破棄される
  • verbose は毎回満了時間を出力する
  • Lanes 経由: レーン番号だけを ID にした待機が次のアイテムで数え直しになる / 別レーンの ID を巻き込まない

新規の本番コードは「消すとテストが落ちる」ことを実測で確認済み(#dropStaleCountDowns() の呼び出しを外すと 5 件、verbose 分岐を潰すと 1 件が失敗する)。

フレームの取得には Writableresize を発火させて任意のフェイククロック時刻で1フレーム描画させている。fps タイマーの間隔(1000/30 等)は整数に割り切れず、期待値をハードコードできないため。

互換性

公開シグネチャと exports は無変更。意図された挙動へのバグ修正のため patch 相当。

利用側(@d-zero/beholder / @nitpicker/* 等)はコード変更不要で、@d-zero/dealer を上げるだけで表示が直る。

…splay

Display kept one start timestamp per countdown id and never dropped it, so a reused id resolved
against the previous attempt's elapsed time and rendered a remaining value of 0 forever. Every id
that repeats hit this: beholder's `openPage_<url>` on a navigation retry, and deal()'s per-lane
`<index>_interval` from the second item onward. The map also grew one entry per unique id and was
never trimmed for the whole run.

Drop the ids that are no longer on the display stack whenever the stack is rebuilt, so a returning
id counts down from its full duration again. Verbose mode has no repainted frame — each line is
final at the moment it is written — so it renders the full duration and keeps no state at all.
Display is not re-exported from the package entry and `exports` only exposes `.`, so it cannot be
reached from outside the package. Only Lanes can be used directly.
@YusukeHirao
YusukeHirao requested a review from yusasa16 as a code owner August 26, 2026 07:46
@YusukeHirao
YusukeHirao merged commit 6e4f57e into dev Aug 26, 2026
7 checks passed
@YusukeHirao
YusukeHirao deleted the fix/dealer-countdown-reset branch August 26, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant