feat(05-jamtrack): implement server-side packaging flow
Implements the missing server-side packaging phase that exists in the
legacy system. JamTracks must be packaged/signed on the server before
they can be downloaded.
**Legacy System Flow (8 states):**
1. initial → 2. packaging → 3. downloading → 4. keying → 5. synchronized
**Previous Implementation (6 states - INCORRECT):**
1. idle → 2. checking → 3. downloading (skipped packaging!)
**New Implementation (7 states - CORRECT):**
1. idle → 2. checking → 3. packaging → 4. downloading → 5. keying → 6. synchronized
Changes:
1. REST API (rest.js):
- Added enqueueMixdown(packageId) endpoint
- POST /mixdowns/{packageId}/enqueue
2. Redux State (mediaSlice.js):
- Added 'packaging' state to downloadState
- Added packageId, packaging_steps, current_packaging_step, signing_state fields
- Updated clearDownloadState to reset new fields
3. Download Logic (mediaSlice.js downloadJamTrack thunk):
- Added packaging phase before download
- Calls enqueueMixdown API
- Waits for signing_state === 'SIGNED' (polls Redux state every 500ms)
- 60 second timeout with clear error messages
- Only proceeds to download after package is signed
4. WebSocket Handler (useSessionWebSocket.js):
- Added SUBSCRIBE_NOTIFICATION handler
- Updates packaging progress: signing_state, packaging_steps, current_packaging_step
- Handles packaging errors: ERROR, SIGNING_TIMEOUT, QUEUED_TIMEOUT, QUIET_TIMEOUT
- Logs packaging progress for debugging
5. UI (JKSessionJamTrackPlayer.js):
- Added packaging state display
- Shows "Your JamTrack is currently being created in the JamKazam server"
- Displays signing_state and step progress (X of Y)
- Shows spinner during packaging
- Matches legacy system UX
This fixes the critical issue where JamTracks were attempted to be
downloaded without server-side packaging, which would always fail.
The flow now matches the legacy download_jamtrack.js.coffee behavior
documented in .planning/codebase/JAMTRACK_LEGACY.md lines 57-85.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5fd80ae007
commit
00e2f9cabd
|
|
@ -43,7 +43,8 @@
|
|||
"Skill(gsd:execute-plan)",
|
||||
"Skill(gsd:plan-phase)",
|
||||
"Skill(gsd:progress)",
|
||||
"Bash(git rev-parse:*)"
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(node --check:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,11 +487,32 @@ const JKSessionJamTrackPlayer = ({
|
|||
<div style={{ background: '#f0f8ff', padding: '15px', marginBottom: '10px', border: '1px solid #cce7ff' }}>
|
||||
<h4>
|
||||
{downloadState.state === 'checking' && 'Checking sync status...'}
|
||||
{downloadState.state === 'packaging' && 'Your JamTrack is currently being created in the JamKazam server'}
|
||||
{downloadState.state === 'downloading' && 'Downloading JamTrack...'}
|
||||
{downloadState.state === 'keying' && 'Requesting decryption keys...'}
|
||||
{downloadState.state === 'error' && 'Download Failed'}
|
||||
</h4>
|
||||
|
||||
{downloadState.state === 'packaging' && (
|
||||
<div>
|
||||
<p style={{ marginTop: '10px', fontSize: '14px' }}>
|
||||
{downloadState.signing_state === 'SIGNED' && 'Package ready, starting download...'}
|
||||
{downloadState.signing_state !== 'SIGNED' && downloadState.signing_state && `Status: ${downloadState.signing_state}`}
|
||||
{!downloadState.signing_state && 'Preparing your JamTrack...'}
|
||||
</p>
|
||||
{downloadState.packaging_steps > 0 && (
|
||||
<p style={{ fontSize: '14px', marginTop: '5px' }}>
|
||||
Step {downloadState.current_packaging_step} of {downloadState.packaging_steps}
|
||||
</p>
|
||||
)}
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<div className="spinner-border spinner-border-sm" role="status">
|
||||
<span className="sr-only">Packaging...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{downloadState.state === 'downloading' && (
|
||||
<div>
|
||||
<progress
|
||||
|
|
|
|||
|
|
@ -558,6 +558,16 @@ export const getJamTrackBySlug = options => {
|
|||
});
|
||||
}
|
||||
|
||||
export const enqueueMixdown = (packageId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/mixdowns/${packageId}/enqueue`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const addJamtrackToShoppingCart = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/shopping_carts/add_jamtrack`, {
|
||||
|
|
|
|||
|
|
@ -161,6 +161,38 @@ export const useSessionWebSocket = (sessionId) => {
|
|||
}
|
||||
},
|
||||
|
||||
// Phase 5 Fix: Handle packaging notifications from server
|
||||
// Listens for mixdown package creation/signing progress
|
||||
SUBSCRIBE_NOTIFICATION: (data) => {
|
||||
console.log('[JamTrack] Packaging notification received:', data);
|
||||
|
||||
// Update packaging progress in Redux
|
||||
// data.body contains: signing_state, packaging_steps, current_packaging_step
|
||||
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
|
||||
}));
|
||||
|
||||
console.log(`[JamTrack] Packaging progress: ${data.body.signing_state}, step ${data.body.current_packaging_step}/${data.body.packaging_steps}`);
|
||||
|
||||
// If packaging failed, set error state
|
||||
if (data.body.signing_state === 'ERROR' ||
|
||||
data.body.signing_state === 'SIGNING_TIMEOUT' ||
|
||||
data.body.signing_state === 'QUEUED_TIMEOUT' ||
|
||||
data.body.signing_state === 'QUIET_TIMEOUT') {
|
||||
dispatch(setDownloadState({
|
||||
state: 'error',
|
||||
error: {
|
||||
type: 'download',
|
||||
message: `Packaging failed: ${data.body.signing_state}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Connection events
|
||||
connectionStatusChanged: (data) => {
|
||||
console.log('Connection status changed:', data);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { enqueueMixdown } from '../../helpers/rest';
|
||||
|
||||
// Async thunks for media actions
|
||||
export const openBackingTrack = createAsyncThunk(
|
||||
|
|
@ -49,15 +50,8 @@ export const loadJamTrack = createAsyncThunk(
|
|||
|
||||
export const downloadJamTrack = createAsyncThunk(
|
||||
'media/downloadJamTrack',
|
||||
async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue }) => {
|
||||
async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue, getState }) => {
|
||||
try {
|
||||
dispatch(setDownloadState({
|
||||
jamTrackId: jamTrack.id,
|
||||
mixdownId,
|
||||
fqId,
|
||||
state: 'downloading',
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
// Get client sample rate for package selection (pickMyPackage logic)
|
||||
const rawSampleRate = await jamClient.GetSampleRate();
|
||||
|
|
@ -126,6 +120,81 @@ export const downloadJamTrack = createAsyncThunk(
|
|||
|
||||
const packageId = compatiblePackage.id;
|
||||
|
||||
// ========== PACKAGING PHASE ==========
|
||||
// Before downloading, package must be created/signed on server
|
||||
// This matches legacy download_jamtrack.js.coffee behavior
|
||||
|
||||
dispatch(setDownloadState({
|
||||
jamTrackId: jamTrack.id,
|
||||
mixdownId,
|
||||
fqId,
|
||||
packageId,
|
||||
state: 'packaging',
|
||||
progress: 0,
|
||||
packaging_steps: 0,
|
||||
current_packaging_step: 0,
|
||||
signing_state: null
|
||||
}));
|
||||
|
||||
// Enqueue mixdown for server-side packaging
|
||||
console.log(`[JamTrack] Enqueueing package ${packageId} for packaging`);
|
||||
try {
|
||||
await enqueueMixdown(packageId);
|
||||
} catch (err) {
|
||||
console.error('[JamTrack] Failed to enqueue mixdown:', err);
|
||||
throw new Error(`Failed to start packaging: ${err.message}`);
|
||||
}
|
||||
|
||||
// Wait for packaging to complete (signing_state === 'SIGNED')
|
||||
// WebSocket handler will update downloadState as packaging progresses
|
||||
console.log('[JamTrack] Waiting for package to be signed by server...');
|
||||
|
||||
const waitForPackaging = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Packaging timeout after 60 seconds'));
|
||||
}, 60000); // 60 second timeout
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
const state = getState();
|
||||
const { signing_state, state: downloadStateValue } = state.media.downloadState;
|
||||
|
||||
// Check for success
|
||||
if (signing_state === 'SIGNED') {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(checkInterval);
|
||||
console.log('[JamTrack] Package signed successfully, proceeding to download');
|
||||
resolve();
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (signing_state === 'ERROR' || signing_state === 'SIGNING_TIMEOUT' ||
|
||||
signing_state === 'QUEUED_TIMEOUT' || signing_state === 'QUIET_TIMEOUT') {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(checkInterval);
|
||||
reject(new Error(`Packaging failed with state: ${signing_state}`));
|
||||
}
|
||||
|
||||
// Check if download state shows error
|
||||
if (downloadStateValue === 'error') {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(checkInterval);
|
||||
reject(new Error('Packaging failed'));
|
||||
}
|
||||
}, 500); // Check every 500ms
|
||||
});
|
||||
};
|
||||
|
||||
await waitForPackaging();
|
||||
|
||||
// ========== DOWNLOAD PHASE ==========
|
||||
// Package is now signed and ready to download
|
||||
|
||||
dispatch(setDownloadState({
|
||||
state: 'downloading',
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
// Set up global callbacks for native client
|
||||
window.jamTrackDownloadProgress = (percent) => {
|
||||
dispatch(setDownloadState({ progress: percent }));
|
||||
|
|
@ -266,10 +335,15 @@ const initialState = {
|
|||
jamTrackId: null,
|
||||
mixdownId: null,
|
||||
fqId: null,
|
||||
state: 'idle', // 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error'
|
||||
packageId: null, // Package being packaged/downloaded
|
||||
state: 'idle', // 'idle' | 'checking' | 'packaging' | 'downloading' | 'keying' | 'synchronized' | 'error'
|
||||
progress: 0,
|
||||
currentStep: 0,
|
||||
totalSteps: 0,
|
||||
// Packaging-specific fields (from legacy JamTrackStore)
|
||||
packaging_steps: 0, // Total packaging steps
|
||||
current_packaging_step: 0, // Current step in packaging
|
||||
signing_state: null, // 'SIGNING_TIMEOUT' | 'QUEUED_TIMEOUT' | 'QUIET_TIMEOUT' | 'ERROR' | 'SIGNED' | null
|
||||
error: null
|
||||
},
|
||||
|
||||
|
|
@ -331,10 +405,14 @@ export const mediaSlice = createSlice({
|
|||
jamTrackId: null,
|
||||
mixdownId: null,
|
||||
fqId: null,
|
||||
packageId: null,
|
||||
state: 'idle',
|
||||
progress: 0,
|
||||
currentStep: 0,
|
||||
totalSteps: 0,
|
||||
packaging_steps: 0,
|
||||
current_packaging_step: 0,
|
||||
signing_state: null,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue