# Track Configuration API - Open Questions Answered

**Date:** 2026-01-21
**Status:** ✅ All questions resolved through codebase exploration

---

## Question 1: How do we get track data from native client (`jamClient`)?

### ✅ ANSWER: Use `jamClient.SessionGetAllControlState()`

**Primary Method:**

```javascript
const allTracks = await jamClient.SessionGetAllControlState(true);
// Parameter: true for master mix, false for personal mix
```

**Returns:** Array of track objects with full mixer state

**Concrete Implementation:** `src/hooks/useTrackHelpers.js:152-162`

```javascript
const getTracks = useCallback(async (groupId, allTracks) => {
  const tracks = [];
  if (!allTracks) {
    allTracks = await jamClient.SessionGetAllControlState(true);
  }
  for (const track of allTracks) {
    if (track.group_id === groupId) {
      tracks.push(track);
    }
  }
  return tracks;
}, [jamClient]);
```

### Track Object Structure (from native client)

**Source:** `src/fakeJamClient.js:807-826`

```javascript
{
  client_id: "a2d34590-5e77-4f04-a47e-a2cbb2871baf",  // Client UUID
  group_id: 4,                    // ChannelGroupIds.AudioInputMusicGroup
  id: "mixer-uuid",               // Mixer ID
  master: true,                   // Master vs personal mix
  media_type: "AudioInputMusic",  // Track type
  monitor: false,
  mute: false,
  name: "Guitar",
  range_high: 20,
  range_low: -80,
  record: true,
  stereo: true,                   // true = stereo, false = mono
  volume_left: 0,
  volume_right: 0,
  pan: 0,
  instrument_id: 10,              // Client-side instrument ID (numeric)
  mode: true,
  rid: "resource-uuid"            // Resource ID
}
```

### Filtering Tracks by Type

**Use `group_id` from `src/helpers/globals.js:338-382`:**

```javascript
// User audio tracks
const userTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.AudioInputMusicGroup); // 4

// Metronome track
const metronome = allTracks.find(t => t.group_id === ChannelGroupIds.MetronomeGroup); // 16

// Backing tracks
const backingTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.BackingTrackGroup); // 6

// JamTracks
const jamTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.JamTrackGroup); // 15
```

### Converting Native Tracks to API Format

**Source:** `src/hooks/useTrackHelpers.js:71-91`

```javascript
const buildTrackForAPI = (track) => {
  // Map client instrument ID (numeric) to server instrument ID (string)
  const instrumentId = getInstrumentServerIdFromClientId(track.instrument_id);

  return {
    client_track_id: track.id,           // Mixer ID
    client_resource_id: track.rid,        // Resource ID
    instrument_id: instrumentId,          // "acoustic guitar", "drums", etc.
    sound: track.stereo ? "stereo" : "mono"
  };
};
```

**Instrument ID Mapping:** `src/helpers/globals.js:183-260`

```javascript
// Example mappings:
server_to_client_instrument_map = {
  "Acoustic Guitar": { client_id: 10, server_id: "acoustic guitar" },
  "Bass Guitar": { client_id: 20, server_id: "bass guitar" },
  "Drums": { client_id: 40, server_id: "drums" },
  "Electric Guitar": { client_id: 50, server_id: "electric guitar" },
  "Piano": { client_id: 140, server_id: "piano" },
  "Voice": { client_id: 240, server_id: "vocals" },
  // ... 27 instruments total
};

// Helper function:
export const getInstrumentServerIdFromClientId = (clientId) => {
  const instrument = Object.values(server_to_client_instrument_map)
    .find(inst => inst.client_id === clientId);
  return instrument?.server_id || 'other';
};
```

### Other Track-Related Methods

**JamClient proxy methods** (`src/jamClientProxy.js`):

```javascript
// Backing tracks
jamClient.getBackingTrackList()           // Get available backing track files

// JamTracks
jamClient.JamTrackGetTracks()             // Get JamTrack library

// Individual track info
jamClient.TrackGetInstrument(trackNumber) // Get track's instrument
jamClient.TrackSetInstrument(trackNumber, instrumentId) // Set track's instrument
```

---

## Question 2: Should we optimize the 3-call pattern?

### ✅ RECOMMENDATION: Start with 3 calls, optimize to 1 later

**Legacy pattern** (from test fixtures):
- **Call 1:** ~1 second after joining (initial track setup)
- **Call 2:** ~400ms later at 1.4s (track refinement)
- **Call 3:** ~5 seconds later at 6s (final configuration)

### Why 3 calls in legacy?

**Hypothesis from examining the flow:**

1. **First call (1s):** Wait for native client initialization and initial mixer setup
2. **Second call (1.4s):** React to any auto-configuration from native client
3. **Third call (6s):** Finalize after all components loaded and WebSocket fully established

**This pattern likely evolved organically to handle:**
- Async native client initialization
- Gradual mixer state population
- WebSocket connection establishment timing
- Component mount order dependencies

### Implementation Strategy

**Phase 1: Match legacy exactly (safety first)**

```javascript
// In JKSessionScreen.js or useSessionModel.js
useEffect(() => {
  if (sessionJoined && clientId) {
    // First sync: Initial setup
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 1000);

    // Second sync: Refinement
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 1400);

    // Third sync: Final config
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 6000);
  }
}, [sessionJoined, clientId]);
```

**Phase 2: Optimize (after verification)**

Once we confirm tracks sync correctly, test reducing to single call:

```javascript
useEffect(() => {
  if (sessionJoined && clientId && nativeClientReady) {
    // Single sync after everything initialized
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 2000); // Wait for full initialization
  }
}, [sessionJoined, clientId, nativeClientReady]);
```

**Monitor:**
- Does single call capture all tracks correctly?
- Are there any race conditions?
- Do other participants see tracks immediately?

### Decision: ✅ Start with 3 calls

**Rationale:**
- Legacy pattern is battle-tested
- Minimal risk of missing tracks
- Can optimize later without breaking functionality
- Matches existing behavior for easier debugging

**Future optimization triggers:**
- After successful deployment and monitoring
- If performance metrics show excessive API calls
- If we can reliably detect "all initialized" state

---

## Question 3: Instrument selection UI - does it exist?

### ✅ ANSWER: YES, fully implemented!

**Component:** `src/components/client/JKSessionInstrumentModal.js`

### How It Works

**1. User clicks on their track** (`src/components/client/JKSessionMyTrack.js:81-86`)

```javascript
const handleInstrumentSelect = () => {
  setShowInstrumentModal(true);
};

<div onClick={handleInstrumentSelect}>
  <InstrumentIcon instrumentId={instrumentId} />
</div>
```

**2. Modal opens with instrument list** (27 instruments total)

```javascript
// JKSessionInstrumentModal.js:24
const instrumentList = listInstruments().sort((a, b) =>
  a.description.localeCompare(b.description)
);

// Displays:
// - Acoustic Guitar
// - Bass Guitar
// - Cello
// - Drums
// - Electric Guitar
// - ... (27 total)
```

**3. User selects new instrument**

```javascript
// JKSessionInstrumentModal.js:117
const handleSave = async () => {
  await jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, selectedInstrument);
  onSave(selectedInstrument);
  onRequestClose();
};
```

**4. Parent component receives callback** (`JKSessionMyTrack.js:115-118`)

```javascript
const handleInstrumentSave = instrumentId => {
  jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentId);
  setShowInstrumentModal(false);
  // TODO: Trigger track sync here!
};
```

### Where to Add Track Sync

**Modify `JKSessionMyTrack.js:115-118`:**

```javascript
const handleInstrumentSave = async (instrumentId) => {
  // Update native client
  await jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentId);

  // Sync to server
  dispatch(syncTracksToServer(sessionId)); // ADD THIS LINE

  // Close modal
  setShowInstrumentModal(false);
};
```

### Instrument List Source

**Function:** `src/helpers/globals.js:183-260`

```javascript
export const listInstruments = () => [
  { id: 10, description: "Acoustic Guitar", server_id: "acoustic guitar" },
  { id: 20, description: "Bass Guitar", server_id: "bass guitar" },
  { id: 30, description: "Cello", server_id: "cello" },
  { id: 40, description: "Drums", server_id: "drums" },
  { id: 50, description: "Electric Guitar", server_id: "electric guitar" },
  { id: 60, description: "Fiddle", server_id: "fiddle" },
  // ... 21 more instruments
  { id: 250, description: "Other", server_id: "other" }
];
```

### Current Limitation

**⚠️ Instrument changes are NOT currently synced to server!**

After user changes instrument:
- ✅ Native client is updated (`TrackSetInstrument`)
- ✅ UI shows new instrument icon
- ❌ Server is NOT notified (missing `syncTracksToServer` call)
- ❌ Other participants don't see the change

**This is exactly what we need to fix!**

---

## Question 4: What if native client not initialized yet?

### ✅ ANSWER: Check `jamClient.clientID` before syncing

### Client ID Initialization Flow

**1. Initial UUID generation** (`src/hooks/useJamServer.js:69-70`)

```javascript
// Temporary UUID until login completes
const clientId = generateUUID(); // Not from jamClient.clientID() yet
server.current.clientId = clientId;
```

**Note:** `jamClient.clientID()` currently returns empty string, so we generate UUID as fallback.

**2. Login assigns official clientId** (`src/helpers/JamServer.js:228-240`)

```javascript
loggedIn(header, payload) {
  this.clientID = payload.client_id;  // From WebSocket handshake

  if (typeof window !== 'undefined' && window.jamClient !== undefined) {
    window.jamClient.clientID = this.clientID;  // Set on jamClient proxy
  }

  this.app.clientId = payload.client_id;
}
```

**3. Access throughout app** (`src/hooks/useSessionModel.js:122`)

```javascript
const clientId = jamClient?.clientID || jamClient?.GetClientID() || 'unknown';
```

### Guard Conditions for Track Sync

**Check these conditions before calling API:**

```javascript
const shouldSyncTracks = () => {
  // 1. Client ID must exist
  if (!jamClient?.clientID && !jamClient?.GetClientID()) {
    console.warn('Track sync skipped: clientId not available');
    return false;
  }

  // 2. Session must be active
  if (!sessionId) {
    console.warn('Track sync skipped: no active session');
    return false;
  }

  // 3. User must have joined session
  if (!sessionJoined) {
    console.warn('Track sync skipped: session not joined yet');
    return false;
  }

  // 4. Native client must be connected
  if (!jamClient || !window.jamClient) {
    console.warn('Track sync skipped: native client not available');
    return false;
  }

  return true;
};

// Usage:
const syncTracks = async () => {
  if (!shouldSyncTracks()) return;

  try {
    await dispatch(syncTracksToServer(sessionId));
  } catch (error) {
    console.error('Track sync failed:', error);
  }
};
```

### Recommended Implementation

**In `src/services/trackSyncService.js`:**

```javascript
export const syncTracksToServer = (sessionId) => async (dispatch, getState) => {
  const state = getState();
  const { jamClient } = state.jamClient;
  const { sessionJoined } = state.activeSession;

  // Guard: Check all prerequisites
  const clientId = jamClient?.clientID || jamClient?.GetClientID();

  if (!clientId) {
    console.warn('[Track Sync] Skipped: Client ID not available');
    return { skipped: true, reason: 'no_client_id' };
  }

  if (!sessionId) {
    console.warn('[Track Sync] Skipped: No session ID');
    return { skipped: true, reason: 'no_session_id' };
  }

  if (!sessionJoined) {
    console.warn('[Track Sync] Skipped: Session not joined');
    return { skipped: true, reason: 'session_not_joined' };
  }

  // Proceed with sync
  const payload = await buildTrackSyncPayload(state, sessionId);

  try {
    dispatch(setTrackSyncStatus('syncing'));

    const response = await putTrackSyncChange({ id: sessionId, ...payload });

    dispatch(setTrackSyncStatus('success'));
    dispatch(setUserTracks(response.tracks));
    dispatch(setBackingTrackData(response.backing_tracks));

    console.log('[Track Sync] Success:', response);
    return { success: true, response };

  } catch (error) {
    dispatch(setTrackSyncStatus('error'));
    dispatch(showError('Failed to sync tracks'));

    console.error('[Track Sync] Failed:', error);
    return { success: false, error };
  }
};
```

### Initialization Timeline

**Typical initialization sequence:**

1. **App loads** → jamClient proxy created (fake or real)
2. **User logs in** → Temporary UUID assigned
3. **WebSocket connects** → Official clientId received in `loggedIn` callback
4. **Session joined** → `sessionJoined = true` in Redux
5. **✅ Ready to sync tracks** → All guards pass

**Wait for initialization:**

```javascript
useEffect(() => {
  if (sessionJoined && jamClient?.clientID) {
    // All prerequisites met, safe to sync
    const timer = setTimeout(() => {
      syncTracks();
    }, 1000);

    return () => clearTimeout(timer);
  }
}, [sessionJoined, jamClient?.clientID]);
```

---

## Summary: Implementation Checklist

### ✅ Question 1: Getting Track Data
- [x] Use `jamClient.SessionGetAllControlState(true)`
- [x] Filter by `group_id` to categorize tracks
- [x] Map client instrument IDs to server IDs
- [x] Build API payload with `client_track_id`, `client_resource_id`, `instrument_id`, `sound`

### ✅ Question 2: 3-Call Pattern
- [x] Start with legacy's 3-call pattern (1s, 1.4s, 6s)
- [x] Optimize to 1 call after verification
- [x] Monitor for race conditions

### ✅ Question 3: Instrument Selection
- [x] UI exists: `JKSessionInstrumentModal.js`
- [x] Add track sync after instrument change
- [x] Modify `handleInstrumentSave` in `JKSessionMyTrack.js`

### ✅ Question 4: Initialization Guards
- [x] Check `jamClient?.clientID` exists
- [x] Check `sessionId` exists
- [x] Check `sessionJoined === true`
- [x] Return early if any prerequisite missing
- [x] Log skip reasons for debugging

---

## Next Steps

1. **Create `src/services/trackSyncService.js`** with guards and payload builder
2. **Create `src/hooks/useTrackSync.js`** with debouncing
3. **Modify `JKSessionScreen.js`** to add 3-call pattern on join
4. **Modify `JKSessionMyTrack.js`** to sync on instrument change
5. **Modify `useMediaActions.js`** to sync on media toggle
6. **Write tests** in `test/track-sync/track-configuration.spec.ts`

All questions answered! Ready to implement. 🚀

---

**Generated:** 2026-01-21
**Status:** ✅ All questions resolved
