--- 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" --- 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 @/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md @/Users/nuwan/.claude/get-shit-done/templates/summary.md @.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 Task 1: Create useDebounceCallback hook jam-ui/src/hooks/useDebounceCallback.js 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 File exists at jam-ui/src/hooks/useDebounceCallback.js Exports useDebounceCallback function No ESLint errors: `cd jam-ui && npx eslint src/hooks/useDebounceCallback.js` useDebounceCallback hook created with ref-based pattern, ready for import in useSessionModel Task 2: Consolidate track sync to single debounced call jam-ui/src/components/client/JKSessionScreen.js 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. 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` Track sync useEffect uses single debounced call instead of 3 setTimeout calls Task 3: Fix trackChanges debounce in useSessionModel jam-ui/src/hooks/useSessionModel.js 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. 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` trackChanges uses useDebounceCallback - timer stable across state changes, always reads fresh values 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 ``` - [ ] 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) After completion, create `.planning/phases/32-state-update-optimization/32-01-SUMMARY.md`