13 KiB
JamTrack Loading Sequence and Freeze Issue - Research
Project: jam-cloud (JamKazam) Researched: 2026-02-25 Confidence: HIGH
Executive Summary
The JamTrack freeze issue is caused by premature UI rendering - stems appear in the session screen immediately upon selection, but the backend hasn't finished creating/downloading/syncing the track files. When the user clicks play before backend processes complete, the system enters a "checking sync status" state that blocks the UI.
Root cause: The current flow triggers UI updates (setSelectedJamTrack, setJamTrackStems) immediately when the JamTrack is selected, but the actual backend processing (packaging, downloading, keying) happens asynchronously during the first play attempt in JKSessionJamTrackPlayer.
Correct sequence should be: Select → Backend processing (hidden) → Show UI (stems + player) → Ready to play
Problem Flow (Current - Broken)
1. User selects JamTrack from modal
↓
2. handleJamTrackSelect() in JKSessionScreen.js (line 1165-1190)
- Fetches JamTrack data via REST API
- IMMEDIATELY dispatches: setSelectedJamTrack(), setJamTrackStems(), setJamTrackData()
↓
3. UI updates IMMEDIATELY (line 1405-1431)
- Stems appear in session screen
- WindowPortal opens with player
↓
4. User clicks Play button
↓
5. JKSessionJamTrackPlayer handlePlay() (line 175-211)
- Calls loadJamTrack thunk
↓
6. mediaSlice loadJamTrack() (line 17-49)
- Checks if synchronized
- If NOT synchronized → triggers downloadJamTrack()
- Shows "checking sync status..." (line 602)
↓
7. mediaSlice downloadJamTrack() (line 51-266)
- PACKAGING phase (line 123-223): enqueue mixdown, wait for server signing
- DOWNLOADING phase (line 225-255): download via native client
- KEYING phase (line 256-258): request decryption keys
↓
8. FREEZE occurs if:
- Packaging takes too long (60s timeout)
- Download fails
- Keys not available
- User interacts during process
File Locations
Core Components
| File | Purpose | Lines of Interest |
|---|---|---|
/jam-ui/src/components/client/JKSessionScreen.js |
Main session screen orchestrator | 1165-1190 (handleJamTrackSelect), 1405-1431 (stems rendering), 1746-1761 (WindowPortal) |
/jam-ui/src/components/client/JKSessionJamTrackPlayer.js |
JamTrack player component (WindowPortal content) | 69-160 (initialization), 175-211 (handlePlay), 602 ("checking sync status" text) |
/jam-ui/src/components/client/JKSessionJamTrackStems.js |
Renders stems in session screen | 5-49 (stem track mapping) |
/jam-ui/src/components/client/JKSessionJamTrackModal.js |
JamTrack selection modal | 44-52 (onSelect callback) |
Redux State Management
| File | Purpose | Lines of Interest |
|---|---|---|
/jam-ui/src/store/features/mediaSlice.js |
Media operations (load, download, sync) | 17-49 (loadJamTrack), 51-266 (downloadJamTrack), 268-280 (checkJamTrackSync) |
/jam-ui/src/store/features/activeSessionSlice.js |
Session data (selected track, stems) | 172-178 (setSelectedJamTrack, setJamTrackStems), 211-217 (jamTrackData for player) |
/jam-ui/src/store/features/sessionUISlice.js |
UI state (modal visibility) | 51 (openJamTrack state) |
Infrastructure
| File | Purpose | Lines of Interest |
|---|---|---|
/jam-ui/src/components/common/WindowPortal.js |
Popup window manager | 7 (windowFeatures for sizing) |
/jam-ui/src/helpers/rest.js |
API endpoints | 531-538 (getJamTrack), 935-945 (closeJamTrack) |
Backend API Calls and Timing
1. Initial Selection (REST API - synchronous)
Endpoint: GET /jamtracks/:id
Called by: handleJamTrackSelect() in JKSessionScreen.js (line 1169)
Purpose: Fetch JamTrack metadata with stems/mixdowns
Timing: ~100-500ms
Response includes:
- JamTrack metadata (id, name, artist, etc.)
- Mixdowns array (master, custom-mix, stem types)
- Each mixdown has packages array (file_type, encrypt_type, sample_rate)
- Tracks array (stems with instruments)
2. Packaging Phase (REST + WebSocket - asynchronous)
Endpoint: POST /jam_track_mixdowns/:id/enqueue
Called by: downloadJamTrack() in mediaSlice.js (line 143-148)
Purpose: Request server-side package creation/signing
Timing: 5-30 seconds (can timeout at 60s)
Process:
- Server queues mixdown for packaging
- WebSocket subscription created for progress notifications (line 157-166)
- Polling loop waits for
signing_state === 'SIGNED'(line 179-221) - State updates shown as "Your JamTrack is currently being created" (line 603)
Failure modes:
SIGNING_TIMEOUT,QUEUED_TIMEOUT,QUIET_TIMEOUT→ error- WebSocket not available → error (line 159-161)
3. Download Phase (Native Client - asynchronous)
Method: jamClient.JamTrackDownload()
Called by: downloadJamTrack() in mediaSlice.js (line 247-254)
Purpose: Download encrypted audio files to local storage
Timing: 10-60 seconds (depends on file size, network)
Process:
- Native client initiates download with progress callbacks
- Progress updates via
window.jamTrackDownloadProgress(line 234-236) - Success triggers
window.jamTrackDownloadSuccess(line 238-240) - Failure triggers
window.jamTrackDownloadFail(line 242-244)
4. Keying Phase (Native Client - synchronous)
Method: jamClient.JamTrackKeysRequest()
Called by: downloadJamTrack() in mediaSlice.js (line 257)
Purpose: Request decryption keys for encrypted files
Timing: ~1-5 seconds
State shown: "Requesting decryption keys..." (line 605)
5. Play Phase (Native Client - synchronous)
Method: jamClient.JamTrackPlay(fqId)
Called by: loadJamTrack() in mediaSlice.js (line 39-42)
Purpose: Start audio playback
Timing: ~100-500ms
Requires: Files synchronized (trackDetail.key_state === 'AVAILABLE')
Root Cause of Freeze
The freeze occurs because:
-
Immediate UI update (line 1175-1176 in JKSessionScreen.js):
dispatch(setSelectedJamTrack(jamTrackWithStems)); dispatch(setJamTrackStems(jamTrackWithStems.tracks || []));This causes stems to render immediately at line 1405-1431.
-
Conditional rendering check is wrong (line 1406):
{selectedJamTrack && jamTrackStems.length > 0 && (jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle')The condition allows rendering when
state === 'idle', but "idle" means not yet checked, not "ready to play". -
Play button triggers long async process (line 183-190): When user clicks play, if track isn't synchronized, it triggers packaging → downloading → keying, which can take 30-90 seconds. During this time:
- UI shows "checking sync status..." (line 602)
- User can't interact (buttons disabled via
isOperatingflag) - If process fails or times out, UI is stuck
-
WindowPortal sizing issue: Player window is hardcoded to
width=600,height=500(line 1750), but player content width is420px(line 487 in JKSessionJamTrackPlayer.js), causing poor fit.
Recommended Fix Approach
Strategy: Backend-First Loading
Goal: Don't show UI until backend is ready.
Phase 1: Defer UI Rendering
Change handleJamTrackSelect() to:
- Fetch JamTrack metadata (as now)
- Show loading indicator in modal/toast
- DON'T dispatch
setSelectedJamTrackyet - Trigger
loadJamTrack()thunk (which includes sync check + download if needed) - Wait for
loadJamTrack()to complete - THEN dispatch UI state:
setSelectedJamTrack,setJamTrackStems,setJamTrackData - Close modal, show player + stems
Code change location: JKSessionScreen.js lines 1165-1190
const handleJamTrackSelect = async (jamTrack) => {
try {
// 1. Fetch metadata
const response = await getJamTrack({ id: jamTrack.id });
const jamTrackWithStems = await response.json();
// 2. Show loading
toast.info('Preparing JamTrack...');
// 3. Pre-load backend (sync check + download if needed)
await dispatch(loadJamTrack({
jamTrack: jamTrackWithStems,
mixdownId: null, // Use default
autoPlay: false, // Don't auto-play, just prepare
jamClient,
jamServer
})).unwrap();
// 4. NOW show UI (backend is ready)
dispatch(setSelectedJamTrack(jamTrackWithStems));
dispatch(setJamTrackStems(jamTrackWithStems.tracks || []));
dispatch(setJamTrackData({
jamTrack: jamTrackWithStems,
session: currentSession,
currentUser: currentUser
}));
toast.success(`Loaded JamTrack: ${jamTrackWithStems.name}`);
} catch (error) {
toast.error(`Failed to load JamTrack: ${error.message}`);
}
};
Phase 2: Fix Conditional Rendering
Change line 1406 in JKSessionScreen.js:
// OLD (wrong):
{selectedJamTrack && jamTrackStems.length > 0 &&
(jamTrackDownloadState.state === 'synchronized' || jamTrackDownloadState.state === 'idle')
// NEW (correct):
{selectedJamTrack && jamTrackStems.length > 0 &&
jamTrackDownloadState.state === 'synchronized'
Remove the || jamTrackDownloadState.state === 'idle' condition. UI should only show when explicitly synchronized.
Phase 3: Fix WindowPortal Sizing
Change line 1750 in JKSessionScreen.js:
// OLD:
windowFeatures="width=600,height=500,left=200,top=200,..."
// NEW (matches player content width):
windowFeatures="width=460,height=350,left=200,top=200,..."
Player content is 420px wide (line 487 in JKSessionJamTrackPlayer.js), so 460px window width provides proper padding.
Phase 4: Progress Feedback During Prep
While backend is processing (packaging/downloading), show progress in the modal or as a toast:
Option A: Modal shows progress
- Keep modal open during prep
- Show packaging/downloading progress bars
- Close modal when ready
Option B: Toast notifications
- Close modal immediately
- Show toast: "Preparing JamTrack... (packaging)"
- Update toast: "Preparing JamTrack... (downloading 45%)"
- Final toast: "JamTrack ready!" → open player + stems
"Create Custom Mix" Navigation
Current Implementation
Location: JKSessionJamTrackPlayer.js line 860 Current code:
<a onClick={() => console.log('TODO: Open custom mix creator')}>
create custom mix
</a>
Required Behavior
Should open /jamtracks/{id} in a new browser tab.
Implementation:
<a
onClick={() => {
const url = `/jamtracks/${jamTrack.id}`;
window.open(url, '_blank', 'noopener,noreferrer');
}}
style={{
fontSize: '12px',
color: '#007aff',
textDecoration: 'none',
cursor: 'pointer'
}}
>
create custom mix
</a>
Route: Already exists in routes.js line 36 (/jamtracks)
Component: Should navigate to JamTrack show page where user can create custom mixdowns (existing functionality).
Testing Strategy
Test 1: Fresh JamTrack (Not Synchronized)
- Select JamTrack never downloaded before
- Expected: Modal shows loading, then closes when ready
- Expected: Stems + player appear simultaneously
- Expected: Play button works immediately without "checking sync status"
Test 2: Already Synchronized JamTrack
- Select JamTrack already downloaded
- Expected: Fast load (no packaging/downloading)
- Expected: Stems + player appear quickly
- Expected: Play button works immediately
Test 3: Packaging Failure
- Mock server error during packaging
- Expected: Error toast shown
- Expected: UI does NOT show stems/player (stays clean)
- Expected: User can retry or select different track
Test 4: WindowPortal Sizing
- Open JamTrack player
- Expected: Player fits properly in window (no excessive white space)
- Expected: Controls are visible without scrolling
Test 5: Create Custom Mix Link
- Open JamTrack player
- Click "create custom mix"
- Expected: New tab opens to
/jamtracks/{id} - Expected: User can create custom mixdown
Related Issues
WebSocket Subscription Management
File: mediaSlice.js lines 157-173 Issue: Subscription/unsubscription for packaging progress must be robust Current: Uses polling + timeout Risk: Memory leaks if unsubscribe not called on error/timeout
Native Client Callback Management
File: mediaSlice.js lines 234-244
Issue: Global callbacks (window.jamTrackDownloadProgress) are fragile
Risk: Multiple JamTracks could conflict if callbacks overwrite each other
Error Recovery
Current: User must close player and retry Better: Provide "Retry" button in error state (already exists in player, line 363-379)
Summary
The freeze is NOT a native client bug. It's a UI state management issue where the UI renders before the backend is ready.
Fix priority:
- HIGH: Defer UI rendering until backend ready (Phase 1)
- HIGH: Fix conditional rendering logic (Phase 2)
- MEDIUM: Fix WindowPortal sizing (Phase 3)
- LOW: Add "create custom mix" navigation (it's a TODO, not a bug)
Estimated effort:
- Phase 1: 2-3 hours (requires Redux thunk orchestration)
- Phase 2: 5 minutes (one line change)
- Phase 3: 5 minutes (one line change)
- Phase 4: 30 minutes (optional progress UI)
- Create custom mix: 5 minutes (one line change)
Total: 3-4 hours to fix freeze + sizing issues