# JamTrack Redux State Design

Comprehensive Redux state structure for JamTrack functionality, extending existing mediaSlice, activeSessionSlice, and sessionUISlice with JamTrack-specific data and operations.

## 1. mediaSlice Extensions

### Current State
```javascript
// jam-ui/src/store/features/mediaSlice.js

const initialState = {
  backingTracks: [],        // Backing track list
  jamTracks: [],            // JamTrack stem list (after load)
  recordedTracks: [],       // Recorded track list
  jamTrackState: {},        // Current: minimal state
  downloadingJamTrack: false,
  loading: {
    backingTrack: false,
    jamTrack: false,
    closing: false
  },
  error: null
};
```

### Extended State Structure

```javascript
const initialState = {
  // Existing media arrays (resolved/enriched data from mixer system)
  backingTracks: [],        // EXISTING
  jamTracks: [],            // EXISTING - extend structure
  recordedTracks: [],       // EXISTING

  // EXTENDED: JamTrack playback state (real-time, from WebSocket + polling)
  jamTrackState: {
    isPlaying: boolean,           // Currently playing
    isPaused: boolean,            // Paused state
    currentPositionMs: number,    // Current playback position in milliseconds
    durationMs: number,           // Total duration in milliseconds
    selectedMixdownId: number | null,  // Active mixdown ID
    playbackMode: 'master' | 'custom-mix' | 'stem',  // Current playback mode
    lastUpdate: timestamp         // Last WebSocket update time
  },

  // NEW: Download/sync state machine
  downloadState: {
    jamTrackId: number | null,    // JamTrack being downloaded
    mixdownId: number | null,     // Mixdown package being downloaded
    fqId: string | null,          // Fully-qualified ID {jamTrackId}-{sampleRate}
    state: 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error',
    progress: number,             // Download progress (0-100)
    currentStep: number,          // Current packaging step
    totalSteps: number,           // Total packaging steps
    error: {
      type: 'download' | 'sync' | 'timeout' | 'network',
      message: string,
      canRetry: boolean
    } | null
  },

  // EXTENDED: Loading states
  loading: {
    backingTrack: boolean,        // EXISTING
    jamTrack: boolean,            // EXISTING - now includes download progress
    metronome: boolean,           // EXISTING
    closing: boolean              // EXISTING
  },

  // EXISTING
  error: null | {
    type: string,
    message: string
  }
};
```

### Type Definitions

```typescript
interface JamTrackState {
  isPlaying: boolean;
  isPaused: boolean;
  currentPositionMs: number;
  durationMs: number;
  selectedMixdownId: number | null;
  playbackMode: 'master' | 'custom-mix' | 'stem';
  lastUpdate: number;  // Unix timestamp
}

interface DownloadState {
  jamTrackId: number | null;
  mixdownId: number | null;
  fqId: string | null;
  state: 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error';
  progress: number;  // 0-100
  currentStep: number;
  totalSteps: number;
  error: DownloadError | null;
}

interface DownloadError {
  type: 'download' | 'sync' | 'timeout' | 'network';
  message: string;
  canRetry: boolean;
}
```

## 2. activeSessionSlice Extensions

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

### Extended State Structure

```javascript
const initialState = {
  // EXISTING
  selectedJamTrack: JamTrack | null,  // Full JamTrack object
  jamTrackStems: StemTrack[],         // Stem track array after load
  backingTrackData: any,              // Backing track metadata

  // NEW: Mixdown data
  availableMixdowns: Mixdown[],       // User's custom mixes for current JamTrack
  activeMixdown: Mixdown | null,      // Currently playing mixdown
  mixdownCache: {                     // Cache mixdown data to avoid re-fetching
    [jamTrackId: number]: Mixdown[]
  },

  // ... other session state
};
```

### Type Definitions

```typescript
interface JamTrack {
  id: number;
  name: string;
  artist: string;
  albumArt: string | null;
  jmep: JMEPData | null;
  mixdowns: Mixdown[];
  version: number;
}

interface JMEPData {
  tempo: number;    // BPM or percentage
  pitch: number;    // Semitones shift
}

interface Mixdown {
  id: number;
  name: string;
  type: 'master' | 'custom' | 'stem';
  isDefault: boolean;
  userId: number | null;    // Null for master/default mixes
  packages: MixdownPackage[];
}

interface MixdownPackage {
  id: number;
  file_type: 'ogg';
  encrypt_type: 'jkz';
  sample_rate: 44 | 48;
  signing_state: 'SIGNING_TIMEOUT' | 'QUEUED_TIMEOUT' | 'QUIET_TIMEOUT' | 'ERROR' | 'SIGNED';
  packaging_steps: number | null;
  current_packaging_step: number | null;
}

interface StemTrack {
  id: number;
  name: string;
  instrumentType: string;
  mixer: any;  // Mixer object from session
  track: any;  // Track object from jamClient
}
```

## 3. sessionUISlice Extensions

### Current State
```javascript
const initialState = {
  modals: {
    jamTrack: false,        // JamTrack selection modal
    backingTrack: false,    // Backing track selection modal
    metronome: false,       // Metronome modal
  },
  showMyMixes: false,
  showCustomMixes: false,
  editingMixdownId: null,
  creatingMixdown: false,
  createMixdownErrors: null,
  openBackingTrack: null,   // {path, mode: 'popup'|'modal'}
  // ... other UI state
};
```

### Extended State Structure

```javascript
const initialState = {
  // EXTENDED: Modal visibility
  modals: {
    jamTrack: boolean,          // EXISTING - Selection modal
    backingTrack: boolean,      // EXISTING
    metronome: boolean,         // EXISTING
    jamTrackPlayer: boolean,    // NEW - Player modal
    downloadProgress: boolean   // NEW - Download progress modal (if separate)
  },

  // EXISTING: Mixdown UI state
  showMyMixes: boolean,
  showCustomMixes: boolean,
  editingMixdownId: number | null,
  creatingMixdown: boolean,
  createMixdownErrors: any | null,

  // NEW: JamTrack player UI state
  openJamTrack: {
    fqId: string,
    mode: 'modal' | 'popup'
  } | null,

  // NEW: JamTrack-specific UI preferences
  jamTrackUI: {
    showMixdownSelector: boolean,      // Show mixdown dropdown in player
    showDownloadProgress: boolean,     // Show download progress indicator
    autoPlayAfterDownload: boolean,    // Auto-play after sync completes
    defaultMixdownType: 'master' | 'last-used',  // Default mixdown selection
    playerMode: 'modal' | 'popup'      // Default player mode
  },

  // EXISTING
  openBackingTrack: any | null,
  // ... other UI state
};
```

### Type Definitions

```typescript
interface OpenJamTrack {
  fqId: string;             // {jamTrackId}-{sampleRate}
  mode: 'modal' | 'popup';
}

interface JamTrackUIPreferences {
  showMixdownSelector: boolean;
  showDownloadProgress: boolean;
  autoPlayAfterDownload: boolean;
  defaultMixdownType: 'master' | 'last-used';
  playerMode: 'modal' | 'popup';
}
```

## 4. Async Thunks

### loadJamTrack (ENHANCED)

**Purpose:** Load and play JamTrack with sync check, download, and JMEP support

**Current Implementation (mediaSlice.js:16-40):**
```javascript
export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, jamClient }, { rejectWithValue }) => {
    try {
      // Load JMep data if available
      if (jamTrack.jmep) {
        const sampleRate = await jamClient.GetSampleRate();
        const sampleRateForFilename = sampleRate === 48 ? '48' : '44';
        const fqId = `${jamTrack.id}-${sampleRateForFilename}`;
        await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
      }

      // Play/load the jamtrack
      const result = await jamClient.JamTrackPlay(jamTrack.id);  // ❌ BUG: Should use fqId

      if (!result) {
        throw new Error('Unable to open JamTrack');
      }

      return { jamTrack, result };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Enhanced Implementation:**
```javascript
export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, mixdownId, autoPlay = false, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      // 1. Build fqId
      const sampleRate = await jamClient.GetSampleRate();
      const sampleRateStr = sampleRate === 48 ? '48' : '44';
      const fqId = `${jamTrack.id}-${sampleRateStr}`;

      // 2. Check sync state
      dispatch(setDownloadState({ state: 'checking', fqId, jamTrackId: jamTrack.id }));
      const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);

      // 3. If not synchronized, initiate download
      if (!trackDetail.key_state || trackDetail.key_state !== 'AVAILABLE') {
        dispatch(setDownloadState({ state: 'downloading', fqId, mixdownId }));
        await dispatch(downloadJamTrack({ jamTrackId: jamTrack.id, mixdownId, fqId, jamClient })).unwrap();
      }

      // 4. Load JMEP if present
      if (jamTrack.jmep) {
        await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
      }

      // 5. Load JamTrack
      const result = await jamClient.JamTrackPlay(fqId);  // ✅ FIXED: Uses fqId
      if (!result) {
        throw new Error('Unable to open JamTrack');
      }

      // 6. Get stems
      const stems = await jamClient.JamTrackGetTracks();

      // 7. Start playback if autoPlay
      if (autoPlay) {
        await jamClient.SessionStartPlay();
      }

      dispatch(setDownloadState({ state: 'synchronized', fqId }));

      return { jamTrack, fqId, stems, result };
    } catch (error) {
      dispatch(setDownloadState({ state: 'error', error: { type: 'playback', message: error.message, canRetry: true } }));
      return rejectWithValue({ type: 'playback', message: error.message });
    }
  }
);
```

**Parameters:**
- `jamTrack: JamTrack` - Full JamTrack object
- `mixdownId?: number` - Optional pre-selected mixdown
- `autoPlay?: boolean` - Auto-start playback after load (default: false)
- `jamClient: JamClient` - Native client instance

**Returns:** `{ jamTrack, fqId, stems, result }`

---

### downloadJamTrack (NEW)

**Purpose:** Download JamTrack package with progress tracking and keying

```javascript
export const downloadJamTrack = createAsyncThunk(
  'media/downloadJamTrack',
  async ({ jamTrackId, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      // 1. Select appropriate mixdown package
      const mixdown = await fetchMixdown(mixdownId);  // REST API call
      const sampleRate = await jamClient.GetSampleRate();
      const package = selectCompatiblePackage(mixdown.packages, sampleRate);

      if (!package) {
        throw new Error(`No compatible package found for sample rate ${sampleRate}kHz`);
      }

      dispatch(setDownloadState({
        state: 'downloading',
        mixdownId: package.id,
        progress: 0,
        totalSteps: package.packaging_steps || 1
      }));

      // 2. Create global callback functions for native client
      window.jamTrackDownloadProgress = (percent) => {
        dispatch(setDownloadState({ progress: percent }));
      };

      window.jamTrackDownloadSuccess = () => {
        dispatch(setDownloadState({ state: 'keying' }));
      };

      window.jamTrackDownloadFail = (error) => {
        dispatch(setDownloadState({
          state: 'error',
          error: { type: 'download', message: error, canRetry: true }
        }));
      };

      // 3. Initiate download (callbacks as string names)
      await jamClient.JamTrackDownload(
        jamTrackId,
        package.id,
        currentUserId,
        'jamTrackDownloadProgress',
        'jamTrackDownloadSuccess',
        'jamTrackDownloadFail'
      );

      // 4. Wait for download completion (poll for success callback)
      await waitForDownloadComplete();

      // 5. Request encryption keys
      dispatch(setDownloadState({ state: 'keying' }));
      await jamClient.JamTrackKeysRequest();

      // 6. Poll for key availability
      let keyAttempts = 0;
      while (keyAttempts < 20) {  // 10 second timeout (500ms * 20)
        await sleep(500);
        const detail = await jamClient.JamTrackGetTrackDetail(fqId);
        if (detail.key_state === 'AVAILABLE') {
          dispatch(setDownloadState({ state: 'synchronized' }));
          return { jamTrackId, fqId, package };
        }
        keyAttempts++;
      }

      throw new Error('Timeout requesting encryption keys');

    } catch (error) {
      const errorType = error.message.includes('Timeout') ? 'timeout' : 'sync';
      dispatch(setDownloadState({
        state: 'error',
        error: { type: errorType, message: error.message, canRetry: true }
      }));
      return rejectWithValue({ type: errorType, message: error.message });
    } finally {
      // Cleanup global callbacks
      delete window.jamTrackDownloadProgress;
      delete window.jamTrackDownloadSuccess;
      delete window.jamTrackDownloadFail;
    }
  }
);
```

**Parameters:**
- `jamTrackId: number` - JamTrack ID
- `mixdownId: number` - Selected mixdown package ID
- `fqId: string` - Fully-qualified ID
- `jamClient: JamClient` - Native client instance

**Returns:** `{ jamTrackId, fqId, package }`

---

### checkJamTrackSync (NEW)

**Purpose:** Check download/key state without initiating download

```javascript
export const checkJamTrackSync = createAsyncThunk(
  'media/checkJamTrackSync',
  async ({ jamTrackId, jamClient }, { rejectWithValue }) => {
    try {
      const sampleRate = await jamClient.GetSampleRate();
      const sampleRateStr = sampleRate === 48 ? '48' : '44';
      const fqId = `${jamTrackId}-${sampleRateStr}`;

      const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);

      const isSynchronized = trackDetail.key_state === 'AVAILABLE';

      return {
        jamTrackId,
        fqId,
        isSynchronized,
        keyState: trackDetail.key_state,
        version: trackDetail.version
      };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Parameters:**
- `jamTrackId: number` - JamTrack ID to check
- `jamClient: JamClient` - Native client instance

**Returns:** `{ jamTrackId, fqId, isSynchronized, keyState, version }`

---

### loadJMEP (NEW)

**Purpose:** Load tempo/pitch modifications separately from main load

```javascript
export const loadJMEP = createAsyncThunk(
  'media/loadJMEP',
  async ({ jamTrack, jmepData, jamClient }, { rejectWithValue }) => {
    try {
      const sampleRate = await jamClient.GetSampleRate();
      const sampleRateStr = sampleRate === 48 ? '48' : '44';
      const fqId = `${jamTrack.id}-${sampleRateStr}`;

      await jamClient.JamTrackLoadJmep(fqId, jmepData);

      return { jamTrackId: jamTrack.id, fqId, jmepData };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Parameters:**
- `jamTrack: JamTrack` - JamTrack object
- `jmepData: JMEPData` - Tempo/pitch modification data
- `jamClient: JamClient` - Native client instance

**Returns:** `{ jamTrackId, fqId, jmepData }`

---

### seekJamTrack (NEW)

**Purpose:** Seek playback with error handling and state update

```javascript
export const seekJamTrack = createAsyncThunk(
  'media/seekJamTrack',
  async ({ positionMs, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      await jamClient.SessionJamTrackSeekMs(positionMs);

      // Update state immediately (optimistic update)
      dispatch(updateJamTrackState({ currentPositionMs: positionMs }));

      return { positionMs };
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Parameters:**
- `positionMs: number` - Target position in milliseconds
- `jamClient: JamClient` - Native client instance

**Returns:** `{ positionMs }`

---

### closeJamTrack (NEW)

**Purpose:** Stop playback and cleanup state

```javascript
export const closeJamTrack = createAsyncThunk(
  'media/closeJamTrack',
  async ({ jamClient }, { dispatch, rejectWithValue }) => {
    try {
      await jamClient.JamTrackStopPlay();

      // Clear state
      dispatch(clearJamTrackState());
      dispatch(setSelectedJamTrack(null));
      dispatch(setJamTrackStems([]));

      return null;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
```

**Parameters:**
- `jamClient: JamClient` - Native client instance

**Returns:** `null`

## 5. Reducers

### mediaSlice Reducers

```javascript
reducers: {
  // EXISTING
  setBackingTracks: (state, action) => {
    state.backingTracks = action.payload;
  },

  setJamTracks: (state, action) => {
    state.jamTracks = action.payload;
  },

  // EXTENDED: Update real-time JamTrack playback state
  updateJamTrackState: (state, action) => {
    state.jamTrackState = {
      ...state.jamTrackState,
      ...action.payload,
      lastUpdate: Date.now()
    };
  },

  // NEW: Clear JamTrack state
  clearJamTrackState: (state) => {
    state.jamTrackState = {
      isPlaying: false,
      isPaused: false,
      currentPositionMs: 0,
      durationMs: 0,
      selectedMixdownId: null,
      playbackMode: 'master',
      lastUpdate: Date.now()
    };
  },

  // NEW: Update download/sync state
  setDownloadState: (state, action) => {
    state.downloadState = {
      ...state.downloadState,
      ...action.payload
    };
  },

  // NEW: Reset download state
  clearDownloadState: (state) => {
    state.downloadState = {
      jamTrackId: null,
      mixdownId: null,
      fqId: null,
      state: 'idle',
      progress: 0,
      currentStep: 0,
      totalSteps: 0,
      error: null
    };
  }
}
```

### activeSessionSlice Reducers

```javascript
reducers: {
  // EXISTING
  setSelectedJamTrack: (state, action) => {
    state.selectedJamTrack = action.payload;
  },

  setJamTrackStems: (state, action) => {
    state.jamTrackStems = action.payload;
  },

  // NEW: Set available mixdowns for current JamTrack
  setAvailableMixdowns: (state, action) => {
    state.availableMixdowns = action.payload;

    // Cache mixdowns
    if (state.selectedJamTrack) {
      state.mixdownCache[state.selectedJamTrack.id] = action.payload;
    }
  },

  // NEW: Set active mixdown
  setActiveMixdown: (state, action) => {
    state.activeMixdown = action.payload;
  },

  // NEW: Clear JamTrack data
  clearJamTrackData: (state) => {
    state.selectedJamTrack = null;
    state.jamTrackStems = [];
    state.availableMixdowns = [];
    state.activeMixdown = null;
  }
}
```

### sessionUISlice Reducers

```javascript
reducers: {
  // EXISTING
  setShowJamTrackModal: (state, action) => {
    state.modals.jamTrack = action.payload;
  },

  // NEW: Set JamTrack player modal visibility
  setShowJamTrackPlayer: (state, action) => {
    state.modals.jamTrackPlayer = action.payload;
  },

  // NEW: Set open JamTrack (popup/modal)
  setOpenJamTrack: (state, action) => {
    state.openJamTrack = action.payload;
  },

  // NEW: Update JamTrack UI preferences
  updateJamTrackUI: (state, action) => {
    state.jamTrackUI = {
      ...state.jamTrackUI,
      ...action.payload
    };
  }
}
```

## 6. Selectors

### Media Selectors

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

export const selectJamTracks = (state) => state.media.jamTracks;
export const selectJamTrackState = (state) => state.media.jamTrackState;
export const selectDownloadState = (state) => state.media.downloadState;
export const selectIsJamTrackPlaying = (state) => state.media.jamTrackState.isPlaying;
export const selectIsJamTrackPaused = (state) => state.media.jamTrackState.isPaused;
export const selectJamTrackPosition = (state) => state.media.jamTrackState.currentPositionMs;
export const selectJamTrackDuration = (state) => state.media.jamTrackState.durationMs;
export const selectJamTrackLoading = (state) => state.media.loading.jamTrack;
export const selectDownloadProgress = (state) => state.media.downloadState.progress;
export const selectDownloadError = (state) => state.media.downloadState.error;
```

### Active Session Selectors

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

export const selectSelectedJamTrack = (state) => state.activeSession.selectedJamTrack;
export const selectJamTrackStems = (state) => state.activeSession.jamTrackStems;
export const selectAvailableMixdowns = (state) => state.activeSession.availableMixdowns;
export const selectActiveMixdown = (state) => state.activeSession.activeMixdown;
export const selectMixdownCache = (state) => state.activeSession.mixdownCache;

// Memoized selector: Get cached mixdowns for a JamTrack
export const selectCachedMixdowns = createSelector(
  [selectMixdownCache, (state, jamTrackId) => jamTrackId],
  (cache, jamTrackId) => cache[jamTrackId] || []
);
```

### Session UI Selectors

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

export const selectShowJamTrackModal = (state) => state.sessionUI.modals.jamTrack;
export const selectShowJamTrackPlayer = (state) => state.sessionUI.modals.jamTrackPlayer;
export const selectOpenJamTrack = (state) => state.sessionUI.openJamTrack;
export const selectJamTrackUI = (state) => state.sessionUI.jamTrackUI;
export const selectAutoPlayAfterDownload = (state) => state.sessionUI.jamTrackUI.autoPlayAfterDownload;
```

## 7. WebSocket Integration

### MIXER_CHANGES Handler (EXISTING - Extend)

```javascript
// jam-ui/src/hooks/useSessionWebSocket.js

const handleMixerChanges = useCallback((message) => {
  const { sessionMixers } = message;

  // EXISTING: Update backing tracks
  dispatch(setBackingTracks(sessionMixers.mixers.backingTracks));

  // EXTENDED: Update JamTrack stems
  if (sessionMixers.mixers.jamTracks && sessionMixers.mixers.jamTracks.length > 0) {
    dispatch(setJamTracks(sessionMixers.mixers.jamTracks));
  }

  // EXISTING: Update recorded tracks
  dispatch(setRecordedTracks(sessionMixers.mixers.recordedTracks));
}, [dispatch]);
```

### JAM_TRACK_CHANGES Handler (NEW)

```javascript
const handleJamTrackChanges = useCallback((message) => {
  const { jamTrackState } = message;

  // Update real-time JamTrack playback state
  dispatch(updateJamTrackState({
    isPlaying: jamTrackState.isPlaying,
    isPaused: jamTrackState.isPaused,
    currentPositionMs: jamTrackState.currentPositionMs,
    selectedMixdownId: jamTrackState.selectedMixdownId
  }));
}, [dispatch]);

// Register handler
useEffect(() => {
  if (socket) {
    socket.on('JAM_TRACK_CHANGES', handleJamTrackChanges);
    return () => socket.off('JAM_TRACK_CHANGES', handleJamTrackChanges);
  }
}, [socket, handleJamTrackChanges]);
```

### MIXDOWN_CHANGES Handler (NEW)

```javascript
const handleMixdownChanges = useCallback((message) => {
  const { mixdownPackage } = message;

  // Update packaging progress
  if (mixdownPackage.signing_state !== 'SIGNED') {
    dispatch(setDownloadState({
      currentStep: mixdownPackage.current_packaging_step,
      totalSteps: mixdownPackage.packaging_steps
    }));
  } else {
    // Packaging complete - ready to download
    dispatch(setDownloadState({
      state: 'downloading',
      currentStep: mixdownPackage.packaging_steps,
      totalSteps: mixdownPackage.packaging_steps
    }));
  }

  // Handle error states
  if (['SIGNING_TIMEOUT', 'QUEUED_TIMEOUT', 'QUIET_TIMEOUT', 'ERROR'].includes(mixdownPackage.signing_state)) {
    dispatch(setDownloadState({
      state: 'error',
      error: {
        type: 'timeout',
        message: `Packaging ${mixdownPackage.signing_state.toLowerCase().replace('_', ' ')}`,
        canRetry: true
      }
    }));
  }
}, [dispatch]);

// Register handler
useEffect(() => {
  if (socket) {
    socket.on('MIXDOWN_CHANGES', handleMixdownChanges);
    return () => socket.off('MIXDOWN_CHANGES', handleMixdownChanges);
  }
}, [socket, handleMixdownChanges]);
```

## 8. State Machine for Download/Sync

### States and Transitions

| State | Description | Possible Next States | Trigger |
|-------|-------------|---------------------|---------|
| **idle** | No active download/sync | checking, downloading | User selects JamTrack |
| **checking** | Querying sync state via `JamTrackGetTrackDetail()` | synchronized, downloading, error | Sync check complete |
| **downloading** | Download in progress via `JamTrackDownload()` | keying, error | Download complete/failed |
| **keying** | Requesting encryption keys via `JamTrackKeysRequest()` | synchronized, error | Keys received/timeout |
| **synchronized** | JamTrack on disk with keys, ready to play | idle | `JamTrackPlay()` success or user closes |
| **error** | Download/sync/keying failed | idle, downloading (retry) | Error occurred |

### State Machine Diagram

```
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  [User Selects JamTrack]                                    │
│         │                                                    │
│         ▼                                                    │
│      ┌──────┐  JamTrackGetTrackDetail()  ┌──────────┐      │
│      │ idle │──────────────────────────▶│ checking │      │
│      └──────┘                            └─────┬────┘      │
│         ▲                                      │            │
│         │                                      │            │
│         │              ┌───────────────────────┼────────┐   │
│         │              │                       │        │   │
│         │              ▼                       ▼        ▼   │
│         │      ┌──────────────┐       ┌────────────┐ ┌───────────┐
│         │      │ synchronized │◀──────│downloading │ │  error  │
│         │      └──────────────┘       └─────┬──────┘ └─────┬─────┘
│         │              ▲                    │              │
│         │              │                    ▼              │
│         │              │             ┌────────────┐        │
│         │              └─────────────│  keying    │        │
│         │                            └────────────┘        │
│         │                                                  │
│         └──────────────────────────────────────────────────┘
│                      [Cleanup / Close]
└─────────────────────────────────────────────────────────────┘
```

### State Actions

**idle:**
- No UI indication
- downloadState.state = 'idle'

**checking:**
- Show "Checking sync state..." spinner
- downloadState.state = 'checking'

**downloading:**
- Show progress bar with percentage
- Update on `jamTrackDownloadProgress` callback
- downloadState.state = 'downloading', progress = 0-100

**keying:**
- Show "Requesting encryption keys..." spinner
- Timeout after 10 seconds
- downloadState.state = 'keying'

**synchronized:**
- Hide download UI
- Enable play button
- downloadState.state = 'synchronized'

**error:**
- Show error banner with message
- Provide retry button
- downloadState.state = 'error', error = { type, message, canRetry }

### Timeout Handling

```javascript
const TIMEOUT_LIMITS = {
  checking: 5000,      // 5 seconds
  downloading: 300000, // 5 minutes
  keying: 10000        // 10 seconds
};

// Example: Keying timeout
let keyingTimeout;
dispatch(setDownloadState({ state: 'keying' }));
keyingTimeout = setTimeout(() => {
  dispatch(setDownloadState({
    state: 'error',
    error: {
      type: 'timeout',
      message: 'Timeout requesting encryption keys',
      canRetry: true
    }
  }));
}, TIMEOUT_LIMITS.keying);

// Clear timeout on success
clearTimeout(keyingTimeout);
```

## 9. Comparison to Backing Track Redux Patterns

| Aspect | Backing Track | JamTrack | Complexity Change |
|--------|---------------|----------|-------------------|
| **State slices** | 2 (media, sessionUI) | 3 (media, activeSession, sessionUI) | +1 slice for mixdowns |
| **Async thunks** | 2 (open, close) | 6 (load, download, check, loadJMEP, seek, close) | +4 thunks |
| **Loading states** | 2 (loading, operating) | 3 (loading, operating, syncing) | +1 state |
| **Error types** | 4 (file, network, playback, general) | 6 (file, network, playback, general, download, sync) | +2 types |
| **WebSocket handlers** | 1 (MIXER_CHANGES) | 3 (MIXER_CHANGES, JAM_TRACK_CHANGES, MIXDOWN_CHANGES) | +2 handlers |
| **State machine** | None (immediate load) | 6-state download/sync machine | New complexity |
| **Mixdown support** | N/A | Full mixdown selection/management | New feature |
| **JMEP support** | N/A | Separate thunk for tempo/pitch | New feature |
| **Sample rate** | Not critical | Critical (fqId format) | New requirement |

**Reusable Patterns:**
- ✓ Async thunk structure (try/catch with rejectWithValue)
- ✓ Loading state management (loading.jamTrack boolean)
- ✓ Error handling (type-based errors with retry flag)
- ✓ WebSocket integration (message handlers dispatching actions)
- ✓ Selector patterns (memoized selectors with createSelector)

**New Patterns:**
- Download/sync state machine with progress tracking
- Global callback functions for native client download
- Mixdown caching to avoid re-fetching
- fqId format construction and validation
- JMEP loading as separate operation

---

## Summary

**Redux Extensions:**
- mediaSlice: +2 new state objects (jamTrackState, downloadState)
- activeSessionSlice: +3 new state objects (availableMixdowns, activeMixdown, mixdownCache)
- sessionUISlice: +2 new state objects (openJamTrack, jamTrackUI)

**New Thunks:**
1. loadJamTrack (enhanced with sync check and download)
2. downloadJamTrack (download with progress callbacks)
3. checkJamTrackSync (sync state query)
4. loadJMEP (tempo/pitch modifications)
5. seekJamTrack (seek with state update)
6. closeJamTrack (cleanup)

**WebSocket Handlers:**
1. JAM_TRACK_CHANGES (real-time playback state)
2. MIXDOWN_CHANGES (packaging progress)

**State Machine:**
- 6-state download/sync flow (idle → checking → downloading → keying → synchronized → error)
- Timeout handling for each state
- Progress tracking with WebSocket updates

**Critical Fix:**
- loadJamTrack thunk: Use fqId format instead of jamTrack.id

This design provides comprehensive state management for JamTrack functionality while following established Redux patterns from the Backing Track implementation.
