214 lines
5.8 KiB
TypeScript
214 lines
5.8 KiB
TypeScript
/**
|
|
* Sync Manager for offline actions
|
|
* Handles synchronization of queued actions when connection is restored
|
|
*/
|
|
|
|
import { offlineStorage, type QueuedAction } from './offline-storage';
|
|
import { secureAuthService } from './secure-auth';
|
|
|
|
class SyncManager {
|
|
private isSyncing = false;
|
|
private syncInProgress = false;
|
|
|
|
/**
|
|
* Start sync process
|
|
*/
|
|
async startSync(): Promise<void> {
|
|
if (this.syncInProgress) {
|
|
console.log('Sync already in progress');
|
|
return;
|
|
}
|
|
|
|
this.syncInProgress = true;
|
|
console.log('Starting sync of offline actions...');
|
|
|
|
try {
|
|
// Get all pending actions
|
|
const actions = await offlineStorage.getAll<QueuedAction>('queued_actions');
|
|
const pendingActions = actions.filter((a) => a.status === 'pending');
|
|
|
|
console.log(`Found ${pendingActions.length} pending actions to sync`);
|
|
|
|
// Process each action
|
|
for (const action of pendingActions) {
|
|
await this.processAction(action);
|
|
}
|
|
|
|
console.log('Sync completed successfully');
|
|
} catch (error) {
|
|
console.error('Sync failed:', error);
|
|
throw error;
|
|
} finally {
|
|
this.syncInProgress = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a single action
|
|
*/
|
|
private async processAction(action: QueuedAction): Promise<void> {
|
|
try {
|
|
console.log(`Processing action ${action.id} of type ${action.type}`);
|
|
|
|
// Update status to processing
|
|
await offlineStorage.update('queued_actions', {
|
|
...action,
|
|
status: 'processing',
|
|
});
|
|
|
|
// Execute action based on type
|
|
let success = false;
|
|
switch (action.type) {
|
|
case 'send_message':
|
|
success = await this.syncMessage(action);
|
|
break;
|
|
case 'upload_track':
|
|
success = await this.syncUpload(action);
|
|
break;
|
|
case 'update_profile':
|
|
success = await this.syncProfileUpdate(action);
|
|
break;
|
|
default:
|
|
console.warn(`Unknown action type: ${action.type}`);
|
|
}
|
|
|
|
if (success) {
|
|
// Mark as completed and remove from queue
|
|
await offlineStorage.update('queued_actions', {
|
|
...action,
|
|
status: 'completed',
|
|
});
|
|
console.log(`Action ${action.id} synced successfully`);
|
|
} else {
|
|
throw new Error('Action failed');
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to sync action ${action.id}:`, error);
|
|
|
|
// Increment retry count
|
|
await offlineStorage.update('queued_actions', {
|
|
...action,
|
|
retries: action.retries + 1,
|
|
status: action.retries + 1 >= 3 ? 'failed' : 'pending',
|
|
});
|
|
|
|
if (action.retries >= 3) {
|
|
console.error(`Action ${action.id} failed after 3 retries`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync a message action
|
|
*/
|
|
private async syncMessage(action: QueuedAction): Promise<boolean> {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/chat/messages`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(action.payload),
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error('Failed to sync message:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync an upload action
|
|
*/
|
|
private async syncUpload(action: QueuedAction): Promise<boolean> {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
|
|
|
|
try {
|
|
// Get file from storage
|
|
const fileData = await offlineStorage.get('uploaded_files', action.payload.fileId);
|
|
if (!fileData) {
|
|
console.error('File not found in storage');
|
|
return false;
|
|
}
|
|
|
|
// Upload file
|
|
const formData = new FormData();
|
|
formData.append('file', fileData.file);
|
|
formData.append('title', action.payload.title);
|
|
formData.append('artist', action.payload.artist);
|
|
|
|
const response = await fetch(`${API_BASE_URL}/upload`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: formData,
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error('Failed to sync upload:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync a profile update action
|
|
*/
|
|
private async syncProfileUpdate(action: QueuedAction): Promise<boolean> {
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/profile`, {
|
|
method: 'PATCH',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(action.payload),
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error('Failed to sync profile update:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get sync status
|
|
*/
|
|
async getSyncStatus(): Promise<{
|
|
pending: number;
|
|
processing: number;
|
|
failed: number;
|
|
}> {
|
|
const actions = await offlineStorage.getAll<QueuedAction>('queued_actions');
|
|
|
|
return {
|
|
pending: actions.filter((a) => a.status === 'pending').length,
|
|
processing: actions.filter((a) => a.status === 'processing').length,
|
|
failed: actions.filter((a) => a.status === 'failed').length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear failed actions
|
|
*/
|
|
async clearFailed(): Promise<void> {
|
|
const actions = await offlineStorage.getAll<QueuedAction>('queued_actions');
|
|
const failedActions = actions.filter((a) => a.status === 'failed');
|
|
|
|
for (const action of failedActions) {
|
|
await offlineStorage.delete('queued_actions', action.id);
|
|
}
|
|
|
|
console.log(`Cleared ${failedActions.length} failed actions`);
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const syncManager = new SyncManager();
|
|
|