18 KiB
Track Configuration API Implementation Plan
Feature: PUT /api/sessions/{id}/tracks - Track Configuration Date: 2026-01-21 Status: Planning Phase
1. Backend API Specification (From Rails Analysis)
Endpoint
PUT /api/sessions/{session_id}/tracks
Purpose
Synchronizes participant's audio track configuration with the server. Updates:
- Active audio tracks (instrument, sound settings)
- Backing tracks
- Metronome open/closed state
Request Payload (Expected Format)
{
"client_id": "client-uuid",
"tracks": [
{
"instrument_id": "electric guitar",
"sound": "stereo",
"client_track_id": "client-track-guid",
"client_resource_id": "resource-guid"
}
],
"backing_tracks": [
{
"filename": "backing-track.mp3",
"client_track_id": "backing-track-guid",
"client_resource_id": "backing-resource-guid"
}
],
"metronome_open": false
}
Response
{
"tracks": [...],
"backing_tracks": [...]
}
Backend Behavior (Rails)
- Updates
Trackrecords in database - Updates
BackingTrackrecords - Updates
Connection.metronome_openflag - Updates
MusicSessionUserHistory.instruments - Sends WebSocket notification to other participants:
tracks_changed - Returns updated track list
When Called in Legacy
- First call: ~1 second after joining session (initial track setup)
- Second call: ~400ms later (track refinement)
- Third call: ~5 seconds later (final configuration)
- Subsequent calls: When user changes instruments, mixer settings, or opens/closes media
2. Current jam-ui State (What Exists)
✅ API Client Already Exists!
File: src/helpers/rest.js:887-897
export const putTrackSyncChange = (options = {}) => {
const { id, ...rest } = options;
return new Promise((resolve, reject) => {
apiFetch(`/sessions/${id}/tracks`, {
method: 'PUT',
body: JSON.stringify(rest)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}
Status: ✅ Already implemented, just not called anywhere!
✅ Redux State Management Ready
Session State: src/store/features/activeSessionSlice.js
userTracks- Participant's tracks in sessionbackingTrackData- Backing track statejamTrackData- Jam track state- Actions:
setUserTracks(),setBackingTrackData(),setJamTrackData()
Mixer State: src/store/features/mixersSlice.js
- Organized by
group_id(MetronomeGroup: 16, BackingTrackGroup, JamTrackGroup, etc.) - Actions:
setMetronomeTrackMixers(),setBackingTrackMixers(),setJamTrackMixers() - Selectors:
selectUserTracks,selectBackingTracks,selectJamTracks
Metronome State: Recently integrated (commit 4d141c93c)
selectMetronome- Metronome track stateselectMetronomeSettings- Configuration
✅ Components That Use Tracks
-
Session Screen:
src/components/client/JKSessionScreen.js- Main session UI
- Manages track display and media players
-
Track Components:
JKSessionMyTrack.js- User's audio trackJKSessionRemoteTracks.js- Remote participant tracksJKSessionBackingTrackPlayer.js- Backing track playerJKSessionJamTrackPlayer.js- Jam track playerJKSessionMetronomePlayer.js- Metronome player
-
Hooks:
useMixerHelper.js- Mixer management and categorizationuseMediaActions.js- Media control actionsuseTrackHelpers.js- Track information retrieval
3. What Needs to Be Implemented
❌ Missing: Track Sync Logic
Problem: The API wrapper exists but is never called.
Need to implement:
- Track state builder function - Constructs payload from Redux state
- Sync trigger logic - Determines when to call API
- Redux actions - Update state after API success
- Error handling - Handle API failures gracefully
❌ Missing: Integration Points
Where to call the API:
- On session join - Initial track configuration
- On mixer changes - When user adjusts audio settings
- On media open/close - When backing track/jam track/metronome toggled
- On instrument change - When user selects different instrument
4. Implementation Strategy
Phase 1: Create Track Sync Service
New file: src/services/trackSyncService.js
Purpose:
- Build track sync payload from Redux state
- Call
putTrackSyncChange()API - Handle success/error responses
- Dispatch Redux updates
Functions:
// Build payload from current Redux state
export const buildTrackSyncPayload = (state, sessionId) => {
const userTracks = selectUserTracks(state);
const backingTracks = selectBackingTracks(state);
const metronome = selectMetronome(state);
const clientId = selectClientId(state);
return {
client_id: clientId,
tracks: userTracks.map(track => ({
instrument_id: track.instrument_id,
sound: track.sound,
client_track_id: track.client_track_id,
client_resource_id: track.client_resource_id
})),
backing_tracks: backingTracks.map(bt => ({
filename: bt.filename,
client_track_id: bt.client_track_id,
client_resource_id: bt.client_resource_id
})),
metronome_open: metronome?.isOpen || false
};
};
// Sync tracks to server
export const syncTracksToServer = (sessionId) => async (dispatch, getState) => {
const state = getState();
const payload = buildTrackSyncPayload(state, sessionId);
try {
const response = await putTrackSyncChange({ id: sessionId, ...payload });
// Update Redux with server response
dispatch(setUserTracks(response.tracks));
dispatch(setBackingTrackData(response.backing_tracks));
return response;
} catch (error) {
console.error('Track sync failed:', error);
dispatch(showError('Failed to sync tracks'));
throw error;
}
};
Phase 2: Add Sync Triggers
Location: src/hooks/useSessionModel.js or create src/hooks/useTrackSync.js
Trigger points:
- On session join:
// In useSessionModel or JKSessionScreen
useEffect(() => {
if (sessionJoined && clientId) {
// Initial track sync after joining
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 1000);
}
}, [sessionJoined, clientId]);
- On mixer changes:
// In useMixerHelper or mixer components
const handleMixerChange = (trackId, setting, value) => {
// Update local mixer state
updateMixer(trackId, setting, value);
// Debounced sync to server
debouncedTrackSync();
};
- On media toggle:
// In useMediaActions
export const toggleBackingTrack = (trackId, isOpen) => async (dispatch) => {
// Update local state
dispatch(setBackingTrackOpen({ trackId, isOpen }));
// Sync to server
await dispatch(syncTracksToServer(sessionId));
};
- On metronome toggle:
// In metronome components
const handleMetronomeToggle = () => {
dispatch(toggleMetronome());
dispatch(syncTracksToServer(sessionId));
};
Phase 3: Debouncing and Optimization
Problem: Don't want to spam server with requests on every slider move.
Solution: Debounce track sync calls:
// In trackSyncService.js
import { debounce } from 'lodash';
export const createDebouncedTrackSync = (dispatch, sessionId) => {
return debounce(() => {
dispatch(syncTracksToServer(sessionId));
}, 500); // Wait 500ms after last change
};
Usage:
// In component
const debouncedSync = useMemo(
() => createDebouncedTrackSync(dispatch, sessionId),
[dispatch, sessionId]
);
const handleMixerChange = (change) => {
updateLocalState(change);
debouncedSync(); // Will sync 500ms after last change
};
Phase 4: Initial Sync Sequence (Match Legacy)
Goal: Replicate legacy's 3-call pattern on session join.
// In useSessionModel.js or JKSessionScreen.js
useEffect(() => {
if (sessionJoined && clientId) {
// First sync: Initial setup (~1s after join)
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 1000);
// Second sync: Refinement (~1.4s after join)
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 1400);
// Third sync: Final config (~6s after join)
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 6000);
}
}, [sessionJoined, clientId]);
Note: This mimics legacy behavior. May be optimized later to single call once we verify it works.
5. Data Flow Diagram
User Action (e.g., adjust mixer)
↓
Component Handler (e.g., handleMixerChange)
↓
Update Local Redux State (immediate UI feedback)
↓
Debounced Track Sync Trigger
↓
buildTrackSyncPayload(Redux state)
↓
putTrackSyncChange(sessionId, payload) → Rails API
↓
Rails: Update DB + Send WebSocket notification
↓
Response: Updated track data
↓
Update Redux with server response
↓
Components re-render with confirmed state
6. Files to Create/Modify
New Files
-
src/services/trackSyncService.js(~150 lines)- Track sync business logic
- Payload builder
- Redux thunk actions
-
src/hooks/useTrackSync.js(~80 lines)- Hook to provide track sync functionality
- Debouncing logic
- Effect hooks for auto-sync
Modified Files
-
src/components/client/JKSessionScreen.js- Add initial track sync on session join
- Import and use
useTrackSynchook
-
src/hooks/useMixerHelper.js- Add track sync calls when mixer changes
- Debounce sync to avoid spam
-
src/hooks/useMediaActions.js- Add track sync after media toggle
- Sync when backing track/jam track opened/closed
-
src/components/client/JKSessionMetronomePlayer.js- Add track sync when metronome toggled
-
src/store/features/activeSessionSlice.js(maybe)- Add
setTrackSyncStatusaction (syncing, success, error) - Track last sync timestamp
- Add
7. Testing Strategy
Unit Tests
Test file: src/services/trackSyncService.test.js
describe('trackSyncService', () => {
test('buildTrackSyncPayload constructs correct payload', () => {
const mockState = {
activeSession: {
userTracks: [{ instrument_id: 'guitar', sound: 'stereo' }],
backingTrackData: [],
metronome: { isOpen: false }
}
};
const payload = buildTrackSyncPayload(mockState, 'session-123');
expect(payload.tracks).toHaveLength(1);
expect(payload.tracks[0].instrument_id).toBe('guitar');
expect(payload.metronome_open).toBe(false);
});
test('syncTracksToServer calls API and updates Redux', async () => {
// Mock API call
// Mock dispatch
// Assert correct flow
});
});
Integration Tests (Playwright)
Test file: test/track-sync/track-configuration.spec.ts
test.describe('Track Configuration API', () => {
test('syncs tracks when user joins session', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await createAndJoinSession(page);
// Wait for track sync calls
await page.waitForTimeout(7000);
const trackSyncCalls = apiInterceptor.getCallsByPath('/api/sessions/*/tracks');
const putCalls = trackSyncCalls.filter(c => c.method === 'PUT');
// Should have at least 1 track sync call
expect(putCalls.length).toBeGreaterThanOrEqual(1);
// Validate payload structure
const firstCall = putCalls[0];
expect(firstCall.requestBody).toHaveProperty('client_id');
expect(firstCall.requestBody).toHaveProperty('tracks');
expect(firstCall.requestBody).toHaveProperty('backing_tracks');
expect(firstCall.requestBody).toHaveProperty('metronome_open');
// Validate response
expect(firstCall.responseStatus).toBe(200);
});
test('syncs tracks when metronome toggled', async ({ page }) => {
// Join session
// Click metronome toggle
// Assert PUT /tracks called with metronome_open=true
});
test('syncs tracks when backing track opened', async ({ page }) => {
// Join session
// Open backing track
// Assert PUT /tracks called with backing_tracks array
});
});
Manual Testing Checklist
- Join session → verify 1-3 PUT /tracks calls in DevTools Network tab
- Toggle metronome → verify PUT /tracks called with metronome_open=true
- Open backing track → verify PUT /tracks called with backing_tracks populated
- Adjust mixer → verify debounced PUT /tracks call
- Change instrument → verify PUT /tracks called with new instrument_id
- Network failure → verify error handling (toast/alert shown)
- Verify other participants see track changes via WebSocket
8. Implementation Steps (Order)
-
Create
trackSyncService.js- Implement
buildTrackSyncPayload() - Implement
syncTracksToServer()thunk - Add error handling
- Implement
-
Create
useTrackSync.jshook- Wrap
syncTracksToServerfor component use - Add debouncing logic
- Return
{ syncTracks, isSyncing, lastSyncTime, syncError }
- Wrap
-
Add initial sync to
JKSessionScreen.js- Import
useTrackSync - Add useEffect to sync on session join (3 calls at 1s, 1.4s, 6s)
- Test with manual join
- Import
-
Add sync to metronome toggle
- Modify
JKSessionMetronomePlayer.js - Call
syncTracks()when metronome toggled - Test metronome toggle
- Modify
-
Add sync to media actions
- Modify
useMediaActions.js - Add sync after
toggleBackingTrack,toggleJamTrack - Test opening/closing media
- Modify
-
Add sync to mixer changes (optional, can defer)
- Modify
useMixerHelper.js - Debounced sync on mixer adjustments
- Test mixer slider changes
- Modify
-
Write Playwright tests
- Create
test/track-sync/track-configuration.spec.ts - Test session join sync
- Test metronome sync
- Test media sync
- Create
-
Manual testing and refinement
- Test all flows in browser
- Verify WebSocket notifications to other participants
- Check DevTools for correct API calls
- Optimize timing and debouncing
9. Edge Cases and Error Handling
Edge Cases
-
Session not yet joined
- Don't call sync if
sessionIdis null - Wait for
sessionJoinedflag
- Don't call sync if
-
Client ID not available
- Don't call sync if
clientIdis null - Wait for native client initialization
- Don't call sync if
-
No tracks to sync
- Still call API with empty arrays (server needs metronome state)
-
Rapid consecutive changes
- Debounce to avoid API spam
- Cancel pending requests if new one triggered
-
User leaves session mid-sync
- Cancel pending requests
- Clear sync timers
Error Handling
-
Network failure
- Show user-friendly error toast
- Don't update local state
- Retry with exponential backoff?
-
401 Unauthorized
- Session expired, redirect to login
-
404 Session not found
- Session was deleted, show error and redirect
-
422 Validation error
- Log error, show to user
- Check payload format
10. Success Criteria
Must Have ✅
- PUT /api/sessions/{id}/tracks called on session join
- Payload includes client_id, tracks, backing_tracks, metronome_open
- Response updates Redux state
- Metronome toggle triggers sync
- Backing track open/close triggers sync
- Errors handled gracefully with user feedback
- Playwright tests pass for all scenarios
Should Have 📋
- Debouncing prevents API spam
- Sync status shown in UI (loading spinner?)
- Automatic retry on network failure
- Legacy-like 3-call pattern on session join (for compatibility)
Could Have 💡
- Optimistic updates (update UI before server confirms)
- Sync queue for offline resilience
- Analytics tracking for sync success/failure rates
- Reduce to single call on join (after verifying works)
11. Rollout Plan
Phase 1: Development
- Implement in feature branch:
feature/track-sync-api - Local testing with Rails backend
- Unit tests passing
Phase 2: Integration Testing
- Playwright tests passing
- Manual QA with multiple participants
- Verify WebSocket notifications work
Phase 3: Staging Deployment
- Deploy to staging environment
- Monitor error rates and API performance
- Test with beta users
Phase 4: Production Rollout
- Deploy to production
- Monitor API calls (should see 3 PUT /tracks per session join)
- Monitor error rates
- Verify no performance degradation
12. Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| API spam from debouncing bugs | High server load | Implement rate limiting, test debouncing thoroughly |
| Wrong payload format breaks Rails | 422 errors, broken sync | Validate payload against Rails specs, add schema validation |
| Sync failures silent | Users unaware of issues | Add error toasts, log to analytics |
| Race conditions with WebSocket updates | Inconsistent state | Implement proper Redux action ordering |
| Breaking changes to legacy app | Backward compatibility issues | Test with both legacy and jam-ui simultaneously |
13. Questions to Resolve Before Implementation
-
Should we match legacy's 3-call pattern exactly?
- Pro: Ensures compatibility
- Con: Seems redundant, could optimize to 1 call
- Decision: Start with 3 calls, optimize later
-
What happens if native client not initialized yet?
- Need to wait for
clientIdbefore calling API - Add guard:
if (!clientId) return;
- Need to wait for
-
Should mixer changes sync immediately or debounced?
- Debounced (500ms) to avoid spam
- User gets immediate UI feedback from local state
-
How to handle tracks from native client?
- Native client provides track list via
jamClientAPI - Need to map native client tracks to API format
- Check if
jamClient.getTracks()method exists
- Native client provides track list via
-
What about instrument selection?
- Need UI for user to select instrument
- Sync after instrument change
- Is this already implemented?
Next Steps:
- Review this plan with team
- Answer open questions
- Get approval on approach
- Start implementation with Phase 1 (trackSyncService)
Generated: 2026-01-21 Status: ✅ Ready for Implementation