jam-cloud/.planning/phases/32-state-update-optimization/32-01-PLAN.md

295 lines
10 KiB
Markdown

---
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>