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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-15 17:15:38 +05:30
parent 12527c4eb1
commit 965dc2d708
5 changed files with 96 additions and 38 deletions

View File

@ -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]);

View File

@ -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 (
<JamServerContext.Provider value={{
@ -24,6 +26,8 @@ export const JamServerProvider = ({ children }) => {
lastError,
registerMessageCallback,
unregisterMessageCallback,
subscribe,
unsubscribe,
}}
>
{children}

View File

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

View File

@ -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) => {

View File

@ -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);
}
};