Files
PrivyDrop/frontend/lib/receive/ProgressReporter.ts
T
2025-09-14 08:36:20 +08:00

309 lines
9.3 KiB
TypeScript

import { SpeedCalculator } from "@/lib/speedCalculator";
import { ReceptionStateManager } from "./ReceptionStateManager";
import { ReceptionConfig } from "./ReceptionConfig";
import { postLogToBackend } from "@/app/config/api";
const developmentEnv = process.env.NODE_ENV;
/**
* 🚀 Progress callback type
*/
export type ProgressCallback = (fileId: string, progress: number, speed: number) => void;
/**
* 🚀 Progress statistics interface
*/
export interface ProgressStats {
fileProgress: Record<string, number>;
folderProgress: Record<string, number>;
currentSpeed: number;
averageSpeed: number;
totalBytesReceived: number;
estimatedTimeRemaining: number | null;
}
/**
* 🚀 Progress reporter
* Handles progress calculation, speed tracking, and progress callback management
*/
export class ProgressReporter {
private speedCalculator: SpeedCalculator;
private progressCallback: ProgressCallback | null = null;
// Progress tracking
private fileProgressMap = new Map<string, number>();
private folderProgressMap = new Map<string, number>();
private lastProgressUpdate = new Map<string, number>();
constructor(private stateManager: ReceptionStateManager) {
this.speedCalculator = new SpeedCalculator();
}
/**
* Set progress callback
*/
setProgressCallback(callback: ProgressCallback): void {
this.progressCallback = callback;
}
/**
* Update file reception progress
*/
updateFileProgress(
byteLength: number,
fileId: string,
fileSize: number
): void {
const peerId = this.stateManager.getCurrentPeerId();
if (!peerId) return;
const activeReception = this.stateManager.getActiveFileReception();
if (!activeReception) return;
// Update received size
activeReception.receivedSize += byteLength;
const totalReceived = activeReception.initialOffset + activeReception.receivedSize;
const currentFolderName = this.stateManager.getCurrentFolderName();
if (currentFolderName) {
// Update folder progress
this.updateFolderProgress(currentFolderName, byteLength, peerId);
} else {
// Update individual file progress
this.speedCalculator.updateSendSpeed(peerId, totalReceived);
const speed = this.speedCalculator.getSendSpeed(peerId);
const progress = fileSize > 0 ? totalReceived / fileSize : 0;
// Store progress for statistics
this.fileProgressMap.set(fileId, progress);
// Throttle progress callbacks to avoid overwhelming the UI
const now = Date.now();
const lastUpdate = this.lastProgressUpdate.get(fileId) || 0;
const shouldUpdate = now - lastUpdate > 100; // Update at most every 100ms
if (shouldUpdate || progress >= 1) {
this.progressCallback?.(fileId, progress, speed);
this.lastProgressUpdate.set(fileId, now);
}
if (ReceptionConfig.DEBUG_CONFIG.ENABLE_PROGRESS_LOGGING && progress >= 1) {
postLogToBackend(
`[DEBUG] 📈 File progress 100% - ${fileId}, speed: ${(speed / 1024 / 1024).toFixed(1)}MB/s`
);
}
}
}
/**
* Update folder reception progress
*/
private updateFolderProgress(
folderName: string,
byteLength: number,
peerId: string
): void {
// Update folder received size in state manager
this.stateManager.updateFolderReceivedSize(folderName, byteLength);
const folderProgress = this.stateManager.getFolderProgress(folderName);
if (!folderProgress) return;
this.speedCalculator.updateSendSpeed(peerId, folderProgress.receivedSize);
const speed = this.speedCalculator.getSendSpeed(peerId);
const progress = folderProgress.totalSize > 0
? folderProgress.receivedSize / folderProgress.totalSize
: 0;
// Store progress for statistics
this.folderProgressMap.set(folderName, progress);
// Throttle folder progress callbacks
const now = Date.now();
const lastUpdate = this.lastProgressUpdate.get(folderName) || 0;
const shouldUpdate = now - lastUpdate > 200; // Update folders less frequently
if (shouldUpdate || progress >= 1) {
this.progressCallback?.(folderName, progress, speed);
this.lastProgressUpdate.set(folderName, now);
}
if (ReceptionConfig.DEBUG_CONFIG.ENABLE_PROGRESS_LOGGING && progress >= 1) {
postLogToBackend(
`[DEBUG] 📈 Folder progress 100% - ${folderName}, speed: ${(speed / 1024 / 1024).toFixed(1)}MB/s`
);
}
}
/**
* Report file completion (100% progress)
*/
reportFileComplete(fileId: string): void {
if (!this.progressCallback) return;
const peerId = this.stateManager.getCurrentPeerId();
if (!peerId) return;
// Get final speed and report 100% progress
const speed = this.speedCalculator.getSendSpeed(peerId);
this.progressCallback(fileId, 1, speed);
this.fileProgressMap.set(fileId, 1);
if (ReceptionConfig.DEBUG_CONFIG.ENABLE_PROGRESS_LOGGING) {
postLogToBackend(
`[DEBUG] ✅ File completion reported - ${fileId}, final speed: ${(speed / 1024 / 1024).toFixed(1)}MB/s`
);
}
}
/**
* Report folder completion (100% progress)
*/
reportFolderComplete(folderName: string): void {
if (!this.progressCallback) return;
const peerId = this.stateManager.getCurrentPeerId();
if (!peerId) return;
// Get final speed and report 100% progress
const speed = this.speedCalculator.getSendSpeed(peerId);
this.progressCallback(folderName, 1, speed);
this.folderProgressMap.set(folderName, 1);
if (ReceptionConfig.DEBUG_CONFIG.ENABLE_PROGRESS_LOGGING) {
postLogToBackend(
`[DEBUG] ✅ Folder completion reported - ${folderName}, final speed: ${(speed / 1024 / 1024).toFixed(1)}MB/s`
);
}
}
/**
* Get current progress for a file or folder
*/
getCurrentProgress(id: string): number {
return this.fileProgressMap.get(id) || this.folderProgressMap.get(id) || 0;
}
/**
* Get current speed for peer
*/
getCurrentSpeed(): number {
const peerId = this.stateManager.getCurrentPeerId();
return peerId ? this.speedCalculator.getSendSpeed(peerId) : 0;
}
/**
* Get detailed progress statistics
*/
getProgressStats(): ProgressStats {
const peerId = this.stateManager.getCurrentPeerId();
const currentSpeed = peerId ? this.speedCalculator.getSendSpeed(peerId) : 0;
const averageSpeed = currentSpeed; // SpeedCalculator doesn't have getAverageSpeed method
// Calculate total bytes received
let totalBytesReceived = 0;
const activeReception = this.stateManager.getActiveFileReception();
if (activeReception) {
totalBytesReceived = activeReception.initialOffset + activeReception.receivedSize;
}
// Estimate time remaining
let estimatedTimeRemaining: number | null = null;
if (activeReception && currentSpeed > 0) {
const remainingBytes = activeReception.meta.size - totalBytesReceived;
if (remainingBytes > 0) {
estimatedTimeRemaining = remainingBytes / currentSpeed; // seconds
}
}
const fileProgress: Record<string, number> = {};
this.fileProgressMap.forEach((progress, fileId) => {
fileProgress[fileId] = progress;
});
const folderProgress: Record<string, number> = {};
this.folderProgressMap.forEach((progress, folderName) => {
folderProgress[folderName] = progress;
});
return {
fileProgress,
folderProgress,
currentSpeed,
averageSpeed,
totalBytesReceived,
estimatedTimeRemaining,
};
}
/**
* Reset progress for a specific file or folder
*/
resetProgress(id: string): void {
this.fileProgressMap.delete(id);
this.folderProgressMap.delete(id);
this.lastProgressUpdate.delete(id);
}
/**
* Reset all progress data
*/
resetAllProgress(): void {
this.fileProgressMap.clear();
this.folderProgressMap.clear();
this.lastProgressUpdate.clear();
// Reset speed calculator for current peer
// Note: SpeedCalculator doesn't have resetSpeed method, so we create a new instance
this.speedCalculator = new SpeedCalculator();
}
/**
* Get progress update frequency (for debugging)
*/
getUpdateFrequency(id: string): number {
const lastUpdate = this.lastProgressUpdate.get(id);
return lastUpdate ? Date.now() - lastUpdate : 0;
}
/**
* Check if progress should be throttled
*/
shouldThrottleProgress(id: string, isFolder: boolean = false): boolean {
const now = Date.now();
const lastUpdate = this.lastProgressUpdate.get(id) || 0;
const threshold = isFolder ? 200 : 100; // Folders update less frequently
return now - lastUpdate < threshold;
}
/**
* Force progress update (bypass throttling)
*/
forceProgressUpdate(id: string, progress: number): void {
if (!this.progressCallback) return;
const speed = this.getCurrentSpeed();
this.progressCallback(id, progress, speed);
this.lastProgressUpdate.set(id, Date.now());
// Update internal maps
if (this.fileProgressMap.has(id)) {
this.fileProgressMap.set(id, progress);
} else if (this.folderProgressMap.has(id)) {
this.folderProgressMap.set(id, progress);
}
}
/**
* Clean up resources
*/
cleanup(): void {
this.resetAllProgress();
this.progressCallback = null;
if (ReceptionConfig.DEBUG_CONFIG.ENABLE_PROGRESS_LOGGING) {
postLogToBackend("[DEBUG] 🧹 ProgressReporter cleaned up");
}
}
}