veza/apps/web/src/services/sync-manager.ts
2025-12-03 22:56:50 +01:00

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();