Files
2026-05-27 23:29:53 +01:00

7.7 KiB
Raw Permalink Blame History

Design — audio-shipped-file-size-cleanup

This change is a pure mechanical refactor across three packages. The design is the split strategy per file: every public symbol stays exported from its current module path; the splits happen below the public surface.


1. @suaveplan/audio-timeline — engine/engine.ts (548 → ≤300)

TimelineEngine is the single class in this file and currently owns:

  • Construction + adapter wiring (setAdapter, dispose)
  • Track CRUD (addTrack, removeTrack, updateTrack, setTracks)
  • Clip CRUD (addClip, removeClip, updateClip, splitClip, joinClip)
  • Mixer mutations (setMasterVolume, setTrackVolume, setTrackMute, setTrackSolo, setTrackPan)
  • Transport (play, pause, stop, seek, setLoop, setTempo)
  • Snapshot / restore + undo/redo dispatch
  • Event emission

Proposed split

Keep TimelineEngine in engine.ts as the public class, but extract helper modules called from its methods:

src/engine/
  engine.ts                        ← class shell + delegating methods (≤300)
  engine-tracks.ts                 ← addTrack/removeTrack/updateTrack/setTracks helpers
  engine-tracks.md
  engine-clips.ts                  ← addClip/removeClip/updateClip/splitClip/joinClip helpers
  engine-clips.md
  engine-mixer.ts                  ← setMasterVolume/setTrackVolume/Mute/Solo/Pan helpers
  engine-mixer.md
  engine-transport.ts              ← play/pause/stop/seek/setLoop/setTempo helpers
  engine-transport.md
  engine.test.ts                   ← class-level invariants only (≤500)
  engine-tracks.test.ts            ← extracted track tests
  engine-clips.test.ts             ← extracted clip tests
  engine-mixer.test.ts             ← extracted mixer tests
  engine-transport.test.ts         ← extracted transport tests

Each helper module exports pure functions of (state, args) => newState (where state is TimelineEngine's internal record) plus any side-effect-free derived values. TimelineEngine methods become 1–3 liners that call the helper and forward the result. This pattern matches how audio-core's process/ directory is structured.

The original engine.test.ts splits along the same four axes; the residual engine.test.ts keeps construction, adapter wiring, snapshot/restore, undo/redo dispatch, and event emission tests — the cross-cutting concerns no single helper owns.


2. @suaveplan/audio-transport — transport/transport.ts (419 → ≤300)

Transport is the single class and owns:

  • Construction + clock injection
  • BPM + time-signature + PPQ state
  • Tick advance + lookahead scheduling loop
  • Schedule / cancel API
  • Loop region + swing
  • Tempo automation

Proposed split

src/transport/
  transport.ts                     ← class shell (≤300)
  transport-schedule.ts            ← schedule/cancel/lookahead helpers
  transport-schedule.md
  transport-tempo.ts               ← BPM + tempo automation + tick conversion helpers
  transport-tempo.md
  transport.test.ts                ← stays as-is (already under cap)

No test splitting needed — transport.test.ts is already within the 500-line cap.


3. @suaveplan/audio-metadata — parsers/id3v2/id3v2.ts (536 → ≤300)

id3v2.ts is the most over-budget src file. It owns:

  • Magic-byte detection
  • Header + extended-header parsing
  • Frame iteration
  • Per-frame-type decoders (TXXX, T..., COMM, USLT, APIC, UFID, WCOM, WCOP, …)
  • Text-encoding decoders (ISO-8859-1, UTF-16, UTF-16BE, UTF-8)
  • Unsynchronisation reversal

Proposed split

src/parsers/id3v2/
  id3v2.ts                         ← public parseId3v2() + frame dispatch (≤300)
  id3v2-header.ts                  ← header + extended-header parsing
  id3v2-header.md
  id3v2-frames-text.ts             ← T... / TXXX text frame decoders
  id3v2-frames-text.md
  id3v2-frames-binary.ts           ← APIC / UFID / GEOB binary frame decoders
  id3v2-frames-binary.md
  id3v2-frames-url.ts              ← W... URL frame decoders
  id3v2-frames-url.md
  id3v2-text-encoding.ts           ← ISO-8859-1 / UTF-16 / UTF-16BE / UTF-8 decoders
  id3v2-text-encoding.md
  id3v2-unsync.ts                  ← unsynchronisation reversal
  id3v2-unsync.md
  id3v2.test.ts                    ← integration parses on real fixture files (≤500)
  id3v2-header.test.ts             ← header-specific
  id3v2-frames-text.test.ts        ← text-frame-specific
  id3v2-frames-binary.test.ts      ← binary-frame-specific
  id3v2-frames-url.test.ts         ← url-frame-specific
  id3v2-text-encoding.test.ts      ← encoding-specific
  id3v2-unsync.test.ts             ← unsync-specific

The split mirrors the spec sections of the ID3v2.4 standard, so the file boundaries match how future maintainers will reason about the format.

id3v2.test.ts (1340 → 7 files of ≤500 each)

The current id3v2.test.ts mixes header tests, every frame type, encoding round-trips, and integration parses. Splitting by frame family (per the source split above) gives ~150–200 lines per file. The residual id3v2.test.ts keeps the end-to-end "parse a real fixture file" cases.


4. @suaveplan/audio-metadata — parsers/mp4-atom/mp4-atom.ts (351 → ≤300)

Lightest split. The file owns atom-walking + per-atom decoders (ftyp, moov, mvhd, mdat, ilst and the iTunes metadata sub-atoms). Extract the iTunes-specific decoders:

src/parsers/mp4-atom/
  mp4-atom.ts                      ← public parseMp4Atom() + container walker (≤300)
  mp4-atom-itunes.ts               ← iTunes/ilst metadata decoders
  mp4-atom-itunes.md
  mp4-atom.test.ts                 ← container + non-iTunes tests (≤500)
  mp4-atom-itunes.test.ts          ← iTunes-specific

5. @suaveplan/audio-metadata — parsers/ogg-vorbis/ogg-vorbis.test.ts (803 → ≤500)

Source file is already within cap. Only the test file needs splitting:

src/parsers/ogg-vorbis/
  ogg-vorbis.ts                    ← unchanged
  ogg-vorbis.test.ts               ← page parsing + comment header (≤500)
  ogg-vorbis-frames.test.ts        ← extracted frame-level tests

6. Cross-cutting rules for the split

  • Public surface is invariant. Every existing export from <pkg>/src/index.ts must continue to resolve to the same symbol with the same signature. The split is internal.
  • Subpath exports unchanged. The package.json exports map for each package keeps every existing entry. New internal helper modules are NOT added to exports — they are implementation details.
  • Barrels use explicit named re-exports (§6a). No export * from in new feature barrels.
  • Sibling .md files added for every new .ts per §4 (200-word minimum, six required sections).
  • Coverage stays 100/100/100/100 per vitest.config.ts thresholds.
  • No new behavior. A diff of dist/*.d.ts must show only path changes — no type-shape changes.

7. Risk

Low. The packages have no published consumers; the refactor is mechanical; the tests are the contract and they all must continue to pass. The largest file (id3v2.test.ts at 1340 lines) is the highest-leverage win: shrinking it makes the format-spec correspondence explicit and gives future maintainers per-frame-family focus.

The one non-mechanical risk is in audio-timeline/engine.ts — extracting helper modules implies extracting a TimelineEngineState shape, which is currently inlined as private class fields. The helpers operate on that shape; if the extraction inadvertently widens the visibility of private state, the public surface widens. Mitigation: keep TimelineEngineState as a non-exported type in engine-state.ts (or inline in engine.ts) and pass it explicitly to helpers; do not export it.