Skip to content

refactor(app): carve count-in (and the song-credits overlay) out of app.js (R3a)#890

Merged
byrongamatos merged 1 commit into
mainfrom
feat/r3-countin-module
Jul 11, 2026
Merged

refactor(app): carve count-in (and the song-credits overlay) out of app.js (R3a)#890
byrongamatos merged 1 commit into
mainfrom
feat/r3-countin-module

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #889 (the state container). static/js/count-in.js (389) — bodies verbatim. app.js 8,223 → 7,913.

Third slice out of the strongly-connected core, and the first that writes shared state rather than only reading it. #889's container is what makes it possible.

imports : loops (setLoop / loopA / loopB — a count-in inside an A–B loop must begin at A),
          audio-el, player-state, host
hooks   : _audioSeek, setPlayButtonState, _songEventPayload, togglePlay + a jucePlayer getter

Nothing imports count-in back — app.js and section-practice both reach it through the seam — so the graph stays acyclic.

The constraint produced better code again

app.js's autoplay path used to reach in and set this module's credits timers itself (_creditsTimer, _creditsHideOnPlay) and read _countingIn. It can't now — and it shouldn't have to. So the module exports the operations: armCreditsHideOnPlay(), scheduleCreditsHide(), holdCreditsThen(start), isCountingIn().

That's the third time this has happened (section-practice's resetSelection, loops' state ownership), and each time the constraint produced something better than what was there: the module keeps its own promises, instead of trusting a caller 6,000 lines away to zero the right fields.

The no-undef gate found five missed members, one at a time

showSongCreditsOverlay and startSongCountIn (my name regex matched startCountIn, not startSongCountIn), then _creditLineLabel, _CREDITS_MAX_MS, _CREDIT_ROLE_VERBS.

A call-graph closure doesn't see a const table. Only the undefined-symbol pass does. This is why every carve ends with it.

And a real trap worth recording

I computed _CREDIT_ROLE_VERBS's span against the already-modified app.js and applied it to the clean one. The line numbers had drifted — the slice would have cut somewhere else entirely. Recomputed every span from the clean file with acorn.

Never carry line numbers across an edit.

Verification

A/B against origin/main in two browsers with a real song: playback state, the public feedBack.isPlaying mirror, audio position, cancel-count-in — identical, zero page errors.

Unit coverage moved with the code: loop_restart's count-in cancellation-token test and the 5 song_credits_overlay tests now read count-in.js; loop_restart's sandbox gains a host object routed at its existing stubs, so every assertion is unchanged.

Honest limit

I could not make the count-in overlay actually render headlessly — its autoplay path needs a fresh-load _pendingAutostart that a scripted playSong() never arms. Behaviour is identical to main on every probe and the unit tests cover the logic, but the on-screen 1-2-3-4 and the credits card want a human look.

pytest 2396 · node 1040/1040 · ESLint 0 (no-cycle clean) · tailwind clean · Codex 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a four-beat count-in before playback and loop restarts.
    • Added song credits overlays with automatic dismissal and safe author-name display.
    • Improved playback coordination between count-ins, autoplay, seeking, and credits.
  • Bug Fixes

    • Prevented timing updates from interfering while a count-in is active.
  • Tests

    • Updated playback restart and credits overlay coverage for the new behavior.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Count-in and credits playback flow

Layer / File(s) Summary
Count-in state and credits overlay
static/js/count-in.js
Adds click generation, cancellation state, numeric countdown UI, credit-role labels, and XSS-safe song-credit overlay handling.
Count-in playback orchestration
static/js/count-in.js
Implements loop/song countdowns, rewind and seek coordination, playback events, state updates, and autoplay credit-timing helpers.
Application integration and host seam
static/app.js
Delegates count-in and credits behavior to the module, updates highway timing guards, and expands host-seam exports.
Module-based test loading
tests/js/loop_restart.test.js, tests/js/song_credits_overlay.test.js
Updates test extraction and sandbox wiring for the new module and host callbacks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant CountIn
  participant Host
  participant Audio
  App->>CountIn: startCountIn(options)
  CountIn->>Host: _audioSeek(loopA)
  Host->>Audio: Seek to loop A
  CountIn->>Audio: Play four count-in clicks
  CountIn->>Host: Update state and emit playback event
  Host->>Audio: Start playback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main refactor: moving count-in and song-credits overlay logic out of app.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/r3-countin-module

Comment @coderabbitai help to get the list of available commands.

…pp.js (R3a)

static/js/count-in.js (389) — bodies VERBATIM. app.js 8,223 → 7,913.
The third slice out of the strongly-connected core, and the first that WRITES
shared state rather than only reading it. #889's container is what makes it possible.

  imports: loops (setLoop/loopA/loopB — a count-in inside an A-B loop must begin at
           A), audio-el, player-state, host
  hooks  : _audioSeek, setPlayButtonState, _songEventPayload, togglePlay + a
           jucePlayer getter
  Nothing imports count-in back — app.js and section-practice both reach it through
  the seam — so the graph stays acyclic.

app.js's autoplay path used to reach IN and set this module's credits timers itself
(_creditsTimer, _creditsHideOnPlay) and read _countingIn. It cannot now, and should
not have to, so the module exports the OPERATIONS instead — armCreditsHideOnPlay(),
scheduleCreditsHide(), holdCreditsThen(start), isCountingIn() — and owns its own
timer invariants. Third time this has happened (section-practice's resetSelection,
loops' state) and each time the constraint produced better code than was there
before: the module keeps its own promises instead of trusting a caller 6,000 lines
away to zero the right fields.

THE no-undef GATE FOUND FIVE MISSED MEMBERS, one at a time: showSongCreditsOverlay
and startSongCountIn (my name regex matched startCountIn, not startSongCountIn),
then _creditLineLabel, _CREDITS_MAX_MS, and _CREDIT_ROLE_VERBS. A call-graph closure
does not see a const table; only the undefined-symbol pass does.

AND A REAL TRAP: I computed _CREDIT_ROLE_VERBS's span against the ALREADY-MODIFIED
app.js and applied it to the clean one — the line numbers had drifted, so the slice
would have cut somewhere else entirely. Recomputed every span from the clean file
with acorn. Never carry line numbers across an edit.

VERIFIED. A/B against origin/main in two browsers with a real song: playback state,
the public feedBack.isPlaying mirror, audio position, cancel-count-in — IDENTICAL,
zero page errors. Unit coverage moved with the code: loop_restart's count-in
cancellation-token test and the 5 song_credits_overlay tests now read count-in.js;
loop_restart's sandbox gains a `host` object routed at its EXISTING stubs, so every
assertion is unchanged.

HONEST LIMIT: I could not make the count-in OVERLAY actually render headlessly —
its autoplay path needs a fresh-load _pendingAutostart that a scripted playSong()
never arms. Behaviour is identical to main on every probe and the unit tests cover
the logic, but the on-screen 1-2-3-4 and the credits card want a human look.

pytest 2396, node 1040/1040, ESLint 0 (no-cycle clean), tailwind clean, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos force-pushed the feat/r3-countin-module branch from 7a4bb6d to 80abdd6 Compare July 11, 2026 19:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/js/loop_restart.test.js (1)

130-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hoist the repeated source-loading line into a single constant.

fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '') is duplicated across four tests (lines 130, 182, 217, 233). The sibling file (tests/js/song_credits_overlay.test.js) already hoists this into a single SRC module-level constant — apply the same pattern here to avoid redundant file I/O per test and keep the two files consistent.

♻️ Proposed refactor
+const SRC = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
+
 test('loop:restart fires once when wrap path runs', async () => {
-    const src = fs.readFileSync(APP_JS, 'utf8').replace(/^export /gm, '');
+    const src = SRC;

(repeat for the other three occurrences at lines 182, 217, 233)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/js/loop_restart.test.js` around lines 130 - 233, Hoist the repeated
APP_JS source-loading expression into a module-level SRC constant in
tests/js/loop_restart.test.js, matching the pattern used by the sibling test
file. Replace all four per-test fs.readFileSync(...).replace(...) assignments in
the affected loop:restart tests with SRC, without changing test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/js/loop_restart.test.js`:
- Around line 130-233: Hoist the repeated APP_JS source-loading expression into
a module-level SRC constant in tests/js/loop_restart.test.js, matching the
pattern used by the sibling test file. Replace all four per-test
fs.readFileSync(...).replace(...) assignments in the affected loop:restart tests
with SRC, without changing test behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f14144d6-7730-4131-ae19-e7161ef29e8b

📥 Commits

Reviewing files that changed from the base of the PR and between 5fb28d5 and 80abdd6.

📒 Files selected for processing (4)
  • static/app.js
  • static/js/count-in.js
  • tests/js/loop_restart.test.js
  • tests/js/song_credits_overlay.test.js

@byrongamatos
byrongamatos merged commit 11f8c36 into main Jul 11, 2026
5 checks passed
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