import { CustomFile } from "@/types/webrtc"; import { TransferConfig } from "./TransferConfig"; import { postLogToBackend } from "@/app/config/api"; const developmentEnv = process.env.NODE_ENV; /** * 🚀 Network chunk interface */ export interface NetworkChunk { chunk: ArrayBuffer | null; chunkIndex: number; totalChunks: number; fileOffset: number; isLastChunk: boolean; } /** * 🚀 High-performance streaming file reader * Uses a two-layer buffering architecture: large batch reading + small network chunk sending * Solves file reading performance bottleneck issues */ export class StreamingFileReader { // Configuration parameters private readonly BATCH_SIZE = TransferConfig.FILE_CONFIG.CHUNK_SIZE * TransferConfig.FILE_CONFIG.BATCH_SIZE; // 32MB batches private readonly NETWORK_CHUNK_SIZE = TransferConfig.FILE_CONFIG.NETWORK_CHUNK_SIZE; // 64KB network chunks private readonly CHUNKS_PER_BATCH = this.BATCH_SIZE / this.NETWORK_CHUNK_SIZE; // 512 chunks // File state private file: File; private fileReader: FileReader; private totalFileSize: number; // Batch buffering state private currentBatch: ArrayBuffer | null = null; // Current 32MB batch data private currentBatchStartOffset = 0; // Starting position of current batch in file private currentChunkIndexInBatch = 0; // Index of current network chunk in batch // Global state private totalFileOffset = 0; // Current position in the entire file private isFinished = false; private isReading = false; // Prevent concurrent reading constructor(file: CustomFile, startOffset: number = 0) { this.file = file; this.totalFileSize = file.size; this.totalFileOffset = startOffset; // 🔧 修复:续传时currentBatchStartOffset应该从startOffset开始 this.currentBatchStartOffset = startOffset; this.fileReader = new FileReader(); if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📖 StreamingFileReader created - file: ${file.name}, size: ${( this.totalFileSize / 1024 / 1024 ).toFixed(1)}MB` ); // 🔍 调试续传初始化 const expectedGlobalChunk = Math.floor( startOffset / this.NETWORK_CHUNK_SIZE ); postLogToBackend( `[DEBUG-RESUME] 🏗️ StreamingFileReader created - totalFileOffset:${this.totalFileOffset}, currentBatchStartOffset:${this.currentBatchStartOffset}, expectedGlobalChunk:${expectedGlobalChunk}` ); } } /** * 🎯 Core method: Get next 64KB network chunk */ async getNextNetworkChunk(): Promise { // 1. Check if new batch needs to be loaded if (this.needsNewBatch()) { await this.loadNextBatch(); } // 2. Check if end of file has been reached if (this.isFinished || !this.currentBatch) { return { chunk: null, chunkIndex: this.calculateGlobalChunkIndex(), totalChunks: this.calculateTotalNetworkChunks(), fileOffset: this.totalFileOffset, isLastChunk: true, }; } // 3. Slice 64KB network chunk from current batch const networkChunk = this.sliceNetworkChunkFromBatch(); const globalChunkIndex = this.calculateGlobalChunkIndex(); const isLast = this.isLastNetworkChunk(networkChunk); // 4. Update state this.updateChunkState(networkChunk); // Delete frequent chunk progress logs // 🔍 调试chunk发送 (前5个和最后5个chunks) const totalChunks = this.calculateTotalNetworkChunks(); const isLastFew = globalChunkIndex >= (totalChunks - 5); if (developmentEnv === "development" && (globalChunkIndex <= 5 || isLastFew || isLast)) { postLogToBackend( `[DEBUG-CHUNKS] 📤 Send chunk #${globalChunkIndex}/${totalChunks} - size:${networkChunk.byteLength}, isLast:${isLast}, fileOffset:${this.totalFileOffset - networkChunk.byteLength}` ); } return { chunk: networkChunk, chunkIndex: globalChunkIndex, totalChunks: this.calculateTotalNetworkChunks(), fileOffset: this.totalFileOffset - networkChunk.byteLength, isLastChunk: isLast, }; } /** * 🔍 Determine if new batch needs to be loaded */ private needsNewBatch(): boolean { return ( this.currentBatch === null || // No batch loaded yet this.currentChunkIndexInBatch >= this.CHUNKS_PER_BATCH || // Current batch exhausted this.isCurrentBatchEmpty() // Current batch has no data ); } /** * 🔍 Check if current batch is empty */ private isCurrentBatchEmpty(): boolean { if (!this.currentBatch) return true; const usedBytes = this.currentChunkIndexInBatch * this.NETWORK_CHUNK_SIZE; return usedBytes >= this.currentBatch.byteLength; } /** * 📥 Load next 32MB batch into memory */ private async loadNextBatch(): Promise { if (this.isReading) { // Prevent concurrent reading while (this.isReading) { await new Promise((resolve) => setTimeout(resolve, 10)); } return; } this.isReading = true; const startTime = performance.now(); try { // 1. Clean up old batch memory this.currentBatch = null; // 2. Calculate size to read this time const remainingFileSize = this.totalFileSize - this.totalFileOffset; const batchSize = Math.min(this.BATCH_SIZE, remainingFileSize); if (batchSize <= 0) { this.isFinished = true; return; } // 3. Perform large chunk file reading const sliceStartTime = performance.now(); const fileSlice = this.file.slice( this.totalFileOffset, this.totalFileOffset + batchSize ); const sliceTime = performance.now() - sliceStartTime; // 4. Asynchronously read file data const readStartTime = performance.now(); this.currentBatch = await this.readFileSlice(fileSlice); const readTime = performance.now() - readStartTime; const batchStartOffset = this.totalFileOffset; this.currentBatchStartOffset = batchStartOffset; // 🔧 修复:如果不是从batch边界开始,说明是续传情况,需要计算正确的batch内索引 if (batchStartOffset % this.BATCH_SIZE !== 0) { // 续传情况:不是从batch边界开始 const globalChunkIndex = Math.floor( batchStartOffset / this.NETWORK_CHUNK_SIZE ); this.currentChunkIndexInBatch = globalChunkIndex % this.CHUNKS_PER_BATCH; } else { // 正常情况:从batch边界开始 this.currentChunkIndexInBatch = 0; } // Only output batch reading logs in development environment if (developmentEnv === "development") { const totalTime = performance.now() - startTime; const speedMBps = batchSize / 1024 / 1024 / (totalTime / 1000); postLogToBackend( `[DEBUG] 📖 BATCH_READ - size: ${(batchSize / 1024 / 1024).toFixed( 1 )}MB, time: ${totalTime.toFixed(0)}ms, speed: ${speedMBps.toFixed( 1 )}MB/s` ); // 🔍 调试batch内索引设置 postLogToBackend( `[DEBUG-RESUME] 📖 BATCH loaded - batchStartOffset:${batchStartOffset}, currentChunkIndexInBatch:${ this.currentChunkIndexInBatch }, isResume:${batchStartOffset % this.BATCH_SIZE !== 0}` ); } } catch (error) { if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ BATCH_READ failed: ${error}`); } throw new Error(`Failed to load file batch: ${error}`); } finally { this.isReading = false; } } /** * 📄 Perform file reading operation */ private async readFileSlice(fileSlice: Blob): Promise { return new Promise((resolve, reject) => { this.fileReader.onload = () => { const result = this.fileReader.result as ArrayBuffer; if (result) { resolve(result); } else { reject(new Error("FileReader result is null")); } }; this.fileReader.onerror = () => { reject( new Error( `File reading failed: ${ this.fileReader.error?.message || "Unknown error" }` ) ); }; this.fileReader.readAsArrayBuffer(fileSlice); }); } /** * ✂️ Slice 64KB network chunk from 32MB batch */ private sliceNetworkChunkFromBatch(): ArrayBuffer { if (!this.currentBatch) { throw new Error("No current batch available for slicing"); } const chunkStartInBatch = this.currentChunkIndexInBatch * this.NETWORK_CHUNK_SIZE; const remainingInBatch = this.currentBatch.byteLength - chunkStartInBatch; const chunkSize = Math.min(this.NETWORK_CHUNK_SIZE, remainingInBatch); if (chunkSize <= 0) { throw new Error("Invalid chunk size calculated"); } const networkChunk = this.currentBatch.slice( chunkStartInBatch, chunkStartInBatch + chunkSize ); // Delete frequent slice logs, only output when needed return networkChunk; } /** * 📊 Calculate global network chunk index */ private calculateGlobalChunkIndex(): number { const batchesBefore = Math.floor( this.currentBatchStartOffset / this.BATCH_SIZE ); const chunksInPreviousBatches = batchesBefore * this.CHUNKS_PER_BATCH; const result = chunksInPreviousBatches + this.currentChunkIndexInBatch; // 🔍 调试chunk索引计算 if ( developmentEnv === "development" && this.currentChunkIndexInBatch <= 5 ) { postLogToBackend( `[DEBUG-RESUME] 🧮 calculateGlobalChunkIndex - batchStartOffset:${this.currentBatchStartOffset}, batchesBefore:${batchesBefore}, chunksInPrev:${chunksInPreviousBatches}, chunkInBatch:${this.currentChunkIndexInBatch}, result:${result}` ); } return result; } /** * 📈 Calculate total network chunk count */ private calculateTotalNetworkChunks(): number { return Math.ceil(this.totalFileSize / this.NETWORK_CHUNK_SIZE); } /** * ⏭️ Update current processing state */ private updateChunkState(chunk: ArrayBuffer): void { this.currentChunkIndexInBatch++; this.totalFileOffset += chunk.byteLength; // Check if end of file has been reached if (this.totalFileOffset >= this.totalFileSize) { this.isFinished = true; } } /** * 🏁 Check if this is the last network chunk */ private isLastNetworkChunk(chunk: ArrayBuffer): boolean { return this.totalFileOffset + chunk.byteLength >= this.totalFileSize; } /** * 📊 Get reading progress information */ public getProgress(): { readBytes: number; totalBytes: number; progressPercent: number; currentBatchInfo?: { batchStartOffset: number; batchSize: number; chunkIndex: number; totalChunks: number; }; } { const progressPercent = this.totalFileSize > 0 ? (this.totalFileOffset / this.totalFileSize) * 100 : 0; const result = { readBytes: this.totalFileOffset, totalBytes: this.totalFileSize, progressPercent, } as any; if (this.currentBatch) { result.currentBatchInfo = { batchStartOffset: this.currentBatchStartOffset, batchSize: this.currentBatch.byteLength, chunkIndex: this.currentChunkIndexInBatch, totalChunks: Math.ceil( this.currentBatch.byteLength / this.NETWORK_CHUNK_SIZE ), }; } return result; } /** * 🔄 Reset reader state (for restarting reading) */ public reset(startOffset: number = 0): void { this.totalFileOffset = startOffset; this.isFinished = false; this.isReading = false; this.currentBatch = null; // 🔧 修复:reset时也要正确设置currentBatchStartOffset this.currentBatchStartOffset = startOffset; this.currentChunkIndexInBatch = 0; // 重置为0,loadNextBatch会重新计算 if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 🔄 StreamingFileReader reset - startOffset:${startOffset}` ); } } /** * 🧹 Cleanup and release resources */ public cleanup(): void { // Abort ongoing file reading if (this.isReading) { this.fileReader.abort(); } // Clean up memory this.currentBatch = null; this.isFinished = true; this.isReading = false; } /** * 🔍 Get debug information */ public getDebugInfo() { return { fileName: this.file.name, fileSize: this.totalFileSize, currentOffset: this.totalFileOffset, isFinished: this.isFinished, isReading: this.isReading, hasBatch: !!this.currentBatch, batchOffset: this.currentBatchStartOffset, chunkInBatch: this.currentChunkIndexInBatch, globalChunkIndex: this.calculateGlobalChunkIndex(), totalChunks: this.calculateTotalNetworkChunks(), }; } }