import * as fs from "fs"; import * as path from "path"; /** * Interface for sync progress state * Tracks the progress of batch sync operations */ export interface SyncProgressState { lastSyncedIndex: number; totalProfiles: number; startTime: number; lastUpdate: number; failedProfiles: Array<{ index: number; profileId: string; error: string }>; profileIds: string[]; // Track order of profile IDs for resume } /** * SyncProgressManager * Manages persistent storage of sync progress state * Enables resuming from checkpoints after failures */ export class SyncProgressManager { private static readonly FILE_PATH = ".sync-progress.json"; /** * Save progress state to file * @param state - Current progress state to save */ static save(state: SyncProgressState): void { try { const filePath = path.resolve(process.cwd(), this.FILE_PATH); state.lastUpdate = Date.now(); fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8"); } catch (error) { console.error("[SyncProgressManager] Error saving progress:", error); } } /** * Load progress state from file * @returns Progress state if exists, null otherwise */ static load(): SyncProgressState | null { try { const filePath = path.resolve(process.cwd(), this.FILE_PATH); if (!fs.existsSync(filePath)) { return null; } const data = fs.readFileSync(filePath, "utf-8"); const state = JSON.parse(data) as SyncProgressState; // Validate required fields if ( typeof state.lastSyncedIndex !== "number" || typeof state.totalProfiles !== "number" || !Array.isArray(state.failedProfiles) || !Array.isArray(state.profileIds) ) { console.warn("[SyncProgressManager] Invalid progress file, starting fresh"); return null; } return state; } catch (error) { console.error("[SyncProgressManager] Error loading progress:", error); return null; } } /** * Clear progress file * Called on successful completion or when starting fresh */ static clear(): void { try { const filePath = path.resolve(process.cwd(), this.FILE_PATH); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log("[SyncProgressManager] Progress file cleared"); } } catch (error) { console.error("[SyncProgressManager] Error clearing progress:", error); } } /** * Get progress percentage * @param state - Current progress state * @returns Percentage complete (0-100) */ static getProgressPercent(state: SyncProgressState): number { if (state.totalProfiles === 0) return 0; return Math.round((state.lastSyncedIndex / state.totalProfiles) * 100); } /** * Format elapsed time as readable string * @param startTime - Start timestamp in milliseconds * @returns Formatted time string (e.g., "2h 30m 15s") */ static formatElapsedTime(startTime: number): string { const elapsed = Date.now() - startTime; const seconds = Math.floor(elapsed / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const parts: string[] = []; if (hours > 0) parts.push(`${hours}h`); if (minutes % 60 > 0) parts.push(`${minutes % 60}m`); if (seconds % 60 > 0 || parts.length === 0) parts.push(`${seconds % 60}s`); return parts.join(" "); } /** * Log current progress to console * @param state - Current progress state */ static logProgress(state: SyncProgressState): void { const percent = this.getProgressPercent(state); const elapsed = this.formatElapsedTime(state.startTime); const failed = state.failedProfiles.length; console.log( `[Sync Progress] ${percent}% (${state.lastSyncedIndex}/${state.totalProfiles}) | Elapsed: ${elapsed} | Failed: ${failed}`, ); } /** * Add failed profile to state * @param state - Progress state to update * @param index - Profile index * @param profileId - Profile ID that failed * @param error - Error message */ static addFailedProfile( state: SyncProgressState, index: number, profileId: string, error: string, ): void { state.failedProfiles.push({ index, profileId, error }); this.save(state); } /** * Initialize new progress state * @param profileIds - Array of profile IDs to sync * @returns New progress state */ static initialize(profileIds: string[]): SyncProgressState { return { lastSyncedIndex: 0, totalProfiles: profileIds.length, startTime: Date.now(), lastUpdate: Date.now(), failedProfiles: [], profileIds, }; } }