jam-cloud/.planning/codebase/JAMTRACK_REACT_PATTERNS.md

20 KiB

JamTrack React Integration Patterns

This document captures existing React patterns in jam-ui that will be extended for JamTrack implementation, building on lessons learned from the Backing Track player (Phase 3).

1. Component Architecture

Existing Components

JKSessionJamTrackModal (jam-ui/src/components/client/JKSessionJamTrackModal.js)

  • Purpose: JamTrack selection modal with search/autocomplete (replaces jQuery dialog)
  • Current state: Functional selection UI with REST API integration
  • Props: {isOpen, toggle, onJamTrackSelect}
  • Features:
    • React autocomplete component (JKJamTracksAutoComplete)
    • Pagination (10 items per page, matches legacy)
    • Search by artist or song name
    • REST API: getPurchasedJamTracks(options)
  • Usage: Already integrated, ready for enhanced JamTrack loading workflow

JKSessionJamTrackStems (jam-ui/src/components/client/JKSessionJamTrackStems.js)

  • Purpose: Stem mixer view showing individual JamTrack channels
  • Current state: Exists (needs verification of completeness)
  • Expected features: Individual stem volume/pan/mute controls

JKJamTrackPlayer (jam-ui/src/components/jamtracks/JKJamTrackPlayer.js)

  • Purpose: Public JamTrack player with master/stems dropdown
  • Current state: Exists (public-facing, not session player)
  • Note: Different from session player needs

Missing Components (To Be Created in Phase 5)

JKSessionJamTrackPlayer (NEW - primary work for Phase 5)

  • Purpose: Session JamTrack player with playback controls
  • Similar to: JKSessionBackingTrackPlayer.js (Phase 3 implementation)
  • Expected features:
    • Play/pause/stop buttons
    • Seek bar with drag-to-position
    • Duration and current time display (MM:SS format)
    • Volume control (per Phase 3 patterns)
    • Mixdown selection dropdown (master vs stems)
    • Error handling with typed errors
    • Loading states during sync/download

JKSessionJamTrackSync (NEW - if separate component chosen)

  • Purpose: Download/sync progress widget (replaces CoffeeScript DownloadJamTrack)
  • Alternative: Could be integrated into JKSessionJamTrackPlayer
  • Features: State machine visualization, progress bar, error handling

2. Redux State Structure

Current State (mediaSlice.js)

// jam-ui/src/store/features/mediaSlice.js

const initialState = {
  // Media arrays (resolved/enriched data)
  backingTracks: [],  // Backing track list
  jamTracks: [],      // JamTrack stem list (after load)
  recordedTracks: [], // Recorded track list

  // JamTrack real-time state (from WebSocket JAM_TRACK_CHANGES)
  jamTrackState: {},  // Current: minimal state
  downloadingJamTrack: false,

  // Loading states for async operations
  loading: {
    backingTrack: false,
    jamTrack: false,
    closing: false
  },

  error: null
};

Async thunks:

  • openBackingTrack({ file, jamClient }) - Open backing track file
  • loadJamTrack({ jamTrack, jamClient }) - Load JamTrack (lines 16-40)
    • Handles JMEP loading if present (tempo/pitch modifications)
    • Calls JamTrackPlay(jamTrack.id) - NOTE: Missing fqId format!
    • Returns {jamTrack, result}
  • closeMedia({ force, jamClient }) - Close all media

Identified gaps for Phase 5:

  • jamTrackState needs expansion for player state (playing, position, duration, error, isLoading)
  • Missing sync/download state tracking (packaging, downloading, keying, synchronized)
  • Missing selected mixdown state
  • loadJamTrack thunk uses incorrect ID format (should be fqId, not just jamTrack.id)

Current State (activeSessionSlice.js)

// jam-ui/src/store/features/activeSessionSlice.js

const initialState = {
  selectedJamTrack: null,  // Currently selected JamTrack for display
  jamTrackStems: [],       // Individual stem tracks from JamTrackGetTracks()
  backingTrackData: null,  // Backing track metadata
  // ... other session state
};

Actions:

  • setSelectedJamTrack(jamTrack) - Set active JamTrack
  • setJamTrackStems(stems) - Update stem list after load

Current State (sessionUISlice.js)

// jam-ui/src/store/features/sessionUISlice.js

const initialState = {
  // Modal visibility
  showBackingTrackModal: false,
  showJamTrackModal: false,  // JamTrack selection modal

  // Mixdown/mix UI state
  showMyMixes: false,         // Show user's custom mixes
  showCustomMixes: false,     // Show custom mix editor
  editingMixdownId: null,     // Currently editing mixdown ID
  creatingMixdown: false,     // Creating new mixdown flag
  createMixdownErrors: null,  // Mixdown creation errors

  // Backing track player popup state
  openBackingTrack: null,     // {path, mode: 'popup'|'modal'}
  // ... other UI state
};

Actions:

  • setShowJamTrackModal(boolean) - Toggle JamTrack selection modal
  • setShowMyMixes(boolean) - Show/hide user mixes
  • setEditingMixdownId(id) - Set mixdown being edited
  • setCreatingMixdown(boolean) - Toggle mixdown creation mode

Identified gaps for Phase 5:

  • Missing openJamTrack equivalent to openBackingTrack for popup/modal mode
  • Need JamTrack player modal visibility state
  • Need download/sync progress modal state

3. Hook Patterns

useMediaActions (jam-ui/src/hooks/useMediaActions.js)

Purpose: Redux-based media action wrappers (replaces MediaContext)

Current implementation:

const useMediaActions = () => {
  const dispatch = useDispatch();
  const { jamClient } = useJamServerContext();

  const openBackingTrack = useCallback(async (file) => {
    await dispatch(openBackingTrackThunk({ file, jamClient })).unwrap();
    dispatch(updateMediaSummary({ backingTrackOpen: true, ... }));
  }, [dispatch, jamClient]);

  const closeMedia = useCallback(async (force = false) => {
    await dispatch(closeMediaThunk({ force, jamClient })).unwrap();
    dispatch(updateMediaSummary({ mediaOpen: false, ... }));
  }, [dispatch, jamClient]);

  const openMetronome = useCallback(async (...) => { ... }, [...]);

  return {
    openBackingTrack,
    closeMedia,
    openMetronome,
    // ... other actions
  };
};

Pattern to follow for JamTrack:

const loadJamTrack = useCallback(async (jamTrack) => {
  // 1. Dispatch async thunk
  await dispatch(loadJamTrackThunk({ jamTrack, jamClient })).unwrap();

  // 2. Update media summary
  dispatch(updateMediaSummary({
    jamTrackOpen: true,
    userNeedsMediaControls: true
  }));
}, [dispatch, jamClient]);

useJamTrack (Expected - To Be Created)

Purpose: JamTrack-specific Redux selectors and operations

Expected shape:

const useJamTrack = () => {
  const dispatch = useDispatch();
  const jamTrackState = useSelector(state => state.media.jamTrackState);
  const selectedJamTrack = useSelector(state => state.activeSession.selectedJamTrack);
  const jamTrackStems = useSelector(state => state.activeSession.jamTrackStems);

  // Mixdown operations
  const selectMixdown = useCallback((mixdownId) => { ... }, [dispatch]);
  const createMixdown = useCallback((mixdownData) => { ... }, [dispatch]);
  const deleteMixdown = useCallback((mixdownId) => { ... }, [dispatch]);

  return {
    jamTrackState,
    selectedJamTrack,
    jamTrackStems,
    selectMixdown,
    createMixdown,
    deleteMixdown
  };
};

useSessionWebSocket (jam-ui/src/hooks/useSessionWebSocket.js)

Purpose: WebSocket message handling with Redux dispatchers

Current pattern:

const useSessionWebSocket = () => {
  // Handles MIXER_CHANGES WebSocket messages
  // Dispatches to mixersSlice actions

  // Expected addition for JamTrack:
  // JAM_TRACK_CHANGES messages → dispatch(updateJamTrackState(payload))
};

WebSocket messages to handle:

  • JAM_TRACK_CHANGES: Real-time JamTrack state updates (playing, position)
  • MIXDOWN_CHANGES: Mixdown packaging progress (signing_state, current_packaging_step)

4. Data Flow

Complete JamTrack Load Flow

1. User Action → Modal Selection
   ├─ User clicks "Open JamTrack" menu item
   ├─ dispatch(setShowJamTrackModal(true))
   └─ JKSessionJamTrackModal renders

2. Modal Selection → JamTrack Choice
   ├─ User searches/selects JamTrack from purchased list
   ├─ REST API: getPurchasedJamTracks(options)
   └─ onJamTrackSelect(jamTrack) callback

3. Redux Dispatch → Async Thunk
   ├─ dispatch(loadJamTrackThunk({ jamTrack, jamClient }))
   ├─ Thunk checks sync state via JamTrackGetTrackDetail(fqId)
   └─ If not synchronized, initiate download/sync flow

4. Download/Sync Flow (if needed)
   ├─ State: packaging → downloading → keying → synchronized
   ├─ JamTrackDownload(jamTrackId, mixdownId, userId, callbacks)
   ├─ WebSocket MIXDOWN_CHANGES → UI progress updates
   └─ JamTrackKeysRequest() → fetch encryption keys

5. JamClient Call → Load JamTrack
   ├─ sampleRate = jamClient.GetSampleRate() // 44 or 48
   ├─ fqId = `${jamTrack.id}-${sampleRateStr}`
   ├─ if (jamTrack.jmep) jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep)
   └─ result = jamClient.JamTrackPlay(fqId)

6. WebSocket Update → State Sync
   ├─ JAM_TRACK_CHANGES message received
   ├─ dispatch(updateJamTrackState(payload))
   └─ dispatch(setJamTrackStems(stems))

7. UI Update → Player Renders
   ├─ useSelector reads jamTrackState from Redux
   ├─ JKSessionJamTrackPlayer re-renders with new state
   └─ Playback controls enabled

Flowchart:

[Open Menu] → [JamTrack Modal] → [Select JamTrack]
                                        ↓
                        [Check Sync State via jamClient]
                                        ↓
                    ┌───────────────────┴───────────────────┐
                    ↓                                       ↓
          [Synchronized]                          [Not Synchronized]
                    ↓                                       ↓
         [Load JamTrack]              [Download/Sync Widget] → [Progress UI]
                    ↓                                       ↓
         [JamTrackPlay(fqId)]                   [JamTrackDownload(...)]
                    ↓                                       ↓
         [WebSocket JAM_TRACK_CHANGES]          [WebSocket MIXDOWN_CHANGES]
                    ↓                                       ↓
         [Redux State Update]                   [Packaging → Downloading → Keying]
                    ↓                                       ↓
         [Player UI Renders]                    [JamTrackKeysRequest()]
                    ↓                                       ↓
         [Playback Controls Active]             [Synchronized] → [Load Flow]

5. Error Handling Strategy

Error Types (from Backing Track Phase 3)

Apply same error categorization to JamTrack:

Type Color Retry Use Cases
file Red Yes Failed to fetch JamTrack details, invalid fqId
network Red Yes Lost connection during download, WebSocket disconnect
playback Yellow No Failed to start playback, seek error
sync Yellow Yes Download timeout, keying timeout, packaging error
general Yellow No jamClient not available, unknown error

Error UI Pattern

// Banner/alert above player controls
{error && (
  <div className={`error-banner error-${error.type}`}>
    <span className="error-icon">⚠️</span>
    <span className="error-message">{error.message}</span>
    <div className="error-actions">
      <button onClick={dismissError}>Dismiss</button>
      {error.canRetry && <button onClick={retryOperation}>Retry</button>}
    </div>
  </div>
)}

Async Error Handling (Thunks)

export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, jamClient }, { rejectWithValue }) => {
    try {
      // ... jamClient operations
      return { jamTrack, result };
    } catch (error) {
      return rejectWithValue({
        type: 'playback',  // Error type categorization
        message: error.message,
        canRetry: false
      });
    }
  }
);

6. Performance Patterns (from Backing Track Phase 3)

Visibility-Aware Polling

// Adjust polling frequency based on tab visibility
useEffect(() => {
  const pollInterval = document.hidden ? 2000 : 500; // 500ms visible, 2s hidden

  const intervalId = setInterval(async () => {
    if (!jamClient.JamTrackIsPlaying()) return;

    const position = parseInt(await jamClient.SessionCurrrentJamTrackPlayPosMs());
    const duration = parseInt(await jamClient.SessionGetJamTracksPlayDurationMs());

    // Only update if values changed (lazy state updates)
    if (position !== currentPosition) {
      setCurrentPosition(position);
    }
  }, pollInterval);

  return () => clearInterval(intervalId);
}, [document.hidden, currentPosition]);

useCallback for Handlers

// Prevent re-renders by memoizing all handler functions
const handlePlay = useCallback(async () => {
  await jamClient.SessionStartPlay();
  setIsPlaying(true);
}, [jamClient]);

const handlePause = useCallback(async () => {
  await jamClient.SessionPausePlay();
  setIsPlaying(false);
}, [jamClient]);

const handleSeek = useCallback(async (positionMs) => {
  await jamClient.SessionJamTrackSeekMs(positionMs);
}, [jamClient]);

Conditional State Updates

// Only update state if value actually changed
const newPosition = parseInt(await jamClient.SessionCurrrentJamTrackPlayPosMs());
if (newPosition !== currentPosition) {
  setCurrentPosition(newPosition);
}

Consecutive Error Tracking

// Track consecutive errors with useRef (not state)
const consecutiveErrorsRef = useRef(0);

const handlePollingError = () => {
  consecutiveErrorsRef.current += 1;

  if (consecutiveErrorsRef.current >= 3) {
    setError({ type: 'network', message: 'Lost connection to jamClient' });
    stopPolling();
  }
};

const handlePollingSuccess = () => {
  consecutiveErrorsRef.current = 0; // Reset on success
};

7. Established Patterns from Phase 3

jamClient Integration

Critical rules:

  1. jamClient returns Promises - Always use async/await
  2. jamClient returns strings - Always parseInt() before math operations
  3. Non-serializable objects - Pass jamClient as prop, NOT stored in Redux
  4. Cleanup on unmount - Stop playback to prevent stale state
// WRONG - storing jamClient in Redux
const initialState = {
  jamClient: null, // ❌ Redux requires serializable state
};

// RIGHT - passing jamClient as prop
const JKSessionJamTrackPlayer = ({ jamClient, jamTrack }) => {
  // Use jamClient directly, don't store in state
};

Async Pattern

// jamClient calls are always async
const duration = parseInt(await jamClient.SessionGetJamTracksPlayDurationMs());
const position = parseInt(await jamClient.SessionCurrrentJamTrackPlayPosMs());
const isPlaying = await jamClient.JamTrackIsPlaying();

Cleanup Pattern

useEffect(() => {
  return () => {
    // Cleanup on unmount
    if (jamClient) {
      jamClient.JamTrackStopPlay();
    }
  };
}, [jamClient]);

8. Comparison to Backing Track Patterns

Feature Backing Track JamTrack Notes
File selection Native dialog (ShowSelectBackingTrackDialog) React modal (JKSessionJamTrackModal) JamTrack: existing modal ready
Loading Immediate (SessionOpenBackingTrackFile) Async sync (JamTrackDownloadJamTrackKeysRequest) JamTrack: requires download/sync widget
Playback API SessionOpenBackingTrackFile(path) JamTrackPlay(fqId) JamTrack: fqId format critical
Volume control SessionSetTrackVolumeData(mixer.id, ...) Same (uses mixer system) Same pattern
Loop control SessionSetBackingTrackFileLoop(path, bool) TBD (needs investigation) JamTrack: API unclear
Seek/position SessionTrackSeekMs(ms), SessionCurrentPlayPosMs() SessionJamTrackSeekMs(ms), SessionCurrrentJamTrackPlayPosMs() Similar APIs
Duration SessionGetTracksPlayDurationMs() SessionGetJamTracksPlayDurationMs() Both plural "Tracks"
Modal/popup mode Supported (Phase 3 fixed popup mode bugs) Expected similar pattern Apply Phase 3 fixes
Error handling 4 error types (file, network, playback, general) 5 error types (+ sync) JamTrack: add sync errors
Performance Visibility-aware polling, useCallback, lazy updates Same patterns Apply Phase 3 optimizations

9. Gaps Identified for Phase 5 Implementation

Redux State Gaps

  • Expand mediaSlice.jamTrackState for player state (position, duration, isPlaying, error, isLoading)
  • Add download/sync state tracking (packaging, downloading, keying, synchronized)
  • Add selected mixdown state to activeSessionSlice
  • Fix loadJamTrack thunk to use correct fqId format (not just jamTrack.id)
  • Add openJamTrack to sessionUISlice for popup/modal mode support

Component Gaps

  • Create JKSessionJamTrackPlayer component (main player with playback controls)
  • Create or integrate download/sync progress widget (state machine visualization)
  • Verify JKSessionJamTrackStems completeness for stem mixer controls
  • Add mixdown selection dropdown to player UI

Hook Gaps

  • Create useJamTrack hook for selectors and mixdown operations
  • Add JAM_TRACK_CHANGES handling to useSessionWebSocket
  • Add MIXDOWN_CHANGES handling for packaging progress
  • Enhance useMediaActions with JamTrack-specific operations

API Integration Gaps

  • Implement download/sync flow with callbacks (JamTrackDownload)
  • Implement keying flow (JamTrackKeysRequest)
  • Handle WebSocket subscriptions for mixdown packaging (from legacy patterns)
  • Implement mixdown CRUD operations (create, edit, delete)
  • Verify loop control API for JamTracks (unclear from legacy code)

Error Handling Gaps

  • Add sync error type for download/packaging/keying failures
  • Implement timeout handling for packaging (state machine max times)
  • Add retry logic for network failures during download
  • Handle SIGNING_TIMEOUT, QUEUED_TIMEOUT, QUIET_TIMEOUT error states

Summary

React patterns established in jam-ui:

  • Redux Toolkit with async thunks for jamClient integration
  • useMediaActions hook pattern for action wrappers
  • Modal components for file/track selection
  • WebSocket integration via useSessionWebSocket
  • Error handling with typed errors and user feedback
  • Performance optimizations from Phase 3

Ready to use:

  • JKSessionJamTrackModal - Selection UI exists and works
  • useMediaActions - Pattern established, needs JamTrack methods
  • Redux infrastructure - Slices exist, need expansion
  • Error patterns - Backing Track patterns apply directly
  • Performance patterns - Apply Phase 3 optimizations

Primary work for Phase 5:

  1. Create JKSessionJamTrackPlayer component (similar to Backing Track)
  2. Implement download/sync state machine in React
  3. Expand Redux state for JamTrack player and sync tracking
  4. Add WebSocket handlers for JAM_TRACK_CHANGES and MIXDOWN_CHANGES
  5. Implement mixdown selection and CRUD operations
  6. Fix loadJamTrack thunk to use correct fqId format

Complexity factors vs. Backing Track:

  • Download/sync flow (8-state machine vs. instant file open)
  • Mixdown selection (user chooses stem configuration)
  • WebSocket packaging progress (real-time server-side mixdown creation)
  • Encryption key management (separate API call after download)
  • JMEP support (tempo/pitch modifications)