feat(05-05): add comprehensive error handling to JamTrack player

- Add 5 error types (file, network, download, playback, general)
- Implement color-coded error display (red for critical, yellow for warnings)
- Add retry button for recoverable errors (network, download, file)
- Implement handleRetryError with type-specific retry logic
- Add network resilience: stop polling after 3 consecutive failures
- Add edge case validation: null jamClient, invalid jamTrack data
- User-friendly error messages with dismiss functionality
- Match Phase 3 Backing Track error handling patterns

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-15 01:09:55 +05:30
parent 90aa66bdb6
commit eee95fe312
1 changed files with 77 additions and 14 deletions

View File

@ -10,6 +10,15 @@ import {
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';
import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice';
// Error types for comprehensive error handling
const ERROR_TYPES = {
FILE: 'file', // Red - critical
NETWORK: 'network', // Red - critical
DOWNLOAD: 'download', // Red - critical
PLAYBACK: 'playback', // Yellow - warning
GENERAL: 'general' // Yellow - warning
};
const JKSessionJamTrackPlayer = ({
isOpen,
onClose,
@ -37,6 +46,7 @@ const JKSessionJamTrackPlayer = ({
const fqIdRef = useRef(null);
const mountedRef = useRef(true);
const pendingSeekRef = useRef(null);
const consecutiveFailuresRef = useRef(0);
// Helper: Build fqId
const buildFqId = useCallback(async () => {
@ -55,6 +65,18 @@ const JKSessionJamTrackPlayer = ({
setIsLoadingSync(true);
setError(null);
// Edge case validation: null jamClient
if (!jamClient) {
setError({ type: ERROR_TYPES.GENERAL, message: 'Native client not available. Please ensure JamKazam is running.' });
return;
}
// Edge case validation: invalid jamTrack
if (!jamTrack || !jamTrack.id) {
setError({ type: ERROR_TYPES.FILE, message: 'Invalid JamTrack data' });
return;
}
// Build fqId
const fqId = await buildFqId();
fqIdRef.current = fqId;
@ -63,8 +85,7 @@ const JKSessionJamTrackPlayer = ({
const syncResult = await dispatch(checkJamTrackSync({ jamTrack, jamClient })).unwrap();
if (!syncResult.isSynchronized) {
// Download flow will be triggered by loadJamTrack
console.log('[JamTrack] Not synchronized, will download');
// Download flow will be triggered by loadJamTrack (no diagnostic logging)
}
// Fetch available mixdowns
@ -116,7 +137,7 @@ const JKSessionJamTrackPlayer = ({
} catch (err) {
console.error('[JamTrack] Initialization error:', err);
setError({ type: 'initialization', message: err.message || 'Failed to initialize JamTrack' });
setError({ type: ERROR_TYPES.GENERAL, message: err.message || 'Failed to initialize JamTrack' });
} finally {
if (mountedRef.current) {
setIsLoadingSync(false);
@ -169,7 +190,7 @@ const JKSessionJamTrackPlayer = ({
}
} catch (err) {
console.error('[JamTrack] Play error:', err);
setError({ type: 'playback', message: 'Failed to play JamTrack' });
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to play JamTrack' });
} finally {
if (mountedRef.current) {
setIsOperating(false);
@ -188,7 +209,7 @@ const JKSessionJamTrackPlayer = ({
dispatch(setJamTrackState({ isPaused: true, isPlaying: false }));
} catch (err) {
console.error('[JamTrack] Pause error:', err);
setError({ type: 'playback', message: 'Failed to pause JamTrack' });
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to pause JamTrack' });
} finally {
if (mountedRef.current) {
setIsOperating(false);
@ -211,7 +232,7 @@ const JKSessionJamTrackPlayer = ({
}));
} catch (err) {
console.error('[JamTrack] Stop error:', err);
setError({ type: 'playback', message: 'Failed to stop JamTrack' });
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to stop JamTrack' });
} finally {
if (mountedRef.current) {
setIsOperating(false);
@ -238,7 +259,7 @@ const JKSessionJamTrackPlayer = ({
dispatch(setJamTrackState({ currentPositionMs: newPositionMs }));
} catch (err) {
console.error('[JamTrack] Seek error:', err);
setError({ type: 'playback', message: 'Failed to seek' });
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to seek' });
}
}, [isOperating, jamClient, jamTrackState.isPaused, dispatch]);
@ -274,7 +295,7 @@ const JKSessionJamTrackPlayer = ({
} catch (err) {
console.error('[JamTrack] Mixdown change error:', err);
setError({ type: 'playback', message: 'Failed to change mixdown' });
setError({ type: ERROR_TYPES.PLAYBACK, message: 'Failed to change mixdown' });
} finally {
if (mountedRef.current) {
setIsOperating(false);
@ -291,7 +312,7 @@ const JKSessionJamTrackPlayer = ({
dispatch(setDownloadState({ state: 'idle', progress: 0 }));
} catch (err) {
console.error('[JamTrack] Cancel download error:', err);
setError({ type: 'download', message: 'Failed to cancel download' });
setError({ type: ERROR_TYPES.DOWNLOAD, message: 'Failed to cancel download' });
}
}, [jamClient, dispatch]);
@ -316,7 +337,7 @@ const JKSessionJamTrackPlayer = ({
} catch (err) {
console.error('[JamTrack] Retry download error:', err);
setError({ type: 'download', message: 'Retry failed' });
setError({ type: ERROR_TYPES.DOWNLOAD, message: 'Retry failed' });
} finally {
if (mountedRef.current) {
setIsOperating(false);
@ -324,6 +345,25 @@ const JKSessionJamTrackPlayer = ({
}
}, [isOperating, jamClient, jamTrack, selectedMixdownId, dispatch]);
// Error retry handler
const handleRetryError = useCallback(async () => {
if (!error || isOperating) return;
setError(null);
// Retry based on error type
if (error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) {
await handleRetryDownload();
} else if (error.type === ERROR_TYPES.NETWORK) {
// Re-initialize player
if (jamTrack && jamClient) {
const fqId = await buildFqId();
fqIdRef.current = fqId;
await dispatch(loadJamTrack({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, jamClient }));
}
}
}, [error, isOperating, jamTrack, jamClient, selectedMixdownId, handleRetryDownload, buildFqId, dispatch]);
// Helper: Format milliseconds to MM:SS
const formatTime = (ms) => {
if (!ms || isNaN(ms)) return '00:00';
@ -371,9 +411,19 @@ const JKSessionJamTrackPlayer = ({
currentPositionMs: 0
}));
}
// Reset error counter on success
consecutiveFailuresRef.current = 0;
} catch (err) {
console.error('[JamTrack] Polling error:', err);
// Don't setError - polling errors are non-critical
consecutiveFailuresRef.current += 1;
// After 3 consecutive failures, show error and stop polling
if (consecutiveFailuresRef.current >= 3) {
setError({ type: ERROR_TYPES.NETWORK, message: 'Lost connection to native client. Check if JamKazam is running.' });
// Stop polling by setting isPlaying to false
dispatch(setJamTrackState({ isPlaying: false }));
}
}
}, pollInterval);
@ -396,9 +446,22 @@ const JKSessionJamTrackPlayer = ({
return (
<div className="jamtrack-player">
{error && (
<div style={{ background: error.type === 'file' || error.type === 'network' ? '#fee' : '#ffd', padding: '10px' }}>
{error.message}
<button onClick={() => setError(null)}>Dismiss</button>
<div
style={{
background: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD) ? '#fee' : '#ffd',
padding: '10px',
marginBottom: '10px',
border: '1px solid',
borderColor: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD) ? '#fcc' : '#fc6'
}}
>
<strong>{error.type.toUpperCase()} ERROR:</strong> {error.message}
<div style={{ marginTop: '5px' }}>
<button onClick={() => setError(null)}>Dismiss</button>
{(error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) && (
<button onClick={handleRetryError} style={{ marginLeft: '5px' }}>Retry</button>
)}
</div>
</div>
)}