Skip to content

refactor(stems): extract the streaming path to src/streaming.js - #34

Merged
byrongamatos merged 2 commits into
mainfrom
feat/es-module-split-streaming
Jul 8, 2026
Merged

byrongamatos merged 2 commits into
mainfrom
feat/es-module-split-streaming

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 8, 2026 •

Copy link
Copy Markdown
Collaborator

What

Step 10 — the first real module lift of the entangled core. Moves the bounded-memory iOS WAV streaming path out of main.js into src/streaming.js (~440 lines):

  • reader/WAV helpers: trackRead, ensureBytes, dropBytes, readWavHeader, dequeueTrackFrames
  • the worklet-feed pump: appendRound, waitPos, runPump
  • seek/reader mgmt: cancelStreamReaders, openTrackStreams, repositionStream
  • setupStreaming, resetStreamState, streamOffsetBuffered, streamingSupported, isWavResponse

main.js 2281 → 1840 lines.

Breaking the cycle

The streaming layer would form a static import cycle: transport imports setupStreaming/repositionStream from here, while the pump resumes a deferred play via transportPlay, and setupStreaming needs persistedSongGain + the shared onWorkletMessage — all in main.js. Since ES cycles are banned by the R0 no-cycle gate, this is broken with a small injected seam wired once at boot:

configureStreaming({ startPendingPlay: transportPlay, songGain: persistedSongGain, onWorkletMessage });

Everything else the layer calls is already an extracted module (audio-ctx, mix, mix-gains, util, wav-pcm, prefs, state), imported by the same names — so the function bodies moved verbatim. Removed the now-dead main.js imports (parseWavHeader/pcm16ToFloat32, ensureCtxAtRate).

Tests

New tests/streaming.test.mjs — real-import coverage for the pure exports: isWavResponse (content-type sniff), streamingSupported (platform-API gate), and streamOffsetBuffered (the buffered-window boundary math). The pump/seek internals stay private, so the seek-token race guard (step 9.1) remains on-device-verified.

Full suite green (npm test, 45 pass); tests/test_manifest.py green. Codex preflight: 0 issues.

CHANGELOG: backfilled the missed step 5–9.1 entries + added step 10.

On-device smoke pending — this moved the actual streaming playback engine, so verify multi-stem playback + seek/scrub + pause/resume + play-through.

Summary by CodeRabbit

  • New Features
    • Introduced a bounded-memory iOS streaming playback path using an AudioWorklet-based stem mixer for smoother audio and more responsive seeking.
    • Added utilities to detect WAV streams and resume playback by repositioning buffered content when needed.
  • Bug Fixes
    • Reduced seek/race-condition issues with safer seek handling and more robust streaming reset/restart behavior.
  • Tests
    • Added unit tests for WAV detection, streaming support checks, and whether requested playback offsets fall within the buffered window.

Move the bounded-memory iOS WAV streaming layer out of main.js into its own
module (~440 lines): the reader/WAV helpers (trackRead/ensureBytes/dropBytes/
readWavHeader/dequeueTrackFrames), the worklet-feed pump (appendRound/waitPos/
runPump) + seek path (cancelStreamReaders/openTrackStreams/repositionStream),
setupStreaming, resetStreamState, and streamOffsetBuffered/streamingSupported/
isWavResponse. main.js 2281 -> 1840 lines.

The layer would form a static import cycle (transport imports setupStreaming/
repositionStream; the pump calls transportPlay, setupStreaming needs
persistedSongGain + the shared onWorkletMessage — all in main.js). Broken with an
injected seam: configureStreaming({ startPendingPlay, songGain, onWorkletMessage })
wired once at boot. Everything else the layer calls is already an extracted module
(audio-ctx, mix, mix-gains, util, wav-pcm, prefs, state), imported by the same
names, so the bodies moved verbatim. Removed now-dead main.js imports
(parseWavHeader/pcm16ToFloat32, ensureCtxAtRate).

New tests/streaming.test.mjs covers the pure exports (isWavResponse,
streamingSupported, streamOffsetBuffered window math). The pump/seek internals
stay private, so the seek-token race guard remains on-device-verified.
CHANGELOG: backfilled steps 5-9.1 (missed) + added step 10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 8, 2026 13:50
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

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: 98e8d275-a47f-4ca3-910b-66c3f60ae1d4

📥 Commits

Reviewing files that changed from the base of the PR and between f3854e5 and b9ab856.

📒 Files selected for processing (1)
  • tests/streaming.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/streaming.test.mjs

📝 Walkthrough

Walkthrough

Bounded-memory streaming playback logic is extracted from src/main.js into src/streaming.js, with main.js updated to delegate orchestration back into the new module. New unit tests cover the exported streaming helpers, and the changelog records the migration steps.

Changes

Streaming module extraction

Layer / File(s) Summary
Streaming module seams and utilities
src/streaming.js
Adds configureStreaming hook injection, streaming constants, WAV response detection, streaming support checks, and streamOffsetBuffered window logic.
Byte-stream reading and WAV decoding helpers
src/streaming.js
Adds incremental byte reading, WAV header parsing, PCM16-to-float conversion, and appendRound frame writing with seek-token guards.
Pump loop, reader lifecycle, and reposition
src/streaming.js
Implements backpressure-aware runPump, reader cancellation, Range-based stream reopening, and repositionStream seek handling.
setupStreaming and resetStreamState
src/streaming.js
Implements full stream/worklet/gain/transport setup and teardown/reset logic.
main.js wiring to streaming module
src/main.js
Updates imports, adds configureStreaming call, removes inline streamOffsetBuffered, and deletes the in-file streaming implementation block.
Streaming tests and changelog
tests/streaming.test.mjs, CHANGELOG.md
Adds tests for isWavResponse, streamingSupported, and streamOffsetBuffered, plus CHANGELOG entries documenting the ES-module migration steps.

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

Sequence Diagram(s)

sequenceDiagram
  participant main.js
  participant streaming.js
  participant AudioWorklet
  participant fetch

  main.js->>streaming.js: configureStreaming(hooks)
  main.js->>streaming.js: setupStreaming(stems, probeResp, fullUrl, gen)
  streaming.js->>fetch: open Range requests for stems
  fetch-->>streaming.js: WAV byte streams
  streaming.js->>AudioWorklet: load worklet, initialize gains
  streaming.js->>streaming.js: runPump append loop
  streaming.js->>AudioWorklet: append PCM frames
  AudioWorklet-->>streaming.js: pos message
  main.js->>streaming.js: transportPlay uses streamOffsetBuffered(offset)
  alt offset not buffered
    main.js->>streaming.js: repositionStream(targetSec)
    streaming.js->>fetch: reopen streams at target sample
    streaming.js->>AudioWorklet: seek message
  end
  main.js->>streaming.js: resetStreamState()
  streaming.js->>fetch: cancel readers
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: extracting the streaming path into src/streaming.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/es-module-split-streaming

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/streaming.test.mjs (1)

22-35: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the other two streamingSupported() failure paths.

This only proves the AudioWorkletNode branch. Add missing-ReadableStream and missing-fetch cases so the && contract is fully pinned down.

♻️ Suggested test expansion
 test('streamingSupported: true only when all three platform APIs exist', () => {
@@
         globalThis.ReadableStream = function () {};
         globalThis.fetch = function () {};
         globalThis.AudioWorkletNode = function () {};
         assert.equal(streamingSupported(), true);
-        delete globalThis.AudioWorkletNode;                  // no worklet → unsupported
+        delete globalThis.ReadableStream;                    // no stream → unsupported
+        assert.equal(streamingSupported(), false);
+        globalThis.ReadableStream = function () {};
+        delete globalThis.fetch;                             // no fetch → unsupported
+        assert.equal(streamingSupported(), false);
+        globalThis.fetch = function () {};
+        delete globalThis.AudioWorkletNode;                  // no worklet → unsupported
         assert.equal(streamingSupported(), false);
🤖 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 `@tests/streaming.test.mjs` around lines 22 - 35, Expand the
streamingSupported() test to cover the remaining false cases in addition to the
existing AudioWorkletNode branch. In tests/streaming.test.mjs, keep using the
same streamingSupported() helper and globalThis overrides, but add assertions
for when ReadableStream is missing and when fetch is missing so the full &&
condition is verified; preserve and restore the original globals in the same
test setup.
src/streaming.js (1)

219-238: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard resp.body before getReader() for consistency with setupStreaming.

setupStreaming explicitly guards if (!resp || !resp.body) return false; (Line 306), but openTrackStreams calls resp.body.getReader() (Line 231) unconditionally. A 204/no-body response on a Range refetch would throw a TypeError; it's caught by repositionStream's try/catch (Line 259) and reported as a generic seek failure, but a targeted guard yields cleaner handling and matches the sibling path.

♻️ Proposed guard
         const resp = await fetch(t.url, { signal: S.abortController.signal, headers });
         if (gen !== S.loadGeneration || token !== ST.streamSeekToken) {
             try { resp.body && resp.body.cancel(); } catch (_) {}
             return;
         }
+        if (!resp.body) { t.reader = null; t.leftover = EMPTY_BYTES; t.done = true; return; }
         t.reader = resp.body.getReader();
🤖 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 `@src/streaming.js` around lines 219 - 238, Guard the fetch response body in
openTrackStreams before calling getReader, matching the existing setupStreaming
check. In the openTrackStreams path, handle a missing resp or resp.body by
returning early instead of unconditionally assigning t.reader from
resp.body.getReader(), so 204/no-body Range responses don’t surface as a
TypeError and are handled consistently with setupStreaming.
🤖 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 `@src/streaming.js`:
- Around line 219-238: Guard the fetch response body in openTrackStreams before
calling getReader, matching the existing setupStreaming check. In the
openTrackStreams path, handle a missing resp or resp.body by returning early
instead of unconditionally assigning t.reader from resp.body.getReader(), so
204/no-body Range responses don’t surface as a TypeError and are handled
consistently with setupStreaming.

In `@tests/streaming.test.mjs`:
- Around line 22-35: Expand the streamingSupported() test to cover the remaining
false cases in addition to the existing AudioWorkletNode branch. In
tests/streaming.test.mjs, keep using the same streamingSupported() helper and
globalThis overrides, but add assertions for when ReadableStream is missing and
when fetch is missing so the full && condition is verified; preserve and restore
the original globals in the same test setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f08fc4c9-a717-487b-92bb-0cce8f51116d

📥 Commits

Reviewing files that changed from the base of the PR and between e789d23 and f3854e5.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/main.js
  • src/streaming.js
  • tests/streaming.test.mjs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the bounded-memory iOS WAV streaming playback path by extracting it from src/main.js into a dedicated src/streaming.js module, while preserving behavior and avoiding an ES-module import cycle via an injected seam (configureStreaming).

Changes:

  • Extracts the entire streaming implementation (reader/WAV helpers, worklet pump, seek/reposition, setup/teardown, and small pure helpers) into src/streaming.js.
  • Wires src/main.js to the new module (imports + one-time configureStreaming({ startPendingPlay, songGain, onWorkletMessage }) boot hook) and removes the now-dead local streaming code.
  • Adds tests/streaming.test.mjs to cover the pure-ish exports (streamingSupported, isWavResponse, streamOffsetBuffered) and updates CHANGELOG.md.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/streaming.test.mjs Adds unit coverage for the extracted module’s pure exports using real ESM imports.
src/streaming.js New module containing the extracted iOS WAV streaming path + injected hook seam to avoid import cycles.
src/main.js Removes inlined streaming implementation; imports/wires the extracted streaming module and its seam.
CHANGELOG.md Documents step 10 extraction (and backfills earlier step entries).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Address CodeRabbit nitpick on #34 — pin the full && contract (missing
ReadableStream / fetch / AudioWorkletNode), not just the worklet branch.

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

Copy link
Copy Markdown
Collaborator Author

Addressed the two CodeRabbit nitpicks:

  1. streamingSupported test — expanded to cover the missing-ReadableStream and missing-fetch paths too (b9ab856), so the full && contract is pinned.

  2. Guard resp.body in openTrackStreams — declining. This path moved verbatim (pre-existing), and the suggested per-track early-return would be less safe: it'd leave a partial track set (some tracks with no reader) that the pump then dereferences on the next appendRound. The current behavior is intentional fail-fast — a bodyless Range response throws in the Promise.all map, which repositionStream's catch turns into a clean whole-seek abort (logged seek refetch failed), keeping the track set consistent. A 204/no-body from the WAV PCM proxy is also not a real scenario. If we ever want explicit handling, it should abort the whole reopen uniformly (which already happens), not open tracks partially.

@byrongamatos
byrongamatos merged commit b634bd9 into main Jul 8, 2026
3 checks passed
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.

2 participants