docs(04-01): document React integration patterns for JamTrack

- Mapped existing components (JKSessionJamTrackModal, stems, player)
- Documented Redux state structure (mediaSlice, activeSessionSlice, sessionUISlice)
- Captured hook patterns (useMediaActions, useSessionWebSocket)
- Illustrated complete data flow from modal → download → playback
- Compared JamTrack vs Backing Track patterns with gap analysis
- Applied Phase 3 performance patterns (visibility-aware polling, useCallback)
- Identified 20+ gaps for Phase 5 implementation
This commit is contained in:
Nuwan 2026-01-14 21:22:59 +05:30
parent d3ff1f1477
commit d402b22fc6
1 changed files with 557 additions and 0 deletions

View File

@ -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 && (
<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)