diff --git a/src/utils/sync-progress.ts b/src/utils/sync-progress.ts new file mode 100644 index 00000000..afee9b20 --- /dev/null +++ b/src/utils/sync-progress.ts @@ -0,0 +1,162 @@ +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, + }; + } +}