rescored / docs /frontend /data-flow.md
calebhan's picture
initial docs
c27ae8d

Frontend Data Flow

State Management Overview

The frontend manages three main data flows:

  1. Job Submission & Progress - WebSocket updates from backend
  2. Notation Data - MusicXML β†’ Zustand β†’ VexFlow rendering
  3. Editing Actions - User input β†’ State updates β†’ Re-render

Architecture

User Action
  ↓
React Component
  ↓
Zustand Store (mutations)
  ↓
VexFlow Re-render + Tone.js Playback

Data Models

API Layer

// api/client.ts
export class RescoredAPI {
  private baseURL = 'http://localhost:8000/api/v1';

  async submitJob(youtubeURL: string): Promise<{ jobId: string }> {
    const response = await fetch(`${this.baseURL}/transcribe`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ youtube_url: youtubeURL }),
    });

    return response.json();
  }

  async getScore(jobId: string): Promise<string> {
    const response = await fetch(`${this.baseURL}/scores/${jobId}`);
    return response.text();  // MusicXML string
  }

  connectWebSocket(jobId: string, onMessage: (msg: any) => void): WebSocket {
    const ws = new WebSocket(`ws://localhost:8000/api/v1/jobs/${jobId}/stream`);

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      onMessage(message);
    };

    return ws;
  }
}

Job Submission Flow

// hooks/useJobSubmission.ts
export function useJobSubmission() {
  const [jobId, setJobId] = useState<string | null>(null);
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');

  const submit = async (url: string) => {
    const api = new RescoredAPI();
    const { jobId } = await api.submitJob(url);

    setJobId(jobId);
    setStatus('processing');

    // Connect WebSocket
    const ws = api.connectWebSocket(jobId, (message) => {
      if (message.type === 'progress') {
        setProgress(message.progress);
      } else if (message.type === 'completed') {
        setStatus('completed');
        loadScore(jobId);  // Fetch MusicXML
      } else if (message.type === 'error') {
        setStatus('failed');
      }
    });
  };

  const loadScore = async (jobId: string) => {
    const api = new RescoredAPI();
    const musicXML = await api.getScore(jobId);

    // Parse and load into notation store
    useNotationStore.getState().loadFromMusicXML(musicXML);
  };

  return { submit, jobId, progress, status };
}

MusicXML Loading

// store/notation.ts
interface NotationStore {
  score: Score;
  loadFromMusicXML: (xml: string) => void;
  exportToMusicXML: () => string;
}

export const useNotationStore = create<NotationStore>((set) => ({
  score: null,

  loadFromMusicXML: (xml) => {
    const parsed = parseMusicXML(xml);  // See notation-rendering.md

    set({
      score: {
        id: generateId(),
        title: parsed.title,
        measures: parsed.measures,
        key: parsed.key,
        timeSignature: parsed.timeSignature,
        tempo: parsed.tempo,
      },
    });
  },

  exportToMusicXML: () => {
    const { score } = get();
    return generateMusicXML(score);  // Reverse of parsing
  },
}));

Component Data Flow

graph TB
    App["App"]
    JobSubmission["JobSubmission<br/>(submit URL, show progress)"]
    ScoreEditor["ScoreEditor"]
    NotationCanvas["NotationCanvas<br/>(reads: score, writes: nothing)"]
    PlaybackControls["PlaybackControls<br/>(reads: score, writes: playback state)"]
    EditorToolbar["EditorToolbar<br/>(writes: score via mutations)"]

    App --> JobSubmission
    JobSubmission -->|"on completion"| ScoreEditor
    ScoreEditor --> NotationCanvas
    ScoreEditor --> PlaybackControls
    ScoreEditor --> EditorToolbar

Optimistic Updates

For better UX, update UI immediately before backend confirmation:

const addNote = (note: Note) => {
  // Optimistically add to state
  set(state => ({
    score: {
      ...state.score,
      measures: addNoteToMeasure(state.score.measures, note),
    },
  }));

  // Save to backend (future: cloud sync)
  // If fails, revert
};

Export Flow

const handleExport = async (format: 'musicxml' | 'midi' | 'pdf') => {
  const { score } = useNotationStore.getState();

  if (format === 'musicxml') {
    const xml = exportToMusicXML(score);
    downloadFile(xml, 'score.musicxml');
  } else if (format === 'midi') {
    const midi = exportToMIDI(score);
    downloadFile(midi, 'score.mid');
  }
};

Next Steps

See WebSocket Protocol for message details.