10 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32-state-update-optimization | 01 | execute | 1 |
|
true |
|
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
<execution_context> @/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md @/Users/nuwan/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/32-state-update-optimization/32-RESEARCH.mdSource 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: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.jsuseDebounceCallback hook created with ref-based pattern, ready for import in useSessionModel
-
Add import for useMemo at top (if not already present)
-
Find the current track sync useEffect (around lines 453-485):
// 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])
- Replace with single debounced call pattern:
// 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]);
- Add lodash debounce import if not present:
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
- Add import at top:
import { useDebounceCallback } from './useDebounceCallback';
- Find current trackChanges (around lines 604-614):
// 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]);
- Replace with useDebounceCallback pattern:
// 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
- Remove lodash debounce import if no longer used elsewhere in file:
Check for other usages first:
grep -n "debounce" useSessionModel.jsIf 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
-
Debounce stability verified:
- useDebounceCallback hook exists with ref pattern
- trackChanges in useSessionModel uses useDebounceCallback
- No useCallback(debounce) pattern in useSessionModel
-
All files pass ESLint:
cd jam-ui && npx eslint src/hooks/useDebounceCallback.js src/hooks/useSessionModel.js src/components/client/JKSessionScreen.js --max-warnings=0
<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>