import { generateFileId } from "@/lib/fileUtils"; import { CustomFile, fileMetadata, WebRTCMessage, FileRequest, EmbeddedChunkMeta, } from "@/types/webrtc"; import { StateManager } from "./StateManager"; import { MessageHandler, MessageHandlerDelegate } from "./MessageHandler"; import { NetworkTransmitter } from "./NetworkTransmitter"; import { ProgressTracker, ProgressCallback } from "./ProgressTracker"; import { StreamingFileReader } from "./StreamingFileReader"; import { TransferConfig } from "./TransferConfig"; import WebRTC_Initiator from "../webrtc_Initiator"; import { postLogToBackend } from "@/app/config/api"; const developmentEnv = process.env.NODE_ENV; /** * ๐Ÿš€ File transfer orchestrator * Integrates all components to provide unified file transfer services */ export class FileTransferOrchestrator implements MessageHandlerDelegate { private stateManager: StateManager; private messageHandler: MessageHandler; private networkTransmitter: NetworkTransmitter; private progressTracker: ProgressTracker; constructor(private webrtcConnection: WebRTC_Initiator) { // Initialize all components this.stateManager = new StateManager(); this.networkTransmitter = new NetworkTransmitter( webrtcConnection, this.stateManager ); this.progressTracker = new ProgressTracker(this.stateManager); this.messageHandler = new MessageHandler(this.stateManager, this); // Set up data handler this.setupDataHandler(); this.log("log", "FileTransferOrchestrator initialized"); } /** * ๐ŸŽฏ Send file metadata */ public sendFileMeta(files: CustomFile[], peerId?: string): void { // Record file sizes belonging to folders for progress calculation files.forEach((file) => { if (file.folderName) { const fileId = generateFileId(file); this.stateManager.addFileToFolder(file.folderName, fileId, file.size); } }); // Loop to send metadata for all files const peers = peerId ? [peerId] : Array.from(this.webrtcConnection.peerConnections.keys()); peers.forEach((pId) => { files.forEach((file) => { const fileId = generateFileId(file); this.stateManager.addPendingFile(fileId, file); const fileMeta = this.getFileMeta(file); const metaDataString = JSON.stringify(fileMeta); const sendResult = this.webrtcConnection.sendData(metaDataString, pId); if (!sendResult) { this.fireError("Failed to send file metadata", { fileMeta, peerId: pId, }); } }); }); } /** * ๐ŸŽฏ Send string content */ public async sendString(content: string, peerId: string): Promise { const chunkSize = TransferConfig.FILE_CONFIG.CHUNK_SIZE; const chunks: string[] = []; for (let i = 0; i < content.length; i += chunkSize) { chunks.push(content.slice(i, i + chunkSize)); } // First send metadata await this.networkTransmitter.sendWithBackpressure( JSON.stringify({ type: "stringMetadata", length: content.length, }), peerId ); // Send chunks one by one using backpressure control for (let i = 0; i < chunks.length; i++) { const data = JSON.stringify({ type: "string", chunk: chunks[i], index: i, total: chunks.length, }); await this.networkTransmitter.sendWithBackpressure(data, peerId); } this.log( "log", `String sent successfully - length: ${content.length}, chunks: ${chunks.length}`, { peerId } ); } /** * ๐ŸŽฏ Set progress callback */ public setProgressCallback(callback: ProgressCallback, peerId: string): void { this.progressTracker.setProgressCallback(callback, peerId); } // ===== MessageHandlerDelegate Implementation ===== /** * ๐Ÿ“„ Handle file request (delegated from MessageHandler) */ async handleFileRequest(request: FileRequest, peerId: string): Promise { const file = this.stateManager.getPendingFile(request.fileId); const offset = request.offset || 0; if (!file) { this.fireError(`File not found for request`, { fileId: request.fileId, peerId, }); return; } await this.sendSingleFile(file, peerId, offset); } /** * ๐Ÿ“ Logging (delegated from MessageHandler) */ public log( level: "log" | "warn" | "error", message: string, context?: Record ): void { const prefix = `[FileTransferOrchestrator]`; console[level](prefix, message, context || ""); } // ===== Internal Orchestration Methods ===== /** * ๐ŸŽฏ Send single file */ private async sendSingleFile( file: CustomFile, peerId: string, offset: number = 0 ): Promise { const fileId = generateFileId(file); const peerState = this.stateManager.getPeerState(peerId); if (peerState.isSending) { this.log("warn", `Already sending file to peer ${peerId}`, { fileId }); return; } // Initialize sending state this.stateManager.updatePeerState(peerId, { isSending: true, currentFolderName: file.folderName, readOffset: offset, bufferQueue: [], isReading: false, }); // Initialize progress statistics const currentSent = this.stateManager.getFileBytesSent(peerId, fileId); this.stateManager.updateFileBytesSent(peerId, fileId, offset - currentSent); try { await this.processSendQueue(file, peerId); await this.waitForTransferComplete(peerId); } catch (error: any) { this.fireError(`Error sending file ${file.name}: ${error.message}`, { fileId, peerId, }); this.abortFileSend(fileId, peerId); } } /** * ๐Ÿš€ Process send queue - Using StreamingFileReader */ private async processSendQueue( file: CustomFile, peerId: string ): Promise { const fileId = generateFileId(file); const peerState = this.stateManager.getPeerState(peerId); const transferStartTime = performance.now(); // 1. Initialize streaming file reader const streamReader = new StreamingFileReader( file, peerState.readOffset || 0 ); if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ๐Ÿš€ Starting transfer - file: ${file.name}, size: ${( file.size / 1024 / 1024 ).toFixed(1)}MB` ); } try { let totalBytesSent = 0; let networkChunkIndex = 0; let totalReadTime = 0; let totalSendTime = 0; let totalProgressTime = 0; let lastProgressTime = performance.now(); // 2. Stream processing: Get 64KB network chunks one by one and send while (peerState.isSending) { // Get next network chunk const readStartTime = performance.now(); const chunkInfo = await streamReader.getNextNetworkChunk(); const readTime = performance.now() - readStartTime; totalReadTime += readTime; // Check if completed if (chunkInfo.chunk === null) { break; } // Build embedded metadata const embeddedMeta: EmbeddedChunkMeta = { chunkIndex: chunkInfo.chunkIndex, totalChunks: chunkInfo.totalChunks, chunkSize: chunkInfo.chunk.byteLength, isLastChunk: chunkInfo.isLastChunk, fileOffset: chunkInfo.fileOffset, fileId, }; // Send network chunk with embedded metadata let sendSuccessful = false; const sendStartTime = performance.now(); try { sendSuccessful = await this.networkTransmitter.sendEmbeddedChunk( chunkInfo.chunk, embeddedMeta, peerId ); if (sendSuccessful) { totalBytesSent += chunkInfo.chunk.byteLength; } } catch (error) { this.log( "warn", `Chunk send failed #${chunkInfo.chunkIndex}: ${error}` ); sendSuccessful = false; } const sendTime = performance.now() - sendStartTime; totalSendTime += sendTime; // Update state and progress if (sendSuccessful) { this.stateManager.updatePeerState(peerId, { readOffset: chunkInfo.fileOffset + chunkInfo.chunk.byteLength, }); const progressStartTime = performance.now(); await this.progressTracker.updateFileProgress( chunkInfo.chunk.byteLength, fileId, file.size, peerId, true ); const progressTime = performance.now() - progressStartTime; totalProgressTime += progressTime; } networkChunkIndex++; // Check if it's the last chunk if (chunkInfo.isLastChunk) { break; } } if (developmentEnv === "development") { const totalTime = performance.now() - transferStartTime; const avgSpeedMBps = totalBytesSent / 1024 / 1024 / (totalTime / 1000); const expectedTotalChunks = Math.ceil(file.size / 65536); const startOffset = peerState.readOffset || 0; const startChunkIndex = Math.floor(startOffset / 65536); const expectedChunksSent = expectedTotalChunks - startChunkIndex; postLogToBackend( `[DEBUG-CHUNKS] โœ… Transfer complete - file: ${file.name}, time: ${( totalTime / 1000 ).toFixed(1)}s, speed: ${avgSpeedMBps.toFixed(1)}MB/s` ); postLogToBackend( `[DEBUG-CHUNKS] Chunks sent: ${networkChunkIndex}, expected: ${expectedChunksSent}, startChunk: ${startChunkIndex}, totalFileChunks: ${expectedTotalChunks}` ); if (networkChunkIndex !== expectedChunksSent) { postLogToBackend( `[DEBUG-CHUNKS] โš ๏ธ CHUNK MISMATCH: sent ${networkChunkIndex} but expected ${expectedChunksSent}` ); } } } catch (error: any) { const errorMessage = `Streaming send error: ${error.message}`; if (developmentEnv === "development") { postLogToBackend(`[DEBUG] โŒ Transfer error: ${errorMessage}`); } this.fireError(errorMessage, { fileId, peerId, offset: peerState.readOffset, }); throw error; } finally { // Clean up resources streamReader.cleanup(); } } /** * โณ Wait for transfer completion confirmation */ private async waitForTransferComplete(peerId: string): Promise { while (true) { const currentPeerState = this.stateManager.getPeerState(peerId); // Check if it has been cleaned up or does not exist if (!currentPeerState || !currentPeerState.isSending) { this.log("log", `Transfer completed or peer disconnected: ${peerId}`); break; } // Check if the WebRTC connection is still valid if (!this.webrtcConnection.peerConnections.has(peerId)) { this.log("log", `Peer connection lost: ${peerId}, stopping transfer`); this.stateManager.updatePeerState(peerId, { isSending: false }); break; } await new Promise((resolve) => setTimeout(resolve, 100)); } } /** * ๐Ÿ“‹ Get file metadata */ private getFileMeta(file: CustomFile): fileMetadata { const fileId = generateFileId(file); return { type: "fileMeta", fileId, name: file.name, size: file.size, fileType: file.type, fullName: file.fullName, folderName: file.folderName, }; } /** * โŒ Abort file sending */ private abortFileSend(fileId: string, peerId: string): void { this.log("warn", `Aborting file send for ${fileId} to ${peerId}`); this.stateManager.resetPeerState(peerId); } /** * ๐Ÿ”ง Set up data handler */ private setupDataHandler(): void { this.webrtcConnection.onDataReceived = (data, peerId) => { if (typeof data === "string") { try { const parsedData = JSON.parse(data) as WebRTCMessage; this.messageHandler.handleSignalingMessage(parsedData, peerId); } catch (error) { this.fireError("Error parsing received JSON data", { error, peerId }); } } }; } /** * ๐Ÿ”ฅ Error handling */ private fireError(message: string, context?: Record) { this.webrtcConnection.fireError(message, { ...context, component: "FileTransferOrchestrator", }); } // ===== State Query and Debugging ===== /** * ๐Ÿ“Š Get transfer statistics */ public getTransferStats(peerId?: string) { const stats = { stateManager: this.stateManager.getStateStats(), progressTracker: peerId ? this.progressTracker.getProgressStats(peerId) : null, networkTransmitter: peerId ? this.networkTransmitter.getTransmissionStats(peerId) : null, }; return stats; } /** * ๐Ÿ”„ Handle peer reconnection */ public handlePeerReconnection(peerId: string): void { // Clear all transfer states for this peer this.stateManager.clearPeerState(peerId); if (developmentEnv === "development") this.log( "log", `Successfully reset transfer state for reconnected peer ${peerId}` ); } /** * ๐Ÿงน Clean up all resources */ public cleanup(): void { this.stateManager.cleanup(); this.networkTransmitter.cleanup(); this.progressTracker.cleanup(); this.messageHandler.cleanup(); if (developmentEnv === "development") this.log("log", "FileTransferOrchestrator cleaned up"); } }