7.7 KiB
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
exportfrom<pkg>/src/index.tsmust continue to resolve to the same symbol with the same signature. The split is internal. - Subpath exports unchanged. The
package.jsonexportsmap for each package keeps every existing entry. New internal helper modules are NOT added toexports— they are implementation details. - Barrels use explicit named re-exports (§6a). No
export * fromin new feature barrels. - Sibling
.mdfiles added for every new.tsper §4 (200-word minimum, six required sections). - Coverage stays 100/100/100/100 per
vitest.config.tsthresholds. - No new behavior. A diff of
dist/*.d.tsmust 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.