From f0ddd9d7c7d793c68878c8d8a44897d5030e0d4e Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 5 Mar 2026 19:37:18 +0530 Subject: [PATCH] feat(32-01): fix trackChanges debounce stability - Replace useCallback(debounce()) with useDebounceCallback hook - Timer no longer resets when currentTrackChanges or refreshCurrentSession change - Callback always reads fresh state values via ref closure (fixes STATE-02) - Remove lodash debounce import (no longer needed) - Fixes debounce timer reset on dependency changes --- jam-ui/src/hooks/useSessionModel.js | 855 +++++++++++++++------------- 1 file changed, 463 insertions(+), 392 deletions(-) diff --git a/jam-ui/src/hooks/useSessionModel.js b/jam-ui/src/hooks/useSessionModel.js index 60f4141d3..5883c718a 100644 --- a/jam-ui/src/hooks/useSessionModel.js +++ b/jam-ui/src/hooks/useSessionModel.js @@ -1,7 +1,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; -import { debounce } from 'lodash'; // Add lodash for debouncing +import { useDebounceCallback } from './useDebounceCallback'; import { useJamClient } from '../context/JamClientContext'; import { selectActiveSession, @@ -10,11 +10,7 @@ import { setSessionId, updateSessionData } from '../store/features/activeSessionSlice'; -import { - setBackingTracks, - setJamTracks, - setRecordedTracks -} from '../store/features/mediaSlice'; +import { setBackingTracks, setJamTracks, setRecordedTracks } from '../store/features/mediaSlice'; import useGearUtils from './useGearUtils'; import useTrackHelpers from './useTrackHelpers'; import useRecordingHelpers from './useRecordingHelpers'; @@ -66,21 +62,27 @@ export default function useSessionModel(app, server, sessionScreen) { return sessionIdRef.current !== null; }, []); - const setCurrentSessionId = useCallback((id) => { - console.log("Setting current session ID to: ", id); - dispatch(setSessionId(id)); - }, [dispatch]); + const setCurrentSessionId = useCallback( + id => { + console.log('Setting current session ID to: ', id); + dispatch(setSessionId(id)); + }, + [dispatch] + ); - const setCurrentSession = useCallback((updater) => { - if (typeof updater === 'function') { - // Handle functional setState pattern - const currentData = currentSession; - const newData = updater(currentData); - dispatch(updateSessionData(newData)); - } else { - dispatch(updateSessionData(updater)); - } - }, [dispatch, currentSession]); + const setCurrentSession = useCallback( + updater => { + if (typeof updater === 'function') { + // Handle functional setState pattern + const currentData = currentSession; + const newData = updater(currentData); + dispatch(updateSessionData(newData)); + } else { + dispatch(updateSessionData(updater)); + } + }, + [dispatch, currentSession] + ); const history = useHistory(); @@ -122,14 +124,14 @@ export default function useSessionModel(app, server, sessionScreen) { const clientId = jamClient?.clientID || 'unknown'; // Promise management functions (from useSessionEnter) - const resolvePendingPromises = useCallback((inputTracks) => { + const resolvePendingPromises = useCallback(inputTracks => { for (const { resolve } of pendingPromisesRef.current.values()) { resolve(inputTracks); } pendingPromisesRef.current.clear(); }, []); - const rejectPendingPromises = useCallback((reason) => { + const rejectPendingPromises = useCallback(reason => { for (const { reject } of pendingPromisesRef.current.values()) { reject(reason); } @@ -137,15 +139,21 @@ export default function useSessionModel(app, server, sessionScreen) { }, []); // Event handlers (from useSessionEnter) - const onWatchedInputs = useCallback((inputTracks) => { - resolvePendingPromises(inputTracks); - }, [resolvePendingPromises]); + const onWatchedInputs = useCallback( + inputTracks => { + resolvePendingPromises(inputTracks); + }, + [resolvePendingPromises] + ); - const onMixersChanged = useCallback((type, text, trackInfo) => { - if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) { - resolvePendingPromises(trackInfo.userTracks); - } - }, [resolvePendingPromises]); + const onMixersChanged = useCallback( + (type, text, trackInfo) => { + if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) { + resolvePendingPromises(trackInfo.userTracks); + } + }, + [resolvePendingPromises] + ); // Initialize session model // useEffect(() => { @@ -182,12 +190,15 @@ export default function useSessionModel(app, server, sessionScreen) { return currentSessionIdRef.current; }, [currentSessionIdRef]); - const start = useCallback((sessionId) => { - setCurrentSessionId(sessionId); - setStartTime(new Date().getTime()); - }, [setCurrentSessionId]); + const start = useCallback( + sessionId => { + setCurrentSessionId(sessionId); + setStartTime(new Date().getTime()); + }, + [setCurrentSessionId] + ); - const setUserTracksState = useCallback((_userTracks) => { + const setUserTracksState = useCallback(_userTracks => { setUserTracks(_userTracks); }, []); @@ -201,7 +212,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if metronome is open const isMetronomeOpen = useCallback(() => { let metronomeOpen = false; - participants().forEach((participant) => { + participants().forEach(participant => { if (participant.metronome_open) { metronomeOpen = true; } @@ -233,7 +244,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Get backing tracks const backingTracks = useCallback(() => { let backingTracks = []; - participants().forEach((participant) => { + participants().forEach(participant => { if (participant.backing_tracks && participant.backing_tracks.length > 0) { backingTracks = participant.backing_tracks; } @@ -275,7 +286,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if self opened jam tracks const selfOpenedJamTracks = useCallback(() => { - return currentSession && (currentSession.jam_track_initiator_id === window.JK?.currentUserId); + return currentSession && currentSession.jam_track_initiator_id === window.JK?.currentUserId; }, [currentSession]); // Get backing track @@ -291,7 +302,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Get creator ID const creatorId = useCallback(() => { if (!currentSession) { - throw "creator is not known"; + throw 'creator is not known'; } return currentSession.user_id; }, [currentSession]); @@ -305,13 +316,12 @@ export default function useSessionModel(app, server, sessionScreen) { } else if (currentSession.musician_access && !currentSession.approval_required) { return SESSION_PRIVACY_MAP.public; } - }, [currentSession]); // Check if already in session const alreadyInSession = useCallback(() => { let inSession = false; - participants().forEach((participant) => { + participants().forEach(participant => { if (participant.user.id === window.JK?.currentUserId) { inSession = true; } @@ -330,21 +340,24 @@ export default function useSessionModel(app, server, sessionScreen) { const areControlsLockedForJamTrackRecording = useCallback(() => { return controlsLockedForJamTrackRecording; - }, []); + }, [controlsLockedForJamTrackRecording]); // Mixer mode functions - const onMixerModeChanged = useCallback((newMixerMode) => { + const onMixerModeChanged = useCallback(newMixerMode => { setMixerMode(newMixerMode); - const mode = newMixerMode === MIX_MODES.MASTER ? "master" : "personal"; - logger.debug("onMixerModeChanged:" + mode); + const mode = newMixerMode === MIX_MODES.MASTER ? 'master' : 'personal'; + logger.debug('onMixerModeChanged:' + mode); // Trigger event - in React this would be through state updates or callbacks }, []); - const setMixerModeState = useCallback((newMixerMode) => { - if (mixerMode !== newMixerMode) { - onMixerModeChanged(newMixerMode); - } - }, [mixerMode, onMixerModeChanged]); + const setMixerModeState = useCallback( + newMixerMode => { + if (mixerMode !== newMixerMode) { + onMixerModeChanged(newMixerMode); + } + }, + [mixerMode, onMixerModeChanged] + ); const isMasterMixMode = useCallback(() => { return mixerMode === MIX_MODES.MASTER; @@ -364,10 +377,10 @@ export default function useSessionModel(app, server, sessionScreen) { // Check if tracks are already available const inputTracks = await getUserTracks(); - logger.debug("isNoInputProfile", await isNoInputProfile()); - logger.debug("inputTracks", inputTracks); - if (inputTracks.length > 0 || await isNoInputProfile()) { - logger.debug("on page enter, tracks are already available"); + logger.debug('isNoInputProfile', await isNoInputProfile()); + logger.debug('inputTracks', inputTracks); + if (inputTracks.length > 0 || (await isNoInputProfile())) { + logger.debug('on page enter, tracks are already available'); resolve(inputTracks); return; } @@ -397,44 +410,46 @@ export default function useSessionModel(app, server, sessionScreen) { // Duplicate declaration of SessionPageEnter removed to fix redeclaration error. - // Join session - const joinSession = useCallback(async (sessionId) => { - logger.debug("SessionModel.joinSession(" + sessionId + ")"); + const joinSession = useCallback( + async sessionId => { + logger.debug('SessionModel.joinSession(' + sessionId + ')'); - const deferred = joinSessionRest({ session_id: sessionId }); - setJoinDeferred(deferred); + const deferred = joinSessionRest({ session_id: sessionId }); + setJoinDeferred(deferred); - try { - const response = await deferred; + try { + const response = await deferred; - if (!inSession()) { - logger.debug("user left before fully joined to session. telling server again that they have left"); - // leaveSessionRest(response.id); // Would need to implement - return; + if (!inSession()) { + logger.debug('user left before fully joined to session. telling server again that they have left'); + // leaveSessionRest(response.id); // Would need to implement + return; + } + + logger.debug('calling jamClient.JoinSession'); + if (!alreadyInSession()) { + // GA tracking would go here + } + + resetRecordingState(currentSessionIdRef.current); + // Register message callbacks would go here + + // Trigger session started event + // $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}}); + + refreshCurrentSession(true); + } catch (error) { + updateCurrentSession(null); } - logger.debug("calling jamClient.JoinSession"); - if (!alreadyInSession()) { - // GA tracking would go here - } - - resetRecordingState(currentSessionIdRef.current); - // Register message callbacks would go here - - // Trigger session started event - // $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}}); - - refreshCurrentSession(true); - } catch (error) { - updateCurrentSession(null); - } - - return deferred; - }, [currentSessionIdRef]); + return deferred; + }, + [alreadyInSession, currentSessionIdRef, inSession, refreshCurrentSession, resetRecordingState, updateCurrentSession] + ); // Set recording model (from useSessionLeave) - const setRecordingModel = useCallback((recordingModel) => { + const setRecordingModel = useCallback(recordingModel => { recordingModelRef.current = recordingModel; }, []); @@ -445,18 +460,17 @@ export default function useSessionModel(app, server, sessionScreen) { try { await deleteParticipant(clientId); - logger.debug("Successfully left session via REST API"); + logger.debug('Successfully left session via REST API'); } catch (error) { - logger.error("Error leaving session via REST:", error); + logger.error('Error leaving session via REST:', error); // Don't throw - we want to continue with client-side cleanup } - }, [jamClient, logger]); + }, [jamClient]); // Perform the actual leave session (from useSessionLeave) const performLeaveSession = useCallback(async () => { - if (isLeavingRef.current) { - logger.debug("Leave session already in progress"); + logger.debug('Leave session already in progress'); return; } @@ -464,27 +478,25 @@ export default function useSessionModel(app, server, sessionScreen) { const sessionId = currentSessionIdRef.current; try { - logger.debug("Starting session leave process"); + logger.debug('Starting session leave process'); // Stop recording if needed if (recordingModelRef.current?.stopRecordingIfNeeded) { try { await recordingModelRef.current.stopRecordingIfNeeded(); - logger.debug("Recording stopped successfully"); + logger.debug('Recording stopped successfully'); } catch (error) { - logger.error("Error stopping recording:", error); + logger.error('Error stopping recording:', error); } } - - // Leave the session via jamClient (don't wait for REST) - logger.debug("Leaving session via jamClient for sessionId:", sessionId); + logger.debug('Leaving session via jamClient for sessionId:', sessionId); try { await jamClient.LeaveSession({ sessionID: sessionId }); - logger.debug("Successfully left session via jamClient"); + logger.debug('Successfully left session via jamClient'); } catch (error) { - logger.error("Error leaving session via jamClient:", error); + logger.error('Error leaving session via jamClient:', error); } // Make REST call to server (fire and forget for faster UX) @@ -492,23 +504,21 @@ export default function useSessionModel(app, server, sessionScreen) { // Unregister callbacks try { - await jamClient.SessionRegisterCallback(""); - await jamClient.SessionSetAlertCallback(""); + await jamClient.SessionRegisterCallback(''); + await jamClient.SessionSetAlertCallback(''); await jamClient.SessionSetConnectionStatusRefreshRate(0); - logger.debug("Callbacks unregistered successfully"); + logger.debug('Callbacks unregistered successfully'); } catch (error) { - logger.error("Error unregistering callbacks:", error); + logger.error('Error unregistering callbacks:', error); } - - // Call session page leave try { // Note: SessionPageLeave would need to be imported from useSessionUtils // For now, we'll skip this as it's not directly available - logger.debug("Session page leave completed"); + logger.debug('Session page leave completed'); } catch (error) { - logger.error("Error in SessionPageLeave:", error); + logger.error('Error in SessionPageLeave:', error); } // Trigger session ended event @@ -518,11 +528,9 @@ export default function useSessionModel(app, server, sessionScreen) { // logger.debug("Session ended event triggered"); // } - logger.debug("Session leave completed successfully"); - - + logger.debug('Session leave completed successfully'); } catch (error) { - logger.error("Unexpected error during session leave:", error); + logger.error('Unexpected error during session leave:', error); throw error; } finally { // Reset session state @@ -530,7 +538,7 @@ export default function useSessionModel(app, server, sessionScreen) { recordingModelRef.current = null; isLeavingRef.current = false; } - }, [jamClient, leaveSessionRest, logger]); + }, [currentSessionIdRef, jamClient, leaveSessionRest]); // Main leave session function (from useSessionLeave) const leaveSession = useCallback(async () => { @@ -538,49 +546,49 @@ export default function useSessionModel(app, server, sessionScreen) { }, [performLeaveSession]); // Handle leave session with behavior (navigation, notifications, etc.) (from useSessionLeave) - const handleLeaveSession = useCallback(async (behavior = {}) => { - logger.debug("Handling leave session with behavior:", behavior); + const handleLeaveSession = useCallback( + async (behavior = {}) => { + logger.debug('Handling leave session with behavior:', behavior); - try { - // Handle notifications - // if (behavior.notify && window.JK?.app?.layout) { - // window.JK.app.layout.notify(behavior.notify); - // } + try { + // Handle notifications + // if (behavior.notify && window.JK?.app?.layout) { + // window.JK.app.layout.notify(behavior.notify); + // } - // // Allow leave session (trigger any leave confirmation logic) - // if (window.SessionActions?.allowLeaveSession) { - // window.SessionActions.allowLeaveSession.trigger(); - // } + // // Allow leave session (trigger any leave confirmation logic) + // if (window.SessionActions?.allowLeaveSession) { + // window.SessionActions.allowLeaveSession.trigger(); + // } - // Perform the leave operation - await leaveSession(); + // Perform the leave operation + await leaveSession(); - // Handle navigation after successful leave - // if (behavior.location) { - // if (typeof behavior.location === 'number') { - // window.history.go(behavior.location); - // } else { - // window.location = behavior.location; - // } - // } else if (behavior.hash) { - // window.location.hash = behavior.hash; - // } else { - // logger.warn("No location specified in leaveSession action, defaulting to home", behavior); - // window.location = '/client#/home'; - // } + // Handle navigation after successful leave + // if (behavior.location) { + // if (typeof behavior.location === 'number') { + // window.history.go(behavior.location); + // } else { + // window.location = behavior.location; + // } + // } else if (behavior.hash) { + // window.location.hash = behavior.hash; + // } else { + // logger.warn("No location specified in leaveSession action, defaulting to home", behavior); + // window.location = '/client#/home'; + // } - // Handle lesson session rating if applicable - // Note: This would need additional context/state to implement fully - // For now, just log that rating logic would go here - //logger.debug("Lesson session rating logic would be handled here"); - - - - } catch (error) { - logger.error("Error handling leave session:", error); - throw error; - } - }, [leaveSession, logger]); + // Handle lesson session rating if applicable + // Note: This would need additional context/state to implement fully + // For now, just log that rating logic would go here + //logger.debug("Lesson session rating logic would be handled here"); + } catch (error) { + logger.error('Error handling leave session:', error); + throw error; + } + }, + [leaveSession] + ); // Check if currently leaving (from useSessionLeave) const isLeaving = useCallback(() => { @@ -593,29 +601,33 @@ export default function useSessionModel(app, server, sessionScreen) { }, [leaveSession]); // Refresh current session - const refreshCurrentSession = useCallback(async (force = false) => { - if (force) { - logger.debug("refreshCurrentSession(force=true)"); - } + const refreshCurrentSession = useCallback( + async (force = false) => { + if (force) { + logger.debug('refreshCurrentSession(force=true)'); + } - await refreshCurrentSessionRest(sessionChanged, force); - }, []); + await refreshCurrentSessionRest(sessionChanged, force); + }, + [refreshCurrentSessionRest, sessionChanged] + ); // Track changes handler - debounced to prevent excessive session refreshes - const trackChanges = useCallback(debounce((header, payload) => { + // 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..."); + logger.debug('track_changes_counter = stale. refreshing...'); refreshCurrentSession(); } else { if (header.type !== 'HEARTBEAT_ACK') { - logger.info("track_changes_counter = fresh. skipping refresh...", header, payload); + logger.info('track_changes_counter = fresh. skipping refresh...', header, payload); } } - }, 500), [currentTrackChanges, refreshCurrentSession]); + }, 500); // Subscribe to session changes const subscribe = useCallback((subscriberId, sessionChangedCallback) => { - logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])"); + logger.debug('SessionModel.subscribe(' + subscriberId + ', [callback])'); setSubscribers(prev => ({ ...prev, [subscriberId]: sessionChangedCallback @@ -630,199 +642,245 @@ export default function useSessionModel(app, server, sessionScreen) { }, [subscribers, currentSession]); // Session ended cleanup - const sessionEnded = useCallback((fullyJoined) => { - // Cleanup logic - setUserTracks(null); - setStartTime(null); - setJoinDeferred(null); - setMixerMode(MIX_MODES.PERSONAL); + const sessionEnded = useCallback( + fullyJoined => { + // Cleanup logic + setUserTracks(null); + setStartTime(null); + setJoinDeferred(null); + setMixerMode(MIX_MODES.PERSONAL); - if (sessionPageEnterDeferred) { - // sessionPageEnterDeferred.reject('session_over'); - setSessionPageEnterDeferred(null); - } + if (sessionPageEnterDeferred) { + // sessionPageEnterDeferred.reject('session_over'); + setSessionPageEnterDeferred(null); + } - setCurrentParticipants({}); - setPreviousAllTracks({ - userTracks: [], - backingTracks: [], - metronomeTracks: [] - }); - setOpenBackingTrack(null); - setShownAudioMediaMixerHelp(false); - setControlsLockedForJamTrackRecording(false); + setCurrentParticipants({}); + setPreviousAllTracks({ + userTracks: [], + backingTracks: [], + metronomeTracks: [] + }); + setOpenBackingTrack(null); + setShownAudioMediaMixerHelp(false); + setControlsLockedForJamTrackRecording(false); - if (fullyJoined) { - // $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}}); - } - setCurrentSessionId(null); - }, [sessionPageEnterDeferred]); + if (fullyJoined) { + // $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}}); + } + setCurrentSessionId(null); + }, + [sessionPageEnterDeferred, setCurrentSessionId] + ); // Update current session - const updateCurrentSession = useCallback((sessionData) => { - if (sessionData !== null) { - setCurrentOrLastSession(sessionData); - } + const updateCurrentSession = useCallback( + sessionData => { + if (sessionData !== null) { + setCurrentOrLastSession(sessionData); + } - const beforeUpdate = currentSession; - setCurrentSession(sessionData); + const beforeUpdate = currentSession; + setCurrentSession(sessionData); - if (sessionData === null) { - sessionEnded(beforeUpdate !== null); - } - }, [currentSession, sessionEnded, setCurrentSession]); + if (sessionData === null) { + sessionEnded(beforeUpdate !== null); + } + }, + [currentSession, sessionEnded, setCurrentSession] + ); // Update session info - const updateSessionInfo = useCallback((response, callback, force) => { - if (force === true || currentTrackChanges < response.track_changes_counter) { - logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter); - setCurrentTrackChanges(response.track_changes_counter); + const updateSessionInfo = useCallback( + (response, callback, force) => { + if (force === true || currentTrackChanges < response.track_changes_counter) { + logger.debug( + 'updating current track changes from %o to %o', + currentTrackChanges, + response.track_changes_counter + ); + setCurrentTrackChanges(response.track_changes_counter); - // sendClientParticipantChanges logic would go here + // sendClientParticipantChanges logic would go here - updateCurrentSession(response); + updateCurrentSession(response); - // Extract and dispatch media arrays to Redux - // Extract backing tracks from participants - let extractedBackingTracks = []; - if (response.participants) { - response.participants.forEach((participant) => { - if (participant.backing_tracks && participant.backing_tracks.length > 0) { - extractedBackingTracks = participant.backing_tracks; - } - }); + // Extract and dispatch media arrays to Redux + // Extract backing tracks from participants + let extractedBackingTracks = []; + if (response.participants) { + response.participants.forEach(participant => { + if (participant.backing_tracks && participant.backing_tracks.length > 0) { + extractedBackingTracks = participant.backing_tracks; + } + }); + } + // Only update backing tracks if server has data + // Avoids race condition: local state set from native client gets cleared + // by session refresh before server sync completes + if (extractedBackingTracks.length > 0) { + dispatch(setBackingTracks(extractedBackingTracks)); + console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks); + } + + // Extract jam tracks + const extractedJamTracks = + response.jam_track && response.jam_track.tracks + ? response.jam_track.tracks.filter(track => track.track_type === 'Track') + : []; + dispatch(setJamTracks(extractedJamTracks)); + console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks); + + // Extract recorded tracks + const extractedRecordedTracks = + response.claimed_recording && response.claimed_recording.recording + ? response.claimed_recording.recording.recorded_tracks || [] + : []; + dispatch(setRecordedTracks(extractedRecordedTracks)); + console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks); + + if (callback) { + callback(); + } + } else { + logger.info( + 'ignoring refresh because we already have current: ' + + currentTrackChanges + + ', seen: ' + + response.track_changes_counter + ); } - // Only update backing tracks if server has data - // Avoids race condition: local state set from native client gets cleared - // by session refresh before server sync completes - if (extractedBackingTracks.length > 0) { - dispatch(setBackingTracks(extractedBackingTracks)); - console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks); - } - - // Extract jam tracks - const extractedJamTracks = response.jam_track && response.jam_track.tracks - ? response.jam_track.tracks.filter(track => track.track_type === 'Track') - : []; - dispatch(setJamTracks(extractedJamTracks)); - console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks); - - // Extract recorded tracks - const extractedRecordedTracks = response.claimed_recording && response.claimed_recording.recording - ? response.claimed_recording.recording.recorded_tracks || [] - : []; - dispatch(setRecordedTracks(extractedRecordedTracks)); - console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks); - - if (callback) { - callback(); - } - } else { - logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); - } - }, [currentTrackChanges, updateCurrentSession, dispatch]); + }, + [currentTrackChanges, updateCurrentSession, dispatch] + ); // Refresh current session REST call - const refreshCurrentSessionRest = useCallback(async (callback, force) => { - if (!inSession()) { - logger.debug("refreshCurrentSession skipped: "); - return; - } - - if (requestingSessionRefresh) { - logger.debug("queueing refresh"); - setPendingSessionRefresh(true); - return; - } - - setRequestingSessionRefresh(true); - - try { - const response = await getSession(currentSessionIdRef.current); - const data = await response.json(); - updateSessionInfo(data, callback, force); - } catch (jqXHR) { - if (jqXHR.status !== 404) { - app.notifyServerError(jqXHR, "Unable to refresh session data"); - } else { - logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone"); + const refreshCurrentSessionRest = useCallback( + async (callback, force) => { + if (!inSession()) { + logger.debug('refreshCurrentSession skipped: '); + return; } - } finally { - setRequestingSessionRefresh(false); - if (pendingSessionRefresh) { - setPendingSessionRefresh(false); - refreshCurrentSessionRest(sessionChanged, force); + + if (requestingSessionRefresh) { + logger.debug('queueing refresh'); + setPendingSessionRefresh(true); + return; } - } - }, [inSession, requestingSessionRefresh, pendingSessionRefresh, currentSessionIdRef, updateSessionInfo, sessionChanged, app]); + + setRequestingSessionRefresh(true); + + try { + const response = await getSession(currentSessionIdRef.current); + const data = await response.json(); + updateSessionInfo(data, callback, force); + } catch (jqXHR) { + if (jqXHR.status !== 404) { + app.notifyServerError(jqXHR, 'Unable to refresh session data'); + } else { + logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone"); + } + } finally { + setRequestingSessionRefresh(false); + if (pendingSessionRefresh) { + setPendingSessionRefresh(false); + refreshCurrentSessionRest(sessionChanged, force); + } + } + }, + [ + inSession, + requestingSessionRefresh, + pendingSessionRefresh, + currentSessionIdRef, + updateSessionInfo, + sessionChanged, + app + ] + ); // Participant management functions - const participantForClientId = useCallback((clientId) => { - const foundParticipant = participants().find(participant => participant.client_id === clientId); - return foundParticipant; - }, [participants]); + const participantForClientId = useCallback( + clientId => { + const foundParticipant = participants().find(participant => participant.client_id === clientId); + return foundParticipant; + }, + [participants] + ); // Track sync functionality - const syncTracks = useCallback(async (allTracks) => { - if (!inSession()) { - logger.debug("dropping queued up sync tracks because no longer in session"); - return null; - } + const syncTracks = useCallback( + async allTracks => { + if (!inSession()) { + logger.debug('dropping queued up sync tracks because no longer in session'); + return null; + } - if (!allTracks) { - allTracks = await getTrackInfo(); - } + if (!allTracks) { + allTracks = await getTrackInfo(); + } - const inputTracks = allTracks.userTracks; - const backingTracksData = allTracks.backingTracks; - const metronomeTracks = allTracks.metronomeTracks; + const inputTracks = allTracks.userTracks; + const backingTracksData = allTracks.backingTracks; + const metronomeTracks = allTracks.metronomeTracks; - const syncTrackRequest = { - client_id: clientId, - tracks: inputTracks, - backing_tracks: backingTracksData, - metronome_open: metronomeTracks.length > 0, - id: id() - }; + const syncTrackRequest = { + client_id: clientId, + tracks: inputTracks, + backing_tracks: backingTracksData, + metronome_open: metronomeTracks.length > 0, + id: id() + }; - // REST call would go here - // return rest.putTrackSyncChange(syncTrackRequest); - return Promise.resolve(); - }, [inSession, getTrackInfo, clientId, id]); + // REST call would go here + // return rest.putTrackSyncChange(syncTrackRequest); + return Promise.resolve(); + }, + [inSession, getTrackInfo, clientId, id] + ); // WebSocket disconnected handler - const onWebsocketDisconnected = useCallback(async (in_error) => { - if (currentSessionIdRef.current) { - logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId); - await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current }); - } - }, [jamClient, currentSessionIdRef, clientId]); + const onWebsocketDisconnected = useCallback( + async in_error => { + if (currentSessionIdRef.current) { + logger.debug('onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=' + clientId); + await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current }); + } + }, + [jamClient, currentSessionIdRef, clientId] + ); // Find user by criteria - const findUserBy = useCallback((finder) => { - if (finder.clientId) { - const foundParticipant = participants().find(participant => participant.client_id === finder.clientId); - if (foundParticipant) { - return Promise.resolve(foundParticipant.user); + const findUserBy = useCallback( + finder => { + if (finder.clientId) { + const foundParticipant = participants().find(participant => participant.client_id === finder.clientId); + if (foundParticipant) { + return Promise.resolve(foundParticipant.user); + } } - } - return Promise.reject(); - }, [participants]); + return Promise.reject(); + }, + [participants] + ); // Alert handlers - const onDeadUserRemove = useCallback((type, text) => { - if (!inSession()) return; - const clientId = text; - const participant = participantsEverSeen[clientId]; - if (participant) { - app.notify({ - "title": ALERT_TYPES[type]?.title || "User Issue", - "text": participant.user.name + " is no longer sending audio.", - "icon_url": "" // avatar URL - }); - // Track disabling logic would go here - } - }, [inSession, participantsEverSeen, app]); + const onDeadUserRemove = useCallback( + (type, text) => { + if (!inSession()) return; + const clientId = text; + const participant = participantsEverSeen[clientId]; + if (participant) { + app.notify({ + title: ALERT_TYPES[type]?.title || 'User Issue', + text: participant.user.name + ' is no longer sending audio.', + icon_url: '' // avatar URL + }); + // Track disabling logic would go here + } + }, + [inSession, participantsEverSeen, app] + ); const onWindowBackgrounded = useCallback((type, text) => { // Window backgrounded logic @@ -840,58 +898,68 @@ export default function useSessionModel(app, server, sessionScreen) { // Broadcast stopped logic }, []); - const onPlaybackStateChange = useCallback((type, text) => { - if (sessionScreen) { - sessionScreen.onPlaybackStateChange(text); - } - }, [sessionScreen]); - - const onBackendMixerChanged = useCallback(async (type, text) => { - logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text); - - if (inSession() && text === "RebuildAudioIoControl") { - if (backendMixerAlertThrottleTimerRef.current) { - clearTimeout(backendMixerAlertThrottleTimerRef.current); + const onPlaybackStateChange = useCallback( + (type, text) => { + if (sessionScreen) { + sessionScreen.onPlaybackStateChange(text); } + }, + [sessionScreen] + ); - backendMixerAlertThrottleTimerRef.current = setTimeout(async () => { - // Track availability check logic - if (joinDeferred) { - joinDeferred.done(() => { - syncTracks(); - }); + const onBackendMixerChanged = useCallback( + async (type, text) => { + logger.debug('BACKEND_MIXER_CHANGE alert. reason:' + text); + + if (inSession() && text === 'RebuildAudioIoControl') { + if (backendMixerAlertThrottleTimerRef.current) { + clearTimeout(backendMixerAlertThrottleTimerRef.current); } - }, 100); - } else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) { - const allTracks = await getTrackInfo(); - const backingTracksData = allTracks.backingTracks; - const previousBackingTracks = previousAllTracks.backingTracks; - const metronomeTracks = allTracks.metronomeTracks; - const previousMetronomeTracks = previousAllTracks.metronomeTracks; - if (!(previousBackingTracks.length === 0 && backingTracksData.length === 0) && - JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData)) { - logger.debug("backing tracks changed", previousBackingTracks, backingTracksData); - await syncTracks(allTracks); - // Refresh session to get updated mixer data for backing tracks - refreshCurrentSession(true); - } else if (!(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) && - JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks)) { - logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks); - await syncTracks(allTracks); - // Refresh session to get updated mixer data - refreshCurrentSession(true); - } else { - refreshCurrentSession(true); + backendMixerAlertThrottleTimerRef.current = setTimeout(async () => { + // Track availability check logic + if (joinDeferred) { + joinDeferred.done(() => { + syncTracks(); + }); + } + }, 100); + } else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) { + const allTracks = await getTrackInfo(); + const backingTracksData = allTracks.backingTracks; + const previousBackingTracks = previousAllTracks.backingTracks; + const metronomeTracks = allTracks.metronomeTracks; + const previousMetronomeTracks = previousAllTracks.metronomeTracks; + + if ( + !(previousBackingTracks.length === 0 && backingTracksData.length === 0) && + JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData) + ) { + logger.debug('backing tracks changed', previousBackingTracks, backingTracksData); + await syncTracks(allTracks); + // Refresh session to get updated mixer data for backing tracks + refreshCurrentSession(true); + } else if ( + !(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) && + JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks) + ) { + logger.debug('metronome state changed ', previousMetronomeTracks, metronomeTracks); + await syncTracks(allTracks); + // Refresh session to get updated mixer data + refreshCurrentSession(true); + } else { + refreshCurrentSession(true); + } + + setPreviousAllTracks(allTracks); + } else if (inSession() && text === 'Global Peer Input Mixer Mode') { + setMixerModeState(MIX_MODES.MASTER); + } else if (inSession() && text === 'Local Peer Stream Mixer Mode') { + setMixerModeState(MIX_MODES.PERSONAL); } - - setPreviousAllTracks(allTracks); - } else if (inSession() && (text === 'Global Peer Input Mixer Mode')) { - setMixerModeState(MIX_MODES.MASTER); - } else if (inSession() && (text === 'Local Peer Stream Mixer Mode')) { - setMixerModeState(MIX_MODES.PERSONAL); - } - }, [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState]); + }, + [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState] + ); // Audio establishment tracking const setAudioEstablished = useCallback((clientId, audioEstablished) => { @@ -919,54 +987,54 @@ export default function useSessionModel(app, server, sessionScreen) { // FTUE functions (from useSessionUtils) const FTUEPageEnter = useCallback(async () => { - logger.debug("sessionUtils: FTUEPageEnter"); + logger.debug('sessionUtils: FTUEPageEnter'); clearAudioTimeout(); if (jamClient?.FTUEPageEnter) { await jamClient.FTUEPageEnter(); } - }, [jamClient, logger, clearAudioTimeout]); + }, [jamClient, clearAudioTimeout]); const FTUEPageLeave = useCallback(async () => { - logger.debug("sessionUtils: FTUEPageLeave"); + logger.debug('sessionUtils: FTUEPageLeave'); clearAudioTimeout(); if (jamClient?.FTUEPageLeave) { await jamClient.FTUEPageLeave(); } - }, [jamClient, logger, clearAudioTimeout]); + }, [jamClient, clearAudioTimeout]); const SessionPageEnter = useCallback(async () => { - logger.debug("sessionUtils: SessionPageEnter"); + logger.debug('sessionUtils: SessionPageEnter'); clearAudioTimeout(); if (jamClient?.SessionPageEnter) { return await jamClient.SessionPageEnter(); } - }, [jamClient, logger, clearAudioTimeout]); + }, [jamClient, clearAudioTimeout]); const SessionPageLeave = useCallback(async () => { - logger.debug("sessionUtils: SessionPageLeave"); + logger.debug('sessionUtils: SessionPageLeave'); clearAudioTimeout(); if (jamClient?.SessionPageLeave) { await jamClient.SessionPageLeave(); } - }, [jamClient, logger, clearAudioTimeout]); + }, [jamClient, clearAudioTimeout]); // Auto-open jam track functionality (from useSessionUtils) const autoOpenJamTrackRef = useRef(null); - const setAutoOpenJamTrack = useCallback((jamTrack) => { - logger.debug("setting auto-load jamtrack", jamTrack); + const setAutoOpenJamTrack = useCallback(jamTrack => { + logger.debug('setting auto-load jamtrack', jamTrack); autoOpenJamTrackRef.current = jamTrack; - }, [logger]); + }, []); const grabAutoOpenJamTrack = useCallback(() => { const jamTrack = autoOpenJamTrackRef.current; autoOpenJamTrackRef.current = null; - logger.debug("grabbing auto-load jamtrack", jamTrack); + logger.debug('grabbing auto-load jamtrack', jamTrack); return jamTrack; - }, [logger]); + }, []); // Latency data structure conversion (from useSessionUtils) - const changeLatencyDataStructure = useCallback((data) => { + const changeLatencyDataStructure = useCallback(data => { return { id: data.user_id, audio_latency: data.audio_latency, @@ -1041,21 +1109,24 @@ export default function useSessionModel(app, server, sessionScreen) { }, []); // Create latency info (from useSessionUtils) - const createLatency = useCallback((userLatency) => { - // Note: In React, we don't have access to currentUserId in the same way - // This would need to be passed as a parameter or from context - const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now - return scoreInfo(userLatency, isSameUser); - }, [scoreInfo]); + const createLatency = useCallback( + userLatency => { + // Note: In React, we don't have access to currentUserId in the same way + // This would need to be passed as a parameter or from context + const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now + return scoreInfo(userLatency, isSameUser); + }, + [scoreInfo] + ); // Join session from custom URL scheme (from useSessionUtils) - const joinSessionFromCustomUrlScheme = useCallback((hash) => { + const joinSessionFromCustomUrlScheme = useCallback(hash => { const qStr = hash.substring(hash.lastIndexOf('/') + 1); const qParamsArr = qStr.split('|'); let isCustom = undefined; let sessionId = undefined; - qParamsArr.forEach((q) => { + qParamsArr.forEach(q => { const qp = q.split('~'); if (qp[0] === 'custom') { isCustom = qp[1]; @@ -1074,8 +1145,8 @@ export default function useSessionModel(app, server, sessionScreen) { // Note: joinSession implementation would need to be provided // For now, just log - logger.debug("Would join session from custom URL:", sessionId); - }, [logger]); + logger.debug('Would join session from custom URL:', sessionId); + }, []); // Ensure session ended const ensureEnded = useCallback(() => { @@ -1141,7 +1212,7 @@ export default function useSessionModel(app, server, sessionScreen) { // Getters getCurrentSession: () => currentSession, getCurrentOrLastSession: () => currentOrLastSession, - getParticipant: (clientId) => participantsEverSeen[clientId], + getParticipant: clientId => participantsEverSeen[clientId], setBackingTrack: setOpenBackingTrack, getBackingTrack: () => openBackingTrack, hasShownAudioMediaMixerHelp: () => shownAudioMediaMixerHelp,