diff --git a/.planning/codebase/JAMTRACK_REACT_PATTERNS.md b/.planning/codebase/JAMTRACK_REACT_PATTERNS.md new file mode 100644 index 000000000..2b435ee8d --- /dev/null +++ b/.planning/codebase/JAMTRACK_REACT_PATTERNS.md @@ -0,0 +1,557 @@ +# 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 && ( +
+ ⚠️ + {error.message} +
+ + {error.canRetry && } +
+
+)} +``` + +### 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)