# 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)

```javascript
// 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)

```javascript
// 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)

```javascript
// 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:**

```javascript
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:**
```javascript
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:**
```javascript
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:**
```javascript
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

```jsx
// 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)

```javascript
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

```javascript
// 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

```javascript
// 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

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

### Consecutive Error Tracking

```javascript
// 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

```javascript
// 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

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

### Cleanup Pattern

```javascript
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 (`JamTrackDownload` → `JamTrackKeysRequest`) | 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)
