mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 12:21:49 +00:00
static/js/library.js (1,988) + static/js/library-state.js (29) — bodies VERBATIM.
app.js 6,313 -> 4,451.
THE BIGGEST SLICE OF THE CARVE: 145 declarations, ~1,900 lines, 30% of what was left.
The grid, the artist tree, the A-Z rail, filters, pagination, selection, favourites, the
scan banner, and the library-provider plumbing.
A LOW module: it imports only leaves (./dom.js, ./format.js, ./library-state.js,
./tuning-display.js — all four import nothing themselves) and needs ZERO host hooks. It
calls nothing in app.js. That is not luck; it is why this cluster was picked. Two entry
points that WOULD have dragged the playback core in were left behind in app.js:
* syncLibrarySong reaches showScreen/playSong
* _handleLibArrowNav Enter on a selected row plays the song
Both are one hop from the library, and app.js is the root, so it imports from both sides
for free. Pulling them in swallows playSong, showScreen and the whole remaining core — I
measured it: the closure jumps from 145 declarations to 189.
library-state.js holds exactly FIVE fields. An imported binding is read-only, and of the
library's outward bindings only these five are genuinely WRITTEN from outside — by
showScreen, deleteSongFromModal and syncLibrarySong, none of which can move in. The other
23 are read-only from outside, so they stay plain exports (ES live bindings mean app.js
still sees every reassignment).
━━━ THE EXPORT LIST NEARLY SHIPPED A DEAD A-Z RAIL ━━━
59 exports — and 43 of them CANNOT be found by a call-graph scan. They are referenced only
from app.js's TOP-LEVEL statements: the Object.assign(window, {...}) contract and the
scattered window.X = X lines, which live outside every function, so a closure walk over
declarations never sees them. Among them are the four handler names app.js composes AT
RUNTIME into onclick="" strings — filterTreeLetter, filterFavTreeLetter, goTreePage,
goFavTreePage — the library A-Z rail and its pagination. No static tool can see those at
all. Had I trusted the call-graph, the rail would have died silently on click with nothing
failing in CI.
━━━ AND MY OWN SCANNER LIED ━━━
The cycle-risk pass reported "(none)" for this carve. It was wrong, and it could not have
been right: a dangling `else if` bound to an inner `if` instead of the outer chain, so its
`imported` map was ALWAYS empty and the check reported clean no matter what. A guard that
cannot fail is worse than no guard. Fixed, and it then found the real edges — dom.js,
format.js, tuning-display.js, library-state.js. All four are leaves, so the carve is
genuinely acyclic; I just now know it instead of assuming it.
(The AST rewriter had its own trap: `MAP[name]` with an object literal and name ===
'constructor' hits Object.prototype.constructor — truthy — and it happily rewrote
`constructor(id)` into `L.function Object() { [native code] }(id)`. Every identifier in
the file is looked up, so the lookup must not see the prototype chain. It is a Map now.)
TESTS. legacy_shim_hits SPLIT (loadLibraryProviders + setLibraryProvider -> the module;
syncLibrarySong stayed in app.js). v3_library_refresh now reads app.js AND the module,
rather than being re-pinned to whichever file happens to hold the emit this week.
VERIFIED. A/B against origin/main in two browsers: the whole window contract, cards render,
grid/tree/sort/filter/clear round-trip — and, specifically, the A-Z rail: 28 onclick
handlers composed at runtime, identical on both, and a real .click() on a letter works.
IDENTICAL on all 33 + 7 probes, no new page errors.
pytest 2396, node 1040/1040, host contract 2/2, ESLint 0 (no-cycle clean).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
1.4 KiB
JavaScript
30 lines
1.4 KiB
JavaScript
// Shared, MUTABLE library state.
|
|
//
|
|
// WHY A CONTAINER AND NOT PLAIN EXPORTS. An imported binding is READ-ONLY:
|
|
// `import { _treeStats }; _treeStats = x` throws. Of the library module's 28 outward
|
|
// bindings, 23 are only ever READ from outside, so they stay plain exports. These five
|
|
// are genuinely WRITTEN from outside — by showScreen (session teardown bumps the epoch,
|
|
// resets the page), deleteSongFromModal, and syncLibrarySong, none of which can move into
|
|
// the library module because they reach the playSong/showScreen core.
|
|
//
|
|
// So exactly these five move onto an object, and no more. `L.treeStats = x` is a property
|
|
// write, which works from any module holding the same `L`. Same shape as ./player-state.js.
|
|
//
|
|
// Add to it when a carve actually needs it, not before — a container is a shared mutable
|
|
// global with better manners, and every field on it is a coupling you have to keep true.
|
|
export const L = {
|
|
/** Library tree stats (artist -> counts), cached from /api/library/tree-stats. */
|
|
treeStats: null,
|
|
/** Same, for the favourites tree. */
|
|
favTreeStats: null,
|
|
/** Tuning names, cached from /api/library/tuning-names. */
|
|
tuningNames: null,
|
|
/**
|
|
* Session generation for the library. Bumped on teardown so an in-flight page fetch
|
|
* that resolves against a stale library can't render into the new one.
|
|
*/
|
|
libEpoch: 0,
|
|
/** Current grid page (0-based). */
|
|
currentPage: 0,
|
|
};
|