mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
refactor(app): carve the playback transport out of app.js — and RETIRE 8 host hooks (R3a) (#894)
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8d0e270345
commit
8bec8d2466
@@ -27,6 +27,7 @@
|
||||
// layer that catches it on the paths a smoke test never runs.
|
||||
import { audio } from './audio-el.js';
|
||||
import { esc } from './dom.js';
|
||||
import { _audioDuration, _audioTime, audioSeekGen } from './transport.js';
|
||||
import { host } from './host.js';
|
||||
|
||||
export function _sectionPracticeBarContains(el) {
|
||||
@@ -85,7 +86,7 @@ export function _setSectionPracticeMode(on, opts = {}) {
|
||||
if (opts.defaultWholeOn) {
|
||||
_sectionPracticeWholeSection = true;
|
||||
}
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
if (opts.defaultWholeOn) {
|
||||
_syncSectionPracticePieceUi();
|
||||
}
|
||||
@@ -104,7 +105,7 @@ export function _setSectionPracticeMode(on, opts = {}) {
|
||||
_sectionPracticeSelected = -1;
|
||||
_sectionPracticeWholeSection = false;
|
||||
_sectionPracticeSavedPartIndex = 0;
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
if (!opts.skipClearLoop && (host.loopA() !== null || host.loopB() !== null)) {
|
||||
host.clearLoop();
|
||||
}
|
||||
@@ -129,7 +130,7 @@ function _sectionPracticeHighway() {
|
||||
}
|
||||
|
||||
function _sectionPracticeDuration() {
|
||||
const d = host._audioDuration();
|
||||
const d = _audioDuration();
|
||||
if (d && Number.isFinite(d) && d > 0) return d;
|
||||
const cd = window.feedBack?.currentSong?.duration;
|
||||
return (cd && Number.isFinite(cd) && cd > 0) ? cd : 0;
|
||||
@@ -914,7 +915,7 @@ export function renderSectionPracticeBar() {
|
||||
// matching one; run it before the piece UI so that reflects the result.
|
||||
_syncSectionPracticeFromLoop();
|
||||
_syncSectionPracticePieceUi();
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
}
|
||||
|
||||
export async function onSectionParentClick(parentIdx) {
|
||||
@@ -927,7 +928,7 @@ export async function onSectionParentClick(parentIdx) {
|
||||
_sectionPracticeSavedPartIndex = 0;
|
||||
_sectionPracticeWholeSection = true;
|
||||
_syncSectionPracticePieceUi();
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
if (_sectionPracticeActiveParentRange() || _sectionPracticeRanges.length) {
|
||||
await practiceSection(0, { whole: true });
|
||||
}
|
||||
@@ -1019,7 +1020,7 @@ function _blurSectionPracticeFocusIfNeeded() {
|
||||
|
||||
export async function practiceSection(index, opts = {}) {
|
||||
const requestGen = ++_sectionPracticeRequestGen;
|
||||
const seekGen = host._audioSeekGen();
|
||||
const seekGen = audioSeekGen();
|
||||
const loopGen = host._loopMutationGen();
|
||||
const whole = !!opts.whole;
|
||||
const r = _sectionPracticeResolveLoopTarget(index, opts);
|
||||
@@ -1045,7 +1046,7 @@ export async function practiceSection(index, opts = {}) {
|
||||
let ok = false;
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
// A newer click or a song/arrangement change supersedes this retry.
|
||||
if (requestGen !== _sectionPracticeRequestGen || seekGen !== host._audioSeekGen() || loopGen !== host._loopMutationGen()) return;
|
||||
if (requestGen !== _sectionPracticeRequestGen || seekGen !== audioSeekGen() || loopGen !== host._loopMutationGen()) return;
|
||||
try {
|
||||
// skipSectionSync: this function owns the section-practice state and
|
||||
// applies it below under the request-gen guard, so a stale retry
|
||||
@@ -1055,7 +1056,7 @@ export async function practiceSection(index, opts = {}) {
|
||||
// after its internal seek await, so a stale loop is never armed.
|
||||
ok = await host.setLoop(start, end, {
|
||||
skipSectionSync: true,
|
||||
commitGuard: () => requestGen === _sectionPracticeRequestGen && seekGen === host._audioSeekGen() && loopGen === host._loopMutationGen(),
|
||||
commitGuard: () => requestGen === _sectionPracticeRequestGen && seekGen === audioSeekGen() && loopGen === host._loopMutationGen(),
|
||||
});
|
||||
} catch (err) {
|
||||
ok = false;
|
||||
@@ -1064,7 +1065,7 @@ export async function practiceSection(index, opts = {}) {
|
||||
await new Promise(res => setTimeout(res, 60 + attempt * 90));
|
||||
}
|
||||
// Re-check after the awaited retries before applying any loop/count-in state.
|
||||
if (requestGen !== _sectionPracticeRequestGen || seekGen !== host._audioSeekGen() || loopGen !== host._loopMutationGen()) return;
|
||||
if (requestGen !== _sectionPracticeRequestGen || seekGen !== audioSeekGen() || loopGen !== host._loopMutationGen()) return;
|
||||
|
||||
if (ok) {
|
||||
_sectionPracticeWholeSection = whole;
|
||||
@@ -1073,7 +1074,7 @@ export async function practiceSection(index, opts = {}) {
|
||||
_sectionPracticeSavedPartIndex = index;
|
||||
}
|
||||
_blurSectionPracticeFocusIfNeeded();
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
host.startCountIn({ immediate: true });
|
||||
} else {
|
||||
_setSectionPracticeMode(false, { skipClearLoop: true });
|
||||
@@ -1120,7 +1121,7 @@ export function _syncSectionPracticeFromLoop() {
|
||||
} else if (_sectionPracticeMode) {
|
||||
_setSectionPracticeMode(false, { skipClearLoop: true });
|
||||
}
|
||||
_updateSectionPracticeHighlight(host._audioTime());
|
||||
_updateSectionPracticeHighlight(_audioTime());
|
||||
}
|
||||
|
||||
function _sectionPracticeIndexAtTime(t) {
|
||||
|
||||
Reference in New Issue
Block a user