feat(03-02): add comprehensive error handling with user feedback

Implemented error state management and user-facing error messages:

Error handling utilities:
- handleError(type, message, err) - Sets error state with type classification
- clearError() - Clears error state
- retryOperation() - Placeholder for retry logic

Error types:
- 'file' - Failed to load track duration, invalid files
- 'network' - Lost connection after 3 consecutive polling failures
- 'playback' - Failed playback operations (play/stop/seek/volume/loop)
- 'general' - Audio engine not available (null jamClient)

Enhanced error handling in:
- fetchDuration: Checks for jamClient, validates duration, handles failures
- Polling interval: Tracks consecutive errors, stops after 3 failures
- handlePlay: Checks jamClient, reverts isPlaying on error
- handleStop: Checks jamClient, force resets UI on error
- handleVolumeChange: Reverts volume slider on error
- handleLoopChange: Reverts checkbox on error
- handleSeek: Reverts position on error

Error UI:
- Error banner with color coding (red for critical, yellow for warning)
- Shows error message with dismiss button
- Retry button for network/file errors
- Added to both popup and modal versions
This commit is contained in:
Nuwan 2026-01-14 15:52:47 +05:30
parent fcf6afb7be
commit acbddd35e3
1 changed files with 158 additions and 8 deletions

View File

@ -26,6 +26,10 @@ const JKSessionBackingTrackPlayer = ({
const [currentPositionMs, setCurrentPositionMs] = useState(0);
const [durationMs, setDurationMs] = useState(0);
// Error handling state
const [error, setError] = useState(null);
const [errorType, setErrorType] = useState(null); // 'file', 'network', 'playback', 'general'
const volumeRef = useRef(null);
const trackVolumeObjectRef = useRef({
volL: 0,
@ -39,6 +43,9 @@ const JKSessionBackingTrackPlayer = ({
// UAT-003 Investigation: Track seek attempts while paused
const pendingSeekPositionRef = useRef(null);
// Error tracking for polling
const consecutivePollingErrorsRef = useRef(0);
// Utility function to format milliseconds to M:SS format
const formatTime = (ms) => {
// Handle undefined, null, or NaN values
@ -51,6 +58,23 @@ const JKSessionBackingTrackPlayer = ({
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
// Error handling utilities
const handleError = (type, message, err) => {
console.error(`[BTP] ${type}:`, message, err);
setError(message);
setErrorType(type);
};
const clearError = () => {
setError(null);
setErrorType(null);
};
const retryOperation = () => {
clearError();
// Specific retry logic handled by individual operations
};
useEffect(() => {
if (isOpen && backingTrack && jamClient) {
// Initialize player state when opened
@ -61,15 +85,30 @@ const JKSessionBackingTrackPlayer = ({
// Fetch and set duration immediately when track loads (async)
const fetchDuration = async () => {
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
return;
}
const durationInMs = await jamClient.SessionGetTracksPlayDurationMs();
console.log('[BTP] Duration from jamClient:', durationInMs, 'Type:', typeof durationInMs);
// Convert string to number (jamClient returns string values)
const validDuration = parseInt(durationInMs, 10) || 0;
// Check for invalid duration (file error)
if (validDuration === 0 || isNaN(validDuration)) {
handleError('file', 'Failed to load track duration. The file may be invalid or unsupported.', null);
setDuration('0:00');
setDurationMs(0);
return;
}
setDurationMs(validDuration);
setDuration(formatTime(validDuration));
clearError(); // Clear any previous errors
} catch (error) {
console.error('Error fetching track duration:', error);
handleError('file', 'Failed to load track duration', error);
setDuration('0:00');
setDurationMs(0);
}
@ -116,8 +155,21 @@ const JKSessionBackingTrackPlayer = ({
console.log('[BTP] Playing state changed from', isPlaying, 'to', trackIsPlaying);
setIsPlaying(trackIsPlaying);
}
// Reset error counter on successful poll
consecutivePollingErrorsRef.current = 0;
} catch (error) {
console.error('Error polling playback status:', error);
console.error('[BTP] Error polling playback status:', error);
consecutivePollingErrorsRef.current += 1;
// After 3 consecutive failures, show error and stop polling
if (consecutivePollingErrorsRef.current >= 3) {
handleError('network', 'Lost connection to audio engine', error);
setIsPlaying(false);
if (intervalId) {
clearInterval(intervalId);
}
}
}
}, 500);
}
@ -132,6 +184,11 @@ const JKSessionBackingTrackPlayer = ({
const handlePlay = async () => {
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
return;
}
console.log('[BTP] handlePlay:', {
isPlaying,
currentPositionMs,
@ -139,6 +196,8 @@ const JKSessionBackingTrackPlayer = ({
hasPendingSeek: pendingSeekPositionRef.current !== null
});
clearError(); // Clear any previous errors
if (isPlaying) {
// Pause
await jamClient.SessionPausePlay();
@ -186,12 +245,19 @@ const JKSessionBackingTrackPlayer = ({
console.log('[BTP] SessionStartPlay completed, isPlaying set to true');
}
} catch (error) {
console.error('Error toggling playback:', error);
handleError('playback', 'Failed to start playback', error);
// Reset isPlaying to ensure UI is consistent
setIsPlaying(false);
}
};
const handleStop = async () => {
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
return;
}
await jamClient.SessionStopPlay();
setIsPlaying(false);
setCurrentTime('0:00');
@ -199,16 +265,29 @@ const JKSessionBackingTrackPlayer = ({
// UAT-003: Clear any pending seek position when stopping
pendingSeekPositionRef.current = null;
clearError(); // Clear any previous errors
} catch (error) {
console.error('Error stopping playback:', error);
handleError('playback', 'Failed to stop playback', error);
// Force reset UI state anyway
setIsPlaying(false);
setCurrentTime('0:00');
setCurrentPositionMs(0);
pendingSeekPositionRef.current = null;
}
};
const handleVolumeChange = async (e) => {
const newVolume = parseInt(e.target.value);
const previousVolume = volume;
setVolume(newVolume);
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
setVolume(previousVolume); // Revert to previous
return;
}
// Volume is controlled through the mixer system
// Get the backing track mixer (use personal mixer)
const backingTrackMixer = backingTrackMixers[0]?.mixers?.personal?.mixer;
@ -240,26 +319,36 @@ const JKSessionBackingTrackPlayer = ({
console.log('[BTP] Volume changed to', newVolume, '% (', volumeDb, 'dB)');
} catch (error) {
console.error('[BTP] Error setting volume:', error);
handleError('playback', 'Failed to adjust volume', error);
setVolume(previousVolume); // Revert to previous on error
}
};
const handleLoopChange = async (e) => {
const shouldLoop = e.target.checked;
const previousLoop = isLooping;
setIsLooping(shouldLoop);
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
setIsLooping(previousLoop); // Revert checkbox
return;
}
// Set loop state using SessionSetBackingTrackFileLoop
// This requires the file path and loop boolean
if (!backingTrack || !backingTrack.path) {
console.warn('[BTP] No backing track path available for loop change');
handleError('playback', 'No backing track available for loop change', null);
setIsLooping(previousLoop); // Revert checkbox
return;
}
await jamClient.SessionSetBackingTrackFileLoop(backingTrack.path, shouldLoop);
console.log('[BTP] Loop changed to', shouldLoop);
} catch (error) {
console.error('[BTP] Error setting loop state:', error);
handleError('playback', 'Failed to toggle loop', error);
setIsLooping(previousLoop); // Revert checkbox on error
}
};
@ -280,8 +369,14 @@ const JKSessionBackingTrackPlayer = ({
*/
const handleSeek = async (e) => {
const seekPositionMs = parseInt(e.target.value);
const previousPosition = currentPositionMs;
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
return;
}
console.log('[BTP] handleSeek:', { seekPositionMs, isPlaying });
// Update local state immediately for responsive UI
@ -305,7 +400,10 @@ const JKSessionBackingTrackPlayer = ({
console.log('[BTP] Seek completed');
} catch (error) {
console.error('[BTP] Error seeking:', error);
handleError('playback', 'Failed to seek to position', error);
// Revert UI position to last known good
setCurrentPositionMs(previousPosition);
setCurrentTime(formatTime(previousPosition));
}
};
@ -392,6 +490,32 @@ const JKSessionBackingTrackPlayer = ({
<span>{getFileName(backingTrack)}</span>
</div>
{/* Error Display */}
{error && (
<div style={{
padding: '12px',
backgroundColor: errorType === 'file' || errorType === 'network' ? '#f8d7da' : '#fff3cd',
color: errorType === 'file' || errorType === 'network' ? '#721c24' : '#856404',
border: `1px solid ${errorType === 'file' || errorType === 'network' ? '#f5c6cb' : '#ffeaa7'}`,
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}>
<div>{error}</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button size="sm" color="secondary" onClick={clearError}>
Dismiss
</Button>
{(errorType === 'network' || errorType === 'file') && (
<Button size="sm" color="primary" onClick={retryOperation}>
Retry
</Button>
)}
</div>
</div>
)}
{/* Row 2: Controls */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
@ -487,6 +611,32 @@ const JKSessionBackingTrackPlayer = ({
<span>{getFileName(backingTrack)}</span>
</div>
{/* Error Display */}
{error && (
<div style={{
padding: '12px',
backgroundColor: errorType === 'file' || errorType === 'network' ? '#f8d7da' : '#fff3cd',
color: errorType === 'file' || errorType === 'network' ? '#721c24' : '#856404',
border: `1px solid ${errorType === 'file' || errorType === 'network' ? '#f5c6cb' : '#ffeaa7'}`,
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}>
<div>{error}</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button size="sm" color="secondary" onClick={clearError}>
Dismiss
</Button>
{(errorType === 'network' || errorType === 'file') && (
<Button size="sm" color="primary" onClick={retryOperation}>
Retry
</Button>
)}
</div>
</div>
)}
{/* Row 2: Controls */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>