jam-cloud/.planning/research/JAMTRACK-LOADING.md

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:

  1. Server queues mixdown for packaging
  2. WebSocket subscription created for progress notifications (line 157-166)
  3. Polling loop waits for signing_state === 'SIGNED' (line 179-221)
  4. 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:

  1. Native client initiates download with progress callbacks
  2. Progress updates via window.jamTrackDownloadProgress (line 234-236)
  3. Success triggers window.jamTrackDownloadSuccess (line 238-240)
  4. 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:

  1. 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.

  2. 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".

  3. 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 isOperating flag)
    • If process fails or times out, UI is stuck
  4. WindowPortal sizing issue: Player window is hardcoded to width=600,height=500 (line 1750), but player content width is 420px (line 487 in JKSessionJamTrackPlayer.js), causing poor fit.

Strategy: Backend-First Loading

Goal: Don't show UI until backend is ready.

Phase 1: Defer UI Rendering

Change handleJamTrackSelect() to:

  1. Fetch JamTrack metadata (as now)
  2. Show loading indicator in modal/toast
  3. DON'T dispatch setSelectedJamTrack yet
  4. Trigger loadJamTrack() thunk (which includes sync check + download if needed)
  5. Wait for loadJamTrack() to complete
  6. THEN dispatch UI state: setSelectedJamTrack, setJamTrackStems, setJamTrackData
  7. 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)

  1. Select JamTrack never downloaded before
  2. Expected: Modal shows loading, then closes when ready
  3. Expected: Stems + player appear simultaneously
  4. Expected: Play button works immediately without "checking sync status"

Test 2: Already Synchronized JamTrack

  1. Select JamTrack already downloaded
  2. Expected: Fast load (no packaging/downloading)
  3. Expected: Stems + player appear quickly
  4. Expected: Play button works immediately

Test 3: Packaging Failure

  1. Mock server error during packaging
  2. Expected: Error toast shown
  3. Expected: UI does NOT show stems/player (stays clean)
  4. Expected: User can retry or select different track

Test 4: WindowPortal Sizing

  1. Open JamTrack player
  2. Expected: Player fits properly in window (no excessive white space)
  3. Expected: Controls are visible without scrolling
  1. Open JamTrack player
  2. Click "create custom mix"
  3. Expected: New tab opens to /jamtracks/{id}
  4. Expected: User can create custom mixdown

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:

  1. HIGH: Defer UI rendering until backend ready (Phase 1)
  2. HIGH: Fix conditional rendering logic (Phase 2)
  3. MEDIUM: Fix WindowPortal sizing (Phase 3)
  4. 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