Skip to content

refactor(highway): make the highway global explicit before the module flip (R3c)#912

Merged
byrongamatos merged 1 commit into
mainfrom
r3c/highway
Jul 12, 2026
Merged

refactor(highway): make the highway global explicit before the module flip (R3c)#912
byrongamatos merged 1 commit into
mainfrom
r3c/highway

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

73 bare highway.x references → window.highway.x, across app.js and 10 other files. Provably a no-op today. It is the precondition for flipping highway.js to a module.

Why this has to land first

highway.js is a classic script. Its top-level const highway = createHighway() therefore creates a global lexical binding — visible as a bare name to every other classic script and to every ES module. 73 call sites quietly rely on that.

The moment highway.js becomes a module, that binding is gone. const in a module is module-scoped, not global. Every one of those 73 sites becomes a ReferenceError, and the flip is impossible until they say what they mean.

window.highway = highway is already set, to the same object, on the same line — so this is an identity rewrite. Verified in the browser below.

The rewrite bit me three times. Regex is not enough for this.

1. A shadowed local. capabilities/note-detection.js does const highway = window.highway. Its 9 bare uses are local and already correct; a blind rewrite would have emitted const window.highway = window.highway. Excluded.

2. Half-converted guards — the dangerous one. Six sites read:

typeof highway !== 'undefined' && highway && typeof highway.setTime === 'function'

The regex converted the consequent and left the test — which is worse than not touching them. After the flip, typeof highway is 'undefined', so each guard is permanently false and the code behind it silently never runs.

transport.js's was the seek → setTime sync: the chart clock would have quietly desynced after every seek, with nothing failing. All six now test window.highway.

3. Two more bare references — found by Codex [P2] and confirmed by an AST scan. app.js:3114 and :3176 use highway && typeof window.highway.getSections === 'function'. My grep searched for typeof highway, not highway &&. After the flip these throw, the catch swallows it, and the editor silently falls back to a ±4s edit window and arrangement 0.

Regex missed a shadow, a half-conversion, and two bare reads. The final check is an AST pass that resolves scopes and reports every highway identifier not bound locally. It now reports zero.

Verification

A/B against origin/main in two browsers — 15 probes, identical, zero page errors:

window.highway is the bare global (same object)
the whole API surface resolves (getTime, setTime, getBPM, getSections, setAvOffset, getSongInfo, getPerf)
a real song plays; the chart clock advances
getPerf().drawMs > 0
seek syncs chart — the exact guard I nearly broke in (2)

node 1045 · pytest 2416 · ESLint 0 · Codex 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved player lifecycle handling when leaving and restarting playback.
    • Preserved resume-session snapshots more reliably during player transitions.
    • Improved synchronization for playback time, looping, lyrics visibility, mastery controls, and section practice.
    • Prevented errors when certain playback services are unavailable.
  • Tests
    • Updated playback tests to reflect improved service initialization and synchronization behavior.

…le flip (R3c)

73 bare `highway.x` references -> `window.highway.x`, across app.js and 10 other files.
Provably a NO-OP today. It is the precondition for flipping highway.js to a module.

━━━ WHY THIS HAS TO LAND FIRST ━━━

highway.js is a CLASSIC script. Its top-level `const highway = createHighway()` therefore
creates a GLOBAL LEXICAL BINDING — visible as a bare name to every other classic script AND
to every ES module. 73 call sites quietly rely on that.

The moment highway.js becomes a module, that binding is gone. `const` in a module is
module-scoped, not global. Every one of those 73 sites becomes a ReferenceError, and the
flip is impossible until they say what they mean.

`window.highway = highway` is already set, to the same object, on the same line. So this is
an identity rewrite — verified in the browser below.

━━━ THE REWRITE BIT ME THREE TIMES. REGEX IS NOT ENOUGH FOR THIS. ━━━

1. A SHADOWED LOCAL. capabilities/note-detection.js does `const highway = window.highway`.
   Its 9 bare uses are LOCAL and already correct; a blind rewrite would have emitted
   `const window.highway = window.highway`. Excluded.

2. HALF-CONVERTED GUARDS — the dangerous one. Six sites read
   `typeof highway !== 'undefined' && highway && typeof highway.setTime === 'function'`.
   The regex converted the CONSEQUENT and left the TEST, which is WORSE than not touching
   them: after the flip `typeof highway` is 'undefined', so each guard is PERMANENTLY FALSE
   and the code behind it silently never runs. transport.js's was the seek->setTime sync:
   the chart clock would have quietly desynced after every seek, with nothing failing.
   All six now test window.highway.

3. TWO MORE BARE REFERENCES, found by Codex [P2] and confirmed by an AST scan: app.js:3114
   and :3176 use `highway && typeof window.highway.getSections === 'function'`. My grep
   searched for `typeof highway`, not `highway &&`. After the flip these throw, the catch
   swallows it, and the editor silently falls back to a ±4s edit window and arrangement 0.

Regex missed a shadow, a half-conversion, and two bare reads. The final check is an
AST pass that resolves scopes and reports every `highway` identifier not bound locally.
It now reports ZERO.

VERIFIED. A/B against origin/main in two browsers, 15 probes, IDENTICAL, zero page errors:
window.highway is the same object as the bare global, the whole API surface resolves, a real
song plays, the chart clock advances, getPerf().drawMs > 0 — and `seek syncs chart` passes,
which is the exact guard I nearly broke in (2).

node 1045, pytest 2416, ESLint 0, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change standardizes highway access through window.highway across playback lifecycle, transport, count-in, rendering, controls, and section-practice code. Resume snapshots now occur before highway teardown, and affected VM tests expose the same global seam.

Changes

Window highway access migration

Layer / File(s) Summary
Application playback lifecycle
static/app.js, static/js/juce-audio.js
Playback startup, teardown, arrangement changes, editor lookups, snapshots, and timing updates use window.highway; related routing comments are aligned.
Transport, count-in, and controls
static/js/count-in.js, static/js/player-controls.js, static/js/resume-session.js, static/js/section-practice.js, static/js/transport.js, static/js/highway-colors.js
Timing, seeking, mastery, resume-session, and section-practice paths resolve highway APIs through window.highway.
Visualization and player controls
static/js/viz.js, static/v3/player-chrome.js, static/v3/venue-scene-3d.js
Renderer selection, notation hints, lyrics synchronization, and related comments use the window-scoped highway seam.
Test sandbox global setup
tests/js/loop_restart.test.js, tests/js/song_event_payload.test.js, tests/js/speed_reset.test.js
VM sandboxes mirror highway onto window.highway for existing playback and transport assertions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlayerScreen
  participant resume-session
  participant window.highway
  PlayerScreen->>resume-session: snapshot resume position
  resume-session->>window.highway: read song info and chart time
  PlayerScreen->>window.highway: stop playback and transport
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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 summarizes the main change: replacing bare highway references with explicit window.highway ahead of the module conversion.
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 r3c/highway

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

@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 (3)
static/app.js (1)

1774-1774: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment references a nonexistent "window.highway.js" file.

The module is highway.js; it only exposes a window.highway global. Wording it as "(window.highway.js)" reads like a filename and may confuse future readers, especially once this file becomes an ES module.

✏️ Suggested wording
-// window._juceMode is otherwise decided once, at song-load time (window.highway.js),
+// window._juceMode is otherwise decided once, at song-load time (highway.js, via window.highway),
-    // Stale until the incoming song's WS handler (window.highway.js) sets it again.
+    // Stale until the incoming song's WS handler (highway.js, via window.highway) sets it again.

Also applies to: 2621-2621

🤖 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/app.js` at line 1774, Update the comments around the _juceMode
references in static/app.js to identify the module as highway.js and its exposed
window.highway global, removing the nonexistent “window.highway.js” filename
wording. Apply the same clarification at both referenced locations without
changing the surrounding logic.
static/js/juce-audio.js (1)

114-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Same "window.highway.js" phrasing nit as in app.js.

These comments say "window.highway.js" as if it were a filename; the actual module is highway.js, exposed globally as window.highway. Purely cosmetic, but worth aligning with the actual filename to avoid confusion once the module conversion lands.

✏️ Suggested wording
-    // window.highway.js's initial song-load routing consults this for the same
+    // highway.js's initial song-load routing (via window.highway) consults this for the same
-        // Don't race window.highway.js's own initial song-load routing: it owns
+        // Don't race highway.js's own initial song-load routing (via window.highway): it owns

Also applies to: 386-386

🤖 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/juce-audio.js` at line 114, Update the comments near the initial
song-load routing references in juce-audio.js to call the module “highway.js”
and its global exposure “window.highway,” removing the incorrect
“window.highway.js” filename phrasing. Apply the same wording correction to both
occurrences.
static/js/viz.js (1)

359-359: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent guarding of window.highway access.

_dropStaleNotationHint, _maybeShowNotationViewHint, and _autoMatchViz guard getSongInfo() with optional chaining (window.highway?.getSongInfo()), but _installVizRenderer (Line 359) and _autoMatchViz's no-match fallback (Line 723) call window.highway.setRenderer(...) unguarded. This mirrors the pre-existing pattern (same asymmetry existed with bare highway.x before this migration), so it's not a regression from this PR, but worth tightening for consistency now that the seam is explicit.

Also applies to: 582-583, 596-597, 644-645, 717-727

🤖 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/viz.js` at line 359, Make all window.highway accesses consistently
guarded in _installVizRenderer and _autoMatchViz, including both setRenderer
calls and the no-match fallback. Use optional chaining so these operations are
skipped when window.highway is unavailable, while preserving existing behavior
when it exists.
🤖 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/app.js`:
- Line 1774: Update the comments around the _juceMode references in
static/app.js to identify the module as highway.js and its exposed
window.highway global, removing the nonexistent “window.highway.js” filename
wording. Apply the same clarification at both referenced locations without
changing the surrounding logic.

In `@static/js/juce-audio.js`:
- Line 114: Update the comments near the initial song-load routing references in
juce-audio.js to call the module “highway.js” and its global exposure
“window.highway,” removing the incorrect “window.highway.js” filename phrasing.
Apply the same wording correction to both occurrences.

In `@static/js/viz.js`:
- Line 359: Make all window.highway accesses consistently guarded in
_installVizRenderer and _autoMatchViz, including both setRenderer calls and the
no-match fallback. Use optional chaining so these operations are skipped when
window.highway is unavailable, while preserving existing behavior when it
exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac2a3d9a-d335-467b-b5da-56cef5f2695a

📥 Commits

Reviewing files that changed from the base of the PR and between 23ecddc and eba258e.

📒 Files selected for processing (14)
  • static/app.js
  • static/js/count-in.js
  • static/js/highway-colors.js
  • static/js/juce-audio.js
  • static/js/player-controls.js
  • static/js/resume-session.js
  • static/js/section-practice.js
  • static/js/transport.js
  • static/js/viz.js
  • static/v3/player-chrome.js
  • static/v3/venue-scene-3d.js
  • tests/js/loop_restart.test.js
  • tests/js/song_event_payload.test.js
  • tests/js/speed_reset.test.js

@byrongamatos
byrongamatos merged commit d9fa6d3 into main Jul 12, 2026
5 checks passed
@byrongamatos
byrongamatos deleted the r3c/highway branch July 12, 2026 09:56
@byrongamatos

Copy link
Copy Markdown
Contributor Author

Correction: this PR's stated premise was wrong

Posting this against the merged PR so the commit message doesn't mislead whoever reads it next.

I claimed:

The moment highway.js becomes a module, that binding is gone. […] Every one of those 73 sites becomes a ReferenceError.

That is false. I have now actually flipped highway.js to a module and measured it:

main (classic script)        flipped (module)
  bare `highway` resolves       bare `highway` resolves
  window.hasOwnProperty: true   window.hasOwnProperty: true

highway.js already did window.highway = highway — which puts the name on the global object, and bare-identifier resolution falls back to the global object regardless of whether a lexical global binding exists. So the 73 sites would have kept working. The flip does not break them.

What that means for this PR

The rewrite is still defensible as hygiene — window.highway.x says what it means, and it stops the code depending on an implicit global-object fallback. But it was not a precondition for the flip, and it did not fix a latent bug.

Worse: the "six half-converted guards" and "two bare reads" I reported as finds were bugs my own regex introduced and then repaired. They were not pre-existing. Net, this PR is churn plus a small hygiene gain, and I over-sold it.

What the real hazard turned out to be

Not window.highwaywindow.createHighway.

A classic script's top-level function createHighway() implicitly becomes window.createHighway. A module's does not. The constitution names window.createHighway as public extension contract, and nothing in-tree calls it — the only consumers are third-party plugins, which I cannot grep.

Verified by removing the explicit assignment from the flip:

flip WITHOUT an explicit assignment:  window.createHighway === undefined   <-- contract silently gone
flip WITH it:                         window.createHighway === function

That is the thing the flip had to preserve, and it is handled in the flip PR. It is also precisely the class of breakage this PR's premise claimed to be preventing — I just had the wrong name.

byrongamatos added a commit that referenced this pull request Jul 12, 2026
Two lines. index.html: defer -> type="module". highway.js: one explicit assignment.
highway.js can now `import`, which is the whole point — the carve can begin.

━━━ THE ONE THING THE FLIP ACTUALLY BREAKS: window.createHighway ━━━

A top-level `function createHighway()` in a CLASSIC script IMPLICITLY becomes
window.createHighway. In a module it does not — module declarations are module-scoped, and
the name vanishes from the global object the instant the tag grows type="module".

The constitution names window.createHighway as PUBLIC EXTENSION CONTRACT (alongside
window.playSong / showScreen / feedBack). NOTHING IN-TREE CALLS IT. That is exactly why this
would have shipped: the only consumers are third-party plugins rendering their own highway
panel, and I cannot grep those. Green CI, green tests, and a broken plugin API.

Verified by removing the assignment and reloading:

    flip WITHOUT an explicit assignment:  window.createHighway === undefined   <-- gone
    flip WITH it:                         window.createHighway === function

So it is assigned explicitly now — same object, same behaviour, no longer an accident of how
the file happens to be loaded.

━━━ AND A CORRECTION TO #912 ━━━

#912 (merged) rewrote 73 bare `highway.x` -> `window.highway.x` on the stated grounds that
the flip would turn every one of them into a ReferenceError. HAVING NOW ACTUALLY FLIPPED IT,
THAT WAS WRONG. highway.js already did `window.highway = highway`, which puts the name on the
GLOBAL OBJECT — and bare-identifier resolution falls back to the global object whether or not
a lexical global binding exists. Measured on both builds: bare `highway` resolves either way.

#912 is defensible as hygiene and it does not hurt, but it was not a precondition and it fixed
no latent bug. A correction is posted on the PR so its commit message does not mislead. The
real hazard was the factory, not the instance — same class of breakage, wrong name.

ORDERING is unchanged: classic-defer and non-async type="module" share ONE post-parse
execution queue, in document order, so highway.js keeps its position at index.html:1244.

VERIFIED. A/B against origin/main: 15 probes IDENTICAL, zero page errors — window.highway,
window.createHighway, the full API surface, a real song playing, the chart clock advancing,
and the seek->setTime sync. THE PERF GATE PASSES at 1.85ms against its 12ms budget (module
evaluation costs nothing at render time), which is exactly what #910 was built to tell me.

node 1045, pytest 2416, ESLint 0, 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