163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
|
|
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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|