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:
parent
12527c4eb1
commit
965dc2d708
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue