Skip to content

refactor(app): carve the playback transport out of app.js — and retire 8 host hooks (R3a)#894

Merged
byrongamatos merged 1 commit into
mainfrom
feat/r3-carve-transport
Jul 11, 2026
Merged

refactor(app): carve the playback transport out of app.js — and retire 8 host hooks (R3a)#894
byrongamatos merged 1 commit into
mainfrom
feat/r3-carve-transport

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #893. static/js/transport.js (377) — bodies verbatim. app.js 6,643 → 6,316.

This is the first carve that subtracts hooks instead of adding them

Every carve before this one added host hooks: a module pulled out of app.js still had to call back into it. But four modules were all reaching through the seam for the same handful of names_audioSeek, _audioTime, setPlayButtonState, _songEventPayload, jucePlayer. Those names have an owner, and it isn't app.js.

Give them one, and the consumers import them directly:

module hooks before after
count-in.js 5 0 host import deleted
juce-audio.js 4 0 host import deleted
loops.js 6 4
section-practice.js 10 7
configureHost() 20 12

A hook is a cycle you agreed to live with. An import is a dependency you actually have. Prefer the import whenever the name has a real owner.

_audioSeekGen now stays private. It has exactly one writer — _resetAudioSeekState(), which moved with it — so readers get audioSeekGen() and nobody outside can desync it. Strictly better than the hook it replaces, which handed out a getter and left the writer behind in app.js.

The dependency scan had a hole, and it bit

Picking the carve by dependency closure over app.js's own top-level decls said this cluster was downward-closed. It wasn't. _currentPlaybackSnapshot reads loopA/loopB — which live in ./js/loops.js, and loops.js imports transport.

The scan saw nothing, because loopA stopped being an app.js decl the moment loops.js was carved out. Any dependency scan of a partly-carved monolith has to resolve the imports too, or it will confidently hand you a cycle.

I added that pass. It found exactly one back-edge. So _currentPlaybackSnapshot stays in app.js — as does restartCurrentSong, which calls _cancelCountIn(). app.js is the root; it imports both sides for free.

Tests

Four harnesses retargeted: play_button_reroute_guard, song_event_payload, song_seektransport.js; playback_app_adapter is split, since _installPlaybackTransportAdapter stayed behind.

The two census tests — "≥8 song:* emit sites" and "every seek callsite passes a reason" — now scan app.js and every static/js/*.js, not one file. Pointed at a single file, their count silently shrinks as code leaves, which reads as "someone deleted an emit" — or, worse, passes while genuinely missing sites. Both bite-tested: stripping a _songEventPayload() from an emit, and adding a reason-less _audioSeek(), each fail the suite.

Verification

A/B against origin/main — real song, real playback, identical on all 21 probes, zero page errors:

result
song:play payload keys exactly audioT, chartT, perfNow, time
song:seek reason: "seek-by", finite from/to
song:* events fired all five (play, pause, seek, position-changed, ready)
seekBy(5) advances the clock ~5s
restartCurrentSong() returns to zero
play button aria-pressed tracks state

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

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a dedicated playback transport supporting play, pause, seeking, playback rates, timing, and JUCE audio integration.
    • Improved seek handling with cancellation safeguards and consistent playback events.
    • Preserved play/pause button state and accessibility updates during playback transitions.
  • Bug Fixes

    • Improved synchronization for loops, section practice, count-ins, and audio mode switching.
  • Tests

    • Updated playback and seek regression coverage for the new transport behavior.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d5ba609-03ad-4554-aed5-d29b6d4f48cf

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0e270 and b1a2d60.

📒 Files selected for processing (10)
  • static/app.js
  • static/js/count-in.js
  • static/js/juce-audio.js
  • static/js/loops.js
  • static/js/section-practice.js
  • static/js/transport.js
  • tests/js/play_button_reroute_guard.test.js
  • tests/js/playback_app_adapter.test.js
  • tests/js/song_event_payload.test.js
  • tests/js/song_seek.test.js

📝 Walkthrough

Walkthrough

Playback and seek responsibilities moved from app.js into transport.js. Count-in, loop, section-practice, JUCE audio shims, host wiring, and source-based tests were updated to use the extracted transport APIs and seek-generation accessor.

Changes

Playback and audio module refactor

Layer / File(s) Summary
Transport implementation
static/js/transport.js
Adds shared JUCE/HTML5 playback controls, timing and event helpers, serialized seeking, generation guards, and play/pause operations.
Transport consumer migration
static/js/count-in.js, static/js/loops.js, static/js/section-practice.js
Replaces host-mediated transport calls with direct imports for playback, timing, payload, seek, and generation operations.
JUCE audio integration
static/js/juce-audio.js
Routes JUCE switching and audio-element shim behavior through the transport module.
Application transport seam
static/app.js
Removes duplicated transport implementations, imports extracted bindings, updates generation checks, and adjusts host wiring.
Extracted transport validation
tests/js/*
Updates source extraction tests and scans all frontend modules for playback and seek event call sites.

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

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Transport
  participant JUCE
  participant feedBack
  App->>Transport: togglePlay or _audioSeek
  Transport->>JUCE: play, pause, or seek
  Transport->>feedBack: emit playback or seek event
  Transport-->>App: return transport state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: extracting playback transport from app.js and removing host hooks.
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-carve-transport

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

…E 8 host hooks (R3a)

static/js/transport.js (377) — bodies VERBATIM. app.js 6,643 → 6,316.

THIS IS THE FIRST CARVE THAT SUBTRACTS HOOKS INSTEAD OF ADDING THEM.

Every carve before this one added host hooks: a module pulled out of app.js still had
to call back into it. But four modules were all reaching through the seam for the SAME
handful of names — _audioSeek, _audioTime, setPlayButtonState, _songEventPayload,
jucePlayer. Those names have an owner, and it isn't app.js. Give them one, and the
consumers import them directly:

    count-in.js           5 hooks -> 0     (host import deleted)
    juce-audio.js         4 hooks -> 0     (host import deleted)
    loops.js              6 hooks -> 4
    section-practice.js  10 hooks -> 7
    ----------------------------------------------------------
    configureHost()      20 hooks -> 12

A hook is a cycle you agreed to live with. An import is a dependency you actually have.
Prefer the import whenever the name has a real owner.

_audioSeekGen now stays PRIVATE. It has exactly one writer — _resetAudioSeekState(),
which moved with it — so readers get audioSeekGen() and nobody outside can desync it.
Strictly better than the hook it replaces, which handed out a getter and left the writer
behind in app.js.

THE SCAN HAD A HOLE, AND IT BIT. Picking the carve by dependency closure over app.js's
own top-level decls said this cluster was downward-closed. It wasn't:
_currentPlaybackSnapshot reads loopA/loopB — which live in ./js/loops.js, and loops.js
imports transport. The scan saw nothing, because loopA STOPPED BEING an app.js decl the
moment loops.js was carved out. Any dependency scan of a partly-carved monolith has to
resolve the imports too, or it will confidently hand you a cycle. Added that pass; it
found exactly one back-edge, and _currentPlaybackSnapshot stays in app.js (as does
restartCurrentSong, which calls _cancelCountIn). app.js is the root — it imports both
sides for free.

TESTS. Four harnesses retargeted (play_button_reroute_guard, song_event_payload,
song_seek -> transport.js; playback_app_adapter SPLIT, since
_installPlaybackTransportAdapter stayed behind).

The two CENSUS tests — "≥8 song:* emit sites", "every seek callsite passes a reason" —
now scan app.js AND every static/js/*.js, not one file. Pointed at a single file, their
count silently shrinks as code leaves, which reads as "someone deleted an emit" or, worse,
passes while genuinely missing sites. Both bite-tested: stripping a _songEventPayload()
from an emit and adding a reason-less _audioSeek() each fail the suite.

VERIFIED. A/B against origin/main, real song, real playback: song:play payload is exactly
{audioT, chartT, perfNow, time}; song:seek carries reason "seek-by" with finite from/to;
all five song:* events fire; seekBy advances the clock; restartCurrentSong returns to zero;
the play button's aria-pressed tracks state. IDENTICAL on all 21 probes, zero page errors.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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