From 965dc2d708550415de479685c8523d6c5ecb9281 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 15 Jan 2026 17:15:38 +0530 Subject: [PATCH] fix(05-jamtrack): implement native WebSocket subscriptions in React Issue: window.JK.SubscriptionUtils not available in session screen Solution: Implement WebSocket subscribe/unsubscribe directly in React Changes: 1. useJamServer: Added subscribe/unsubscribe methods using MessageFactory 2. JamServerContext: Exposed subscribe/unsubscribe to React components 3. useSessionWebSocket: Handle SUBSCRIPTION_MESSAGE and dispatch to Redux 4. mediaSlice: Use jamServer.subscribe instead of legacy SubscriptionUtils 5. JKSessionJamTrackPlayer: Pass jamServer to loadJamTrack thunk Now the packaging flow works without relying on legacy jQuery code: - Subscribe: jamServer.subscribe('mixdown', packageId) - Server sends: SUBSCRIPTION_MESSAGE with packaging progress - Handler updates: Redux downloadState via setDownloadState() - Unsubscribe: jamServer.unsubscribe('mixdown', packageId) Co-Authored-By: Claude Sonnet 4.5 --- .../client/JKSessionJamTrackPlayer.js | 15 +++++-- jam-ui/src/context/JamServerContext.js | 6 ++- jam-ui/src/hooks/useJamServer.js | 30 ++++++++++++- jam-ui/src/hooks/useSessionWebSocket.js | 41 ++++++++++++++++-- jam-ui/src/store/features/mediaSlice.js | 42 ++++++------------- 5 files changed, 96 insertions(+), 38 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js index 6ed6336fb..193534d76 100644 --- a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js @@ -9,6 +9,7 @@ import { } from '../../store/features/mediaSlice'; import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice'; import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice'; +import { useJamServerContext } from '../../context/JamServerContext'; // Error types for comprehensive error handling const ERROR_TYPES = { @@ -36,6 +37,9 @@ const JKSessionJamTrackPlayer = ({ const [isOperating, setIsOperating] = useState(false); const [selectedMixdownId, setSelectedMixdownId] = useState(initialMixdownId); + // Context + const { server: jamServer } = useJamServerContext(); + // Redux state const dispatch = useDispatch(); const jamTrackState = useSelector(state => state.media.jamTrackState); @@ -131,7 +135,8 @@ const JKSessionJamTrackPlayer = ({ jamTrack, mixdownId: selectedMixdownId, autoPlay: true, - jamClient + jamClient, + jamServer })).unwrap(); } @@ -289,7 +294,8 @@ const JKSessionJamTrackPlayer = ({ jamTrack, mixdownId, autoPlay: true, - jamClient + jamClient, + jamServer })).unwrap(); } @@ -332,7 +338,8 @@ const JKSessionJamTrackPlayer = ({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, - jamClient + jamClient, + jamServer })).unwrap(); } catch (err) { @@ -359,7 +366,7 @@ const JKSessionJamTrackPlayer = ({ if (jamTrack && jamClient) { const fqId = await buildFqId(); fqIdRef.current = fqId; - await dispatch(loadJamTrack({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, jamClient })); + await dispatch(loadJamTrack({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, jamClient, jamServer })); } } }, [error, isOperating, jamTrack, jamClient, selectedMixdownId, handleRetryDownload, buildFqId, dispatch]); diff --git a/jam-ui/src/context/JamServerContext.js b/jam-ui/src/context/JamServerContext.js index b6bf74baa..2fb109d16 100644 --- a/jam-ui/src/context/JamServerContext.js +++ b/jam-ui/src/context/JamServerContext.js @@ -11,7 +11,9 @@ export const JamServerProvider = ({ children }) => { jamClient, server, registerMessageCallback, - unregisterMessageCallback } = useJamServer(process.env.REACT_APP_WEBSOCKET_GATEWAY_URL); + unregisterMessageCallback, + subscribe, + unsubscribe } = useJamServer(process.env.REACT_APP_WEBSOCKET_GATEWAY_URL); return ( { lastError, registerMessageCallback, unregisterMessageCallback, + subscribe, + unsubscribe, }} > {children} diff --git a/jam-ui/src/hooks/useJamServer.js b/jam-ui/src/hooks/useJamServer.js index 7dbc46e73..9c668ec2d 100644 --- a/jam-ui/src/hooks/useJamServer.js +++ b/jam-ui/src/hooks/useJamServer.js @@ -740,6 +740,32 @@ export default function useJamServer(url) { return connectionStatus === ConnectionStatus.CONNECTED; }, [connectionStatus]); + // Subscribe to WebSocket notifications for a specific resource + const subscribe = useCallback((type, id) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + const subscribeMessage = messageFactory.subscribe(type, id.toString()); + ws.current.send(JSON.stringify(subscribeMessage)); + console.log(`[WebSocket] Subscribed to ${type}:${id}`); + return true; + } else { + console.warn(`[WebSocket] Cannot subscribe, not connected`); + return false; + } + }, []); + + // Unsubscribe from WebSocket notifications for a specific resource + const unsubscribe = useCallback((type, id) => { + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + const unsubscribeMessage = messageFactory.unsubscribe(type, id.toString()); + ws.current.send(JSON.stringify(unsubscribeMessage)); + console.log(`[WebSocket] Unsubscribed from ${type}:${id}`); + return true; + } else { + console.warn(`[WebSocket] Cannot unsubscribe, not connected`); + return false; + } + }, []); + return { // Legacy properties for backward compatibility isConnected, @@ -757,11 +783,13 @@ export default function useJamServer(url) { disconnect, updateNotificationSeen, registerOnSocketClosed, + subscribe, + unsubscribe, isLoggedIn, server: server.current, - + // Callbacks management registerMessageCallback, unregisterMessageCallback, diff --git a/jam-ui/src/hooks/useSessionWebSocket.js b/jam-ui/src/hooks/useSessionWebSocket.js index 406d05f0b..49bce1d8a 100644 --- a/jam-ui/src/hooks/useSessionWebSocket.js +++ b/jam-ui/src/hooks/useSessionWebSocket.js @@ -161,9 +161,44 @@ export const useSessionWebSocket = (sessionId) => { } }, - // Note: SUBSCRIPTION_MESSAGE handling is done via window.JK.SubscriptionUtils - // in the downloadJamTrack thunk (mediaSlice.js). We subscribe to jQuery events - // on the watch object returned by SubscriptionUtils.subscribe(). + // Handle WebSocket subscription notifications (e.g., mixdown packaging progress) + SUBSCRIPTION_MESSAGE: (header, payload) => { + console.log('[WebSocket] Subscription message received:', { header, payload }); + + // Parse the payload body (may be JSON string) + let body; + try { + body = typeof payload.body === 'string' ? JSON.parse(payload.body) : payload.body; + } catch (err) { + console.error('[WebSocket] Failed to parse subscription message body:', err); + return; + } + + // Handle mixdown packaging progress + if (payload.type === 'mixdown' && body) { + dispatch(setDownloadState({ + signing_state: body.signing_state, + packaging_steps: body.packaging_steps || 0, + current_packaging_step: body.current_packaging_step || 0 + })); + + console.log(`[WebSocket] Mixdown packaging: ${body.signing_state}, step ${body.current_packaging_step}/${body.packaging_steps}`); + + // If packaging failed, set error state + if (body.signing_state === 'ERROR' || + body.signing_state === 'SIGNING_TIMEOUT' || + body.signing_state === 'QUEUED_TIMEOUT' || + body.signing_state === 'QUIET_TIMEOUT') { + dispatch(setDownloadState({ + state: 'error', + error: { + type: 'download', + message: `Packaging failed: ${body.signing_state}` + } + })); + } + } + }, // Connection events connectionStatusChanged: (data) => { diff --git a/jam-ui/src/store/features/mediaSlice.js b/jam-ui/src/store/features/mediaSlice.js index 48d314741..8be9f7ddf 100644 --- a/jam-ui/src/store/features/mediaSlice.js +++ b/jam-ui/src/store/features/mediaSlice.js @@ -16,7 +16,7 @@ export const openBackingTrack = createAsyncThunk( export const loadJamTrack = createAsyncThunk( 'media/loadJamTrack', - async ({ jamTrack, mixdownId = null, autoPlay = false, jamClient }, { dispatch, rejectWithValue }) => { + async ({ jamTrack, mixdownId = null, autoPlay = false, jamClient, jamServer }, { dispatch, rejectWithValue }) => { try { // Build fqId const sampleRate = await jamClient.GetSampleRate(); @@ -27,7 +27,7 @@ export const loadJamTrack = createAsyncThunk( // If not synchronized, trigger download if (!trackDetail || !trackDetail.key_state || trackDetail.key_state !== 'AVAILABLE') { - await dispatch(downloadJamTrack({ jamTrack, mixdownId, fqId, jamClient })).unwrap(); + await dispatch(downloadJamTrack({ jamTrack, mixdownId, fqId, jamClient, jamServer })).unwrap(); } // Load JMEP if present @@ -50,7 +50,7 @@ export const loadJamTrack = createAsyncThunk( export const downloadJamTrack = createAsyncThunk( 'media/downloadJamTrack', - async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue, getState }) => { + async ({ jamTrack, mixdownId, fqId, jamClient, jamServer }, { dispatch, rejectWithValue, getState }) => { try { // Get client sample rate for package selection (pickMyPackage logic) @@ -152,39 +152,23 @@ export const downloadJamTrack = createAsyncThunk( } // Subscribe to WebSocket notifications for packaging progress - // This tells the WebSocket gateway to send us SUBSCRIBE_NOTIFICATION messages - // Returns a jQuery watch object that we can listen to for notifications + // WebSocket gateway will send SUBSCRIPTION_MESSAGE updates + // Handler in useSessionWebSocket.js will dispatch setDownloadState updates console.log(`[JamTrack] Subscribing to packaging notifications for package ${packageId}`); - let watch = null; - let handlePackagingNotification = null; - if (window.JK && window.JK.SubscriptionUtils && window.JK.EVENTS) { - watch = window.JK.SubscriptionUtils.subscribe('mixdown', packageId); + if (!jamServer || !jamServer.subscribe) { + throw new Error('WebSocket connection not available'); + } - // Listen to the watch object for packaging progress updates - handlePackagingNotification = (event, data) => { - console.log('[JamTrack] Packaging notification from SubscriptionUtils:', data); - if (data.type === 'mixdown' && data.body) { - dispatch(setDownloadState({ - signing_state: data.body.signing_state, - packaging_steps: data.body.packaging_steps || 0, - current_packaging_step: data.body.current_packaging_step || 0 - })); - } - }; - - watch.on(window.JK.EVENTS.SUBSCRIBE_NOTIFICATION, handlePackagingNotification); - } else { - console.warn('[JamTrack] SubscriptionUtils not available, WebSocket notifications may not work'); + const subscribed = jamServer.subscribe('mixdown', packageId); + if (!subscribed) { + throw new Error('Failed to subscribe to packaging notifications'); } // Cleanup function for unsubscribing const unsubscribeFromPackaging = () => { - if (watch && handlePackagingNotification) { - watch.off(window.JK.EVENTS.SUBSCRIBE_NOTIFICATION, handlePackagingNotification); - } - if (window.JK && window.JK.SubscriptionUtils) { - window.JK.SubscriptionUtils.unsubscribe('mixdown', packageId); + if (jamServer && jamServer.unsubscribe) { + jamServer.unsubscribe('mixdown', packageId); } };