---
phase: 32-state-update-optimization
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - jam-ui/src/components/client/JKSessionScreen.js
  - jam-ui/src/hooks/useSessionModel.js
  - jam-ui/src/hooks/useDebounceCallback.js
autonomous: true

must_haves:
  truths:
    - "Track sync fires once per session join (not 3 times)"
    - "Debounce timer in trackChanges is stable across state changes"
    - "Debounced callbacks read fresh state values"
  artifacts:
    - path: "jam-ui/src/hooks/useDebounceCallback.js"
      provides: "Reusable ref-based debounce hook"
      exports: ["useDebounceCallback"]
    - path: "jam-ui/src/components/client/JKSessionScreen.js"
      provides: "Single debounced track sync"
      contains: "syncTracksDebounced"
    - path: "jam-ui/src/hooks/useSessionModel.js"
      provides: "Stable trackChanges debounce"
      contains: "useDebounceCallback"
  key_links:
    - from: "jam-ui/src/components/client/JKSessionScreen.js"
      to: "syncTracksToServer"
      via: "single debounced call"
      pattern: "useMemo.*debounce.*syncTracksToServer"
    - from: "jam-ui/src/hooks/useSessionModel.js"
      to: "useDebounceCallback"
      via: "import and usage"
      pattern: "useDebounceCallback.*trackChanges"
---

<objective>
Fix redundant track sync calls and debounce instance recreation

Purpose: Eliminate triple API calls on session join (STATE-01) and fix debounce timer reset on state changes (STATE-02). These are the two remaining redundant operation patterns identified in the investigation phase.

Output: Single debounced track sync call, stable useDebounceCallback hook, refactored trackChanges handler
</objective>

<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/32-state-update-optimization/32-RESEARCH.md

# Source files to modify
@jam-ui/src/components/client/JKSessionScreen.js
@jam-ui/src/hooks/useSessionModel.js
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create useDebounceCallback hook</name>
  <files>jam-ui/src/hooks/useDebounceCallback.js</files>
  <action>
Create a new reusable hook that solves the stale closure problem with debounced callbacks:

```javascript
import { useRef, useEffect, useMemo } from 'react';
import { debounce } from 'lodash';

/**
 * Hook that creates a stable debounced callback with fresh closure access.
 * Solves the problem of debounce recreation when dependencies change.
 *
 * Pattern: useRef stores latest callback, useMemo creates debounce once.
 * Source: https://www.developerway.com/posts/debouncing-in-react
 *
 * @param {Function} callback - The callback to debounce
 * @param {number} delay - Debounce delay in ms (default: 500)
 * @returns {Function} Debounced function that always uses latest callback
 */
export const useDebounceCallback = (callback, delay = 500) => {
  const callbackRef = useRef(callback);

  // Always keep ref current with latest callback
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Create debounced function once - never recreated
  const debouncedFn = useMemo(() =>
    debounce((...args) => {
      callbackRef.current?.(...args);
    }, delay),
    [delay]  // Only recreate if delay changes
  );

  // Cleanup on unmount
  useEffect(() => {
    return () => debouncedFn.cancel();
  }, [debouncedFn]);

  return debouncedFn;
};

export default useDebounceCallback;
```

Key design decisions:
- Uses useRef to store callback, keeping it always current
- Uses useMemo with [delay] deps to create debounce function once
- Handles cleanup on unmount via useEffect return
- Exports both named and default for flexibility
  </action>
  <verify>
File exists at jam-ui/src/hooks/useDebounceCallback.js
Exports useDebounceCallback function
No ESLint errors: `cd jam-ui && npx eslint src/hooks/useDebounceCallback.js`
  </verify>
  <done>
useDebounceCallback hook created with ref-based pattern, ready for import in useSessionModel
  </done>
</task>

<task type="auto">
  <name>Task 2: Consolidate track sync to single debounced call</name>
  <files>jam-ui/src/components/client/JKSessionScreen.js</files>
  <action>
Replace the triple setTimeout pattern (lines ~462-485) with a single debounced call.

1. Add import for useMemo at top (if not already present)

2. Find the current track sync useEffect (around lines 453-485):
```javascript
// Track sync: Sync tracks to server when session joined (3-call pattern matching legacy)
useEffect(() => {
  if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) {
    return;
  }
  const timer1 = setTimeout(() => { ... }, 1000);
  const timer2 = setTimeout(() => { ... }, 1400);
  const timer3 = setTimeout(() => { ... }, 6000);
  return () => { clearTimeout(timer1); ... };
}, [hasJoined, sessionId, mixersReady, dispatch])
```

3. Replace with single debounced call pattern:
```javascript
// Create stable debounced sync function
const syncTracksDebounced = useMemo(() =>
  debounce((sid, cid, d) => {
    d(syncTracksToServer(sid, cid));
  }, 1500),  // 1.5s delay - adequate for mixer initialization
  []
);

// Track sync: Single debounced call when session joined
// Replaces legacy 3-timer pattern (1s, 1.4s, 6s) with single 1.5s debounced call
useEffect(() => {
  if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) {
    return;
  }

  // console.log('[Track Sync] Mixers ready, scheduling single debounced sync');
  syncTracksDebounced(sessionId, server.clientId, dispatch);

  return () => syncTracksDebounced.cancel();
  // Note: server intentionally NOT in deps to avoid re-running on server reference changes
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasJoined, sessionId, mixersReady, dispatch, syncTracksDebounced]);
```

4. Add lodash debounce import if not present:
```javascript
import { debounce } from 'lodash';
```

Why 1.5s delay: Legacy used 1s, 1.4s, 6s. The first two calls at 1s and 1.4s were redundant. A single 1.5s debounced call covers the initial mixer setup window while preventing rapid repeated calls. The 6s call was likely for slow networks - if issues arise, this delay can be increased.
  </action>
  <verify>
Check the track sync useEffect has single debounce call:
`grep -A 20 "Track sync:" jam-ui/src/components/client/JKSessionScreen.js`

Verify no setTimeout pattern remains in that section:
`grep -A 20 "Track sync:" jam-ui/src/components/client/JKSessionScreen.js | grep -c setTimeout` should be 0

ESLint passes: `cd jam-ui && npx eslint src/components/client/JKSessionScreen.js --max-warnings=0`
  </verify>
  <done>
Track sync useEffect uses single debounced call instead of 3 setTimeout calls
  </done>
</task>

<task type="auto">
  <name>Task 3: Fix trackChanges debounce in useSessionModel</name>
  <files>jam-ui/src/hooks/useSessionModel.js</files>
  <action>
Replace the useCallback-wrapped debounce with useDebounceCallback hook.

1. Add import at top:
```javascript
import { useDebounceCallback } from './useDebounceCallback';
```

2. Find current trackChanges (around lines 604-614):
```javascript
// Track changes handler - debounced to prevent excessive session refreshes
const trackChanges = useCallback(debounce((header, payload) => {
  if (currentTrackChanges < payload.track_changes_counter) {
    logger.debug("track_changes_counter = stale. refreshing...");
    refreshCurrentSession();
  } else {
    if (header.type !== 'HEARTBEAT_ACK') {
      logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
    }
  }
}, 500), [currentTrackChanges, refreshCurrentSession]);
```

3. Replace with useDebounceCallback pattern:
```javascript
// Track changes handler - debounced to prevent excessive session refreshes
// Uses useDebounceCallback for stable timer (doesn't reset when deps change)
const trackChanges = useDebounceCallback((header, payload) => {
  if (currentTrackChanges < payload.track_changes_counter) {
    logger.debug("track_changes_counter = stale. refreshing...");
    refreshCurrentSession();
  } else {
    if (header.type !== 'HEARTBEAT_ACK') {
      logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
    }
  }
}, 500);
```

Key changes:
- Remove useCallback wrapper (useDebounceCallback handles memoization internally)
- Remove debounce import call (it's inside useDebounceCallback)
- Remove [currentTrackChanges, refreshCurrentSession] dependency array
- The callback now always reads fresh values via ref closure
- Timer is stable - won't reset on currentTrackChanges updates

4. Remove lodash debounce import if no longer used elsewhere in file:
Check for other usages first: `grep -n "debounce" useSessionModel.js`
If trackChanges was the only use, remove: `import { debounce } from 'lodash';`
If other usages exist, keep the import.
  </action>
  <verify>
trackChanges now uses useDebounceCallback:
`grep -B 2 -A 10 "trackChanges = " jam-ui/src/hooks/useSessionModel.js | head -15`

No useCallback wrapper on trackChanges:
`grep "useCallback(debounce" jam-ui/src/hooks/useSessionModel.js | wc -l` should be 0

ESLint passes: `cd jam-ui && npx eslint src/hooks/useSessionModel.js --max-warnings=0`
  </verify>
  <done>
trackChanges uses useDebounceCallback - timer stable across state changes, always reads fresh values
  </done>
</task>

</tasks>

<verification>
1. Track sync fires once per session join:
   - Grep for setTimeout in track sync section should return 0
   - useMemo + debounce pattern present

2. Debounce stability verified:
   - useDebounceCallback hook exists with ref pattern
   - trackChanges in useSessionModel uses useDebounceCallback
   - No useCallback(debounce) pattern in useSessionModel

3. All files pass ESLint:
   ```bash
   cd jam-ui && npx eslint src/hooks/useDebounceCallback.js src/hooks/useSessionModel.js src/components/client/JKSessionScreen.js --max-warnings=0
   ```
</verification>

<success_criteria>
- [ ] useDebounceCallback.js created with ref-based debounce pattern
- [ ] JKSessionScreen track sync uses single debounced call (not 3 setTimeout)
- [ ] useSessionModel trackChanges uses useDebounceCallback (no dependency array recreation)
- [ ] All modified files pass ESLint
- [ ] No regressions - session join still works (manual verification if time permits)
</success_criteria>

<output>
After completion, create `.planning/phases/32-state-update-optimization/32-01-SUMMARY.md`
</output>
