295 lines
10 KiB
Markdown
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>
|