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 fileloadJamTrack({ 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:
jamTrackStateneeds expansion for player state (playing, position, duration, error, isLoading)- Missing sync/download state tracking (packaging, downloading, keying, synchronized)
- Missing selected mixdown state
loadJamTrackthunk 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 JamTracksetJamTrackStems(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 modalsetShowMyMixes(boolean)- Show/hide user mixessetEditingMixdownId(id)- Set mixdown being editedsetCreatingMixdown(boolean)- Toggle mixdown creation mode
Identified gaps for Phase 5:
- Missing
openJamTrackequivalent toopenBackingTrackfor 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:
- jamClient returns Promises - Always use
async/await - jamClient returns strings - Always
parseInt()before math operations - Non-serializable objects - Pass
jamClientas prop, NOT stored in Redux - 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 (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.jamTrackStatefor player state (position, duration, isPlaying, error, isLoading) - Add download/sync state tracking (packaging, downloading, keying, synchronized)
- Add selected mixdown state to
activeSessionSlice - Fix
loadJamTrackthunk to use correct fqId format (not just jamTrack.id) - Add
openJamTracktosessionUISlicefor popup/modal mode support
Component Gaps
- Create
JKSessionJamTrackPlayercomponent (main player with playback controls) - Create or integrate download/sync progress widget (state machine visualization)
- Verify
JKSessionJamTrackStemscompleteness for stem mixer controls - Add mixdown selection dropdown to player UI
Hook Gaps
- Create
useJamTrackhook for selectors and mixdown operations - Add JAM_TRACK_CHANGES handling to
useSessionWebSocket - Add MIXDOWN_CHANGES handling for packaging progress
- Enhance
useMediaActionswith 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 worksuseMediaActions- 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:
- Create
JKSessionJamTrackPlayercomponent (similar to Backing Track) - Implement download/sync state machine in React
- Expand Redux state for JamTrack player and sync tracking
- Add WebSocket handlers for JAM_TRACK_CHANGES and MIXDOWN_CHANGES
- Implement mixdown selection and CRUD operations
- Fix
loadJamTrackthunk 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)