refactor(app): lift the shared player state onto a container (R3a)#889
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (9)
📝 WalkthroughWalkthroughThe PR introduces shared ChangesPlayback state centralization
Sequence Diagram(s)sequenceDiagram
participant PlaybackControls
participant JUCEWatcher
participant HTML5Audio
participant PlayerState
PlaybackControls->>PlayerState: update S.isPlaying
JUCEWatcher->>PlayerState: snapshot S.isPlaying
JUCEWatcher->>HTML5Audio: switch or resume route
HTML5Audio-->>JUCEWatcher: play or pause result
JUCEWatcher->>PlayerState: commit playback state
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
static/js/host.js (1)
48-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a direct runtime test for
static/js/host.js.tests/js/host_contract.test.jscovers static wiring, but this seam’s runtime guarantees still deserve a small unit test for unconfigured reads, doubleconfigureHost()calls, and writes throwing.🤖 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 `@static/js/host.js` around lines 48 - 99, Add a focused runtime unit test for the host seam exported by host.js, covering that reading a hook before configureHost() throws, calling configureHost() twice throws, and assigning to host properties throws. Keep the test isolated so module state is reset between cases and use the existing public configureHost and host symbols.
🤖 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 `@static/js/host.js`:
- Around line 48-99: Add a focused runtime unit test for the host seam exported
by host.js, covering that reading a hook before configureHost() throws, calling
configureHost() twice throws, and assigning to host properties throws. Keep the
test isolated so module state is reset between cases and use the existing public
configureHost and host symbols.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3036a570-1a39-4761-b1f8-f349250f8d94
📒 Files selected for processing (16)
static/app.jsstatic/js/host.jsstatic/js/loops.jsstatic/js/player-state.jsstatic/js/section-practice.jstests/js/host_contract.test.jstests/js/juce_engine_reroute.test.jstests/js/loop_api.test.jstests/js/loop_restart.test.jstests/js/play_button_reroute_guard.test.jstests/js/playback_app_adapter.test.jstests/js/section_practice_dismiss.test.jstests/js/song_restart.test.jstests/js/song_seek.test.jstests/js/speed_reset.test.jstests/test_plugin_runtime_idempotence.py
static/js/player-state.js — one exported object, two fields. app.js's 70 reference
sites rewritten. Provably a no-op; nothing shrinks.
WHY NOW. Every slice carved out of app.js so far only ever READ the state it shared
(loopA/loopB, _audioSeekGen, currentFilename), so a read-only getter hook was enough
and no container was needed — twice I checked and twice I got away with it. That
runs out at count-in: it genuinely WRITES `isPlaying` (it starts and stops playback,
4 sites) and `lastAudioTime` (2). `import { isPlaying }` then `isPlaying = true`
THROWS — an imported binding cannot be assigned to. So the state has to live on an
object: `S.isPlaying = true` is a property write, and works from any module holding
the same S. Same shape stems, studio, and editor all converged on.
DELIBERATELY SMALL. app.js has ~104 top-level `let` scalars; lifting all of them is
a ~977-site rewrite for no benefit, because most are private to one cluster and
travel with it. Only what a carved module must WRITE goes here. Add on demand.
THE REWRITE IS AST-DRIVEN, NOT TEXTUAL — and that is not fussiness. Of 100 textual
occurrences of these two names, only 70 resolve to the module binding:
* 22 are member accesses (`someObj.isPlaying`, `window.feedBack.isPlaying`)
* 4 are the LOCAL PARAMETER of `function setPlayButtonState(isPlaying)` — a blind
replace yields `function setPlayButtonState(S.isPlaying)`
* 1 is an object key
* 2 are shorthand properties `{ isPlaying }`, which must become
`{ isPlaying: S.isPlaying }` — and acorn gives a shorthand's key and value the
SAME range, so rewriting both produced `isPlaying: S.isPlaying: S.isPlaying`
until I deduped by range
A find-and-replace corrupts all 29. The rewrite walks the AST, skips shadows, member
properties and keys, and replaces identifier RANGES.
`window.feedBack.isPlaying` — the PUBLIC mirror — is a different thing and is
untouched. Two test sandboxes stub it; those were left alone deliberately.
VERIFIED WITH REAL PLAYBACK. A/B against origin/main in two browsers, real song:
togglePlay -> the public mirror goes true -> false -> true across two toggles, the
audio element's paused state follows, seekBy works — IDENTICAL, zero page errors.
Harnesses: 8 vm-sandbox suites slice playback code out of app.js and now see
S.isPlaying — juce_engine_reroute, loop_restart, play_button_reroute_guard,
playback_app_adapter, song_restart, song_seek, speed_reset, and the python
idempotence source-assert. Each gets the same container in its sandbox; every
assertion is unchanged.
pytest 2396, node 1040/1040, ESLint 0, tailwind clean, Codex 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61c9950 to
212afca
Compare
…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>
…pp.js (R3a) (#890) 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>
Why now
Every slice carved out of app.js so far only ever read the state it shared (
loopA/loopB,_audioSeekGen,currentFilename), so a read-only getter hook was enough and no container was needed. Twice I checked, and twice I got away with it.That runs out at count-in: it genuinely writes
isPlaying(it starts and stops playback — 4 sites) andlastAudioTime(2 sites).So the state has to live on an object:
S.isPlaying = trueis a property write, which works from any module holding the sameS. Same shape the stems, studio, and editor migrations all converged on.Deliberately small. app.js has ~104 top-level
letscalars; lifting all of them is a ~977-site rewrite for no benefit, since most are private to one cluster and travel with it. Only what a carved module must write goes here. Add on demand.The rewrite is AST-driven, not textual — and that isn't fussiness
Of 100 textual occurrences of these two names, only 70 resolve to the module binding:
someObj.isPlaying,window.feedBack.isPlaying)function setPlayButtonState(isPlaying)function setPlayButtonState(S.isPlaying){ isPlaying }{ isPlaying: S.isPlaying }A find-and-replace corrupts all 29. The rewrite walks the AST, skips shadows / member properties / keys, and replaces identifier ranges.
One trap worth recording: acorn gives a shorthand property's key and value the same range, so rewriting both produced
isPlaying: S.isPlaying: S.isPlayinguntil I deduped by range.window.feedBack.isPlaying— the public mirror — is a different thing and is untouched. Two test sandboxes stub it; those were deliberately left alone.Verified with real playback
A/B against
origin/mainin two browsers, real song loaded:feedBack.isPlayinginitialtruetogglePlay()falsetogglePlay()trueseekBy(5)Harnesses
8 vm-sandbox suites slice playback code out of app.js and now see
S.isPlaying—juce_engine_reroute,loop_restart,play_button_reroute_guard,playback_app_adapter,song_restart,song_seek,speed_reset, plus the Python source-assert. Each gets the same container in its sandbox; every assertion is unchanged.pytest 2396 · node 1040/1040 · ESLint 0 · tailwind clean · Codex 0.
🤖 Generated with Claude Code
Summary by CodeRabbit